/* LotJitsu marketing site stylesheet.
 *
 * Hand-written, dependency-free. This file replaced the exported Webflow CSS bundle in the
 * 2026-08 clean rebuild: one stylesheet, design tokens up top, plain BEM-ish class names.
 * Fonts load from Google Fonts (Inter Tight for headings, Rethink Sans for body) via a
 * <link> in each page head; self-hosting them is a candidate follow-up.
 */

:root {
  /* EVERY FORM FIELD ON THE SITE (owner, 2026-10-10: "All form fields on the marketing site should
     have the background color #f1f1f1", then "Let's try #ffffff instead"). One token, read by every input, textarea, select and
     search box below, and mirrored into Stripe's card form (STRIPE_APPEARANCE in site.js), which
     sits in an iframe this file cannot reach. Not redeclared under .inverted on purpose. */
  --field-bg: #ffffff;
  /* THE ONE ORANGE (owner, 2026-11-04, pointing at a guide step's coral: "This color orange needs
     to be changed to our primary action color orange, everywhere. This goes for dark and light
     themes"): #CB5F48, the wordmark's Coral Orange, the header avatar and every dark-mode primary
     button. It was #be624b here and a brightened #d17b63 under .inverted; both are this now, so
     the same coral paints links, the active nav item, step markers, the coral band, the Log In
     and Sign Up submits and the Monthly/Yearly pill in both themes. --coral-hover is untouched. */
  --coral: #cb5f48;
  --coral-hover: #a8533e;
  --coral-soft: #cb5f48;
  --teal: #48ac9c;
  --sky: #69a5cb;
  --green: #349744;
  --ink: #1d1a18;
  /* Button chrome: the fill an outline button flips to on hover, and the pill chrome.
     Owner-set #232323 on 2026-08-12, a touch lighter than --ink so button edges read as
     chrome rather than as heading ink. Separate from --ink precisely so heading colour and
     button colour can move independently. Redefined on .inverted below. */
  --btn-ink: #232323;
  /* The border and label of an outline button at rest (owner, 2026-09-14; it was --btn-ink).
     The owner's exact value, one unit off --muted: a separate role, so keep them apart.
     Redefined on .inverted and restored in the light island below. */
  --btn-outline: #6a6361;
  --body-text: #3d3835;
  --muted: #6a6360;
  --surface: #edebe8;
  --band: #e9e1de;
  --band-deep: #e7e0d9;
  --card: #ffffff;
  --dark: #232323;
  --dark-2: #2e2c2a;
  --line: #e2dad3;
  /* The rest border of a secondary/outline control inside a modal. The app's --lj-line-hover,
     ported with the Log In modal (owner, 2026-09-21) for its Cancel button so the two surfaces
     match. Deliberately NOT --btn-outline (#6a6361), a different role and a different colour. */
  --line-hover: #b7b1ab;
  --line-dark: #3d3b39;
  /* Vertical rhythm for a top-level <section>. Half the gap, not the gap: two neighbours
     each contribute this, so 30 + 30 = the 60px boundary the whole site is set to. It was
     72px, a 144px gap, until 2026-08-23, when the owner took the rhythm /how-it-works (renamed
     /features 2026-09-19) and /platforms had been tuned to and made it the site default.
     Still a token rather than a literal so one page could retune from a single line on <body>
     without out-specificity-ing the base rule and then re-beating it inside the 850 query. No
     page does that today. */
  --section-pad: 30px;
  --radius: 14px;
  --radius-lg: 22px;
  --shadow: 0 10px 30px rgba(29, 26, 24, 0.08);
  --font-head: "Inter Tight", -apple-system, "Segoe UI", sans-serif;
  --font-body: "Rethink Sans", -apple-system, "Segoe UI", sans-serif;
}

* { box-sizing: border-box; }
/* While the window is resizing, kill all transitions: crossing the mobile
   breakpoint otherwise ANIMATES the nav from its desktop row state into the
   drawer's off-screen state, flashing a phantom slide-out (owner-reported,
   2026-08-09). site.js adds the class on resize, removes it 200ms after. */
.resize-animation-stopper * { transition: none !important; }
html { scroll-behavior: smooth; }
body {
  margin: 0;
  font-family: var(--font-body);
  font-size: 18px;
  /* 1.4444 = 26/18: the body's 18px text on a 26px line (owner, 2026-10-06: "reduce the
     line-height of all text where the line-height is roughly 30px to 24px", then the same day
     "Change all of those line-heights modified to 26px"). It was 1.65, a 29.7px line. A ratio, not 24px, so larger inheriting text (the phone's 20px body copy, the 19.2px
     lead) scales with it instead of being crammed. Headings set their own tight ratios and are
     untouched. Marketing only, by the owner's choice: the app's /pricing and its copies of the
     Log In, Sign Up and Terms dialogs stay at 1.65 for now, a known D-060 difference. */
  line-height: 1.4444;
  color: var(--body-text);
  background: var(--surface);
  -webkit-font-smoothing: antialiased;
}
img { max-width: 100%; height: auto; display: block; }
a { color: var(--coral); text-decoration: none; }
a:hover { color: var(--coral-hover); }

h1, h2, h3, h4 {
  font-family: var(--font-head);
  color: var(--ink);
  line-height: 1.15;
  margin: 0 0 0.5em;
  font-weight: 600;
  letter-spacing: -0.015em;
}
h1 { font-size: 48px; } /* owner, 2026-08-12; was 50px */
/* A PAGE TITLE, not a display heading (owner, 2026-09-17). 28px, where the base h1 above is
   the 48px a marketing hero wants. An explicit class rather than a structural selector like
   `section:not(.hero) h1`: that shape also matches a blog post's title and a help article's
   title, which are article headings and stay large. Carried today by /faq, /404 and the
   internal /sections catalog at 28px, and by /blog and /support at the larger size below. */
.page-title { font-size: 28px; }
/* /support's and /blog's titles match the app's Pricing page title (owner, 2026-09-19: "needs
   to be the same style as the page title on our pricing page"). That title is
   "Compare Plans" (title case since 2026-09-21; sentence case before, and it read
   "Pricing" for part of that day),
   `.lj-plans .cmp__title` in alpha's apps/web/src/app/pricing/plans.css: the head font, --ink,
   600, 1.15, -0.015em and 34px. The base `h1, h2, h3, h4` rule above already supplies the
   first five with the same values, so the size is the only difference and the only thing set
   here. If that title's look changes in the app, this follows it. Must stay BELOW .page-title:
   the two have equal specificity and the later one wins. */
.page-title--lg { font-size: 34px; }
/* THE LEGAL DOCUMENT'S OWN H1 IS THE PAGE TITLE (owner, 2026-10-03). /terms and /privacy show
   the GitHub documents exactly as delivered, and each document opens with its own h1
   ("LotJitsu Terms of Service"). The page no longer writes one, so that h1 is the page's one
   h1 and takes the look the hand-written title had: `page-title page-title--lg`, which is this
   34px. It is styled by POSITION, inside the container the generator bakes and site.js refills,
   because the document's markup is not ours to put a class on. (0,2,1), so it beats the base
   h1's 48px from anywhere. The 37px under it is the same rule the old title used; see the
   selector list on `.page-title--spaced` below. */
.prose--legal [data-legal-doc] h1 { font-size: 34px; }
/* Flat 34px, owner-set 2026-08-12. This replaced clamp(1.7rem, 3vw, 2.3rem), so an h2 no
   longer scales with the viewport: it used to render 36.8px on a wide desktop and floor at
   27.2px from roughly 907px down, which meant every phone got the 27.2px end. It is now the
   same 34px everywhere. The two components that set their own h2 size, the pricing card
   heading at 20px and the footer column headings at 1.125rem, are unaffected. */
h2 { font-size: 34px; }
/* /support's "Frequently Asked Questions" is 6px smaller (owner, 2026-10-01, as 4 then 2 more):
   28px, from the scale's 34px, at every width. `#faq` is on that page's FAQ band and nowhere
   else. */
#faq h2 { font-size: 28px; }
h3 { font-size: 24px; } /* owner, 2026-08-12; was 1.25rem (20px) */
/* A PAGE SUBTITLE: the base h2 less 8px (owner, 2026-09-21 - 6px, then 2px more; asked for on
   /about-us, which is
   the only page carrying it today). It is a CLASS on each heading rather than a rule scoped to
   the page, because four of that page's h2s belong to shared chrome - the closing band at
   60px, the download card, the footer columns and the signout dialog - and a page-wide h2 rule
   would outrank or tie every one of them and shrink the lot. Written as 28 rather than as a
   calculation because the base is a flat value someone may change; if 34 moves, this is a
   separate decision, not an automatic follow-on. */
.page-subtitle { font-size: 26px; }
/* THE TWO LEGAL DOCUMENTS' NUMBERED SECTION HEADINGS, 10px off the base h2 (owner,
   2026-09-21). /terms and /privacy are one pair: the owner asked for it on Terms and it goes
   on both, because they are structurally identical twins and a 10px gap between them would
   read as a bug. It lands at h3's 24px, which is deliberate - these headings number the
   clauses of a long document rather than open a marketing section, so they want the quieter
   step. Scoped to a prose MODIFIER, not to .prose itself: blog posts and help articles use
   the same .prose container and their h2s stay at 34px. (0,1,1) against the base h2's
   (0,0,1), so it wins from anywhere in the file. Still far above the 18px floor. */
.prose--legal h2 { font-size: 24px; }
/* 20px more air under the legal pages' title (owner, 2026-09-21), asked for on Terms and
   matched on Privacy. 17px is what the title carried, so this is 17 + 20 rather than a round
   number picked for its own sake - written as the sum it is. The h2 below it has margin-top 0,
   so nothing collapses into this and the declared value is the gap you see. Scoped to the
   legal modifier, because .page-title carries no single value to shift: measured, /404 is on
   17px, /support 22px and /blog 0, each set by its own page. Only these two move. */
/* /about-us joined them on 2026-09-21 via the modifier, which is why this is one rule with
   two selectors rather than two rules: 37px is a number the owner chose, and a second copy of
   it somewhere else is how the two quietly drift apart. Add the selector, not the value. */
.prose--legal .page-title,
.prose--legal [data-legal-doc] h1,
.page-title--spaced { margin-bottom: 37px; }
p { margin: 0 0 1em; }
.lead { font-size: 1.2rem; color: var(--muted); }

.container { max-width: 1140px; margin: 0 auto; padding: 0 24px; }
.container--narrow { max-width: 1140px; }
/* Left-aligned prose spans the full container: capping it at 780px left a
   large dead gap on the right (owner-reported, 2026-08-08). Only the
   deliberately CENTERED blocks keep the narrow readable measure, where the
   gap is symmetric and reads as intent. */
.container--centered > * {
  max-width: min(780px, 100%);
  margin-inline: auto;
}
/* The coral CTA band is the exception among the centred blocks. Owner,
   2026-08-14: its content lines up with the header and the footer rather than
   taking the 780px measure, so the band reads as part of the page grid instead
   of a narrower column floating inside it. Its children therefore fill the
   1140px container, and the centring comes from text-align plus the
   `.band--coral .btn-row` rule. .container--narrow stays on as the hook. */
.band--coral .container--narrow > * {
  max-width: 100%;
}
section { padding: var(--section-pad) 0; }
section.tight { padding: 44px 0; }
/* A TITLED PAGE'S FIRST SECTION (owner, 2026-09-21): all page titles sit where Compare
   Plans does. That is the app's /pricing h1, whose box top lands 26px under the header:
   19px of section padding plus the 7px of half-leading it picks up from being centred against
   the taller billing toggle in its row. There is no toggle here, so the h1 sits at the content
   start and the whole 26px is padding. Carried by the seven pages that lead with a page title;
   the marketing-hero pages keep their own air (owner: title pages only).
   section.page-head and section.tight have EQUAL specificity (0,1,1), so this rule must stay
   BELOW that one to win. It also outranks .hero (0,1,0) from anywhere, including the 850px
   block that restates .hero's padding, so a page head is 26px on a phone too. Any inline
   padding-top on these sections was removed rather than left to outrank the class. */
section.page-head { padding-top: 26px; }
/* /about on desktop (owner, 2026-09-30): 40px less under the H1 and 30px less under the intro
   paragraph. The gap under the H1 was the head's 30px bottom padding plus the intro's 30px top
   padding, so the head's goes to 0 and the intro's to 20 (30 + 30 - 40 = 20); the gap under the
   paragraph was the intro's 30px bottom padding plus Leadership's 30px top, so the intro's goes
   to 0. The phone block restores the intro's 30px/30px, because its own spacing was set
   separately. */
section.about-head { padding-bottom: 0; }
section.about-intro { padding-top: 20px; padding-bottom: 0; }
/* "Leadership" (owner, 2026-09-30): 15px more under it, its 13px margin to 28. */
.page-subtitle { margin-bottom: 28px; }
/* The target is the GAP BETWEEN two sections, not the padding of one. Two things feed that
   gap: both neighbours contribute padding, and the last block inside a section contributes its
   own trailing margin, which is 18px on a <p> and 0 on an <img>/gallery. That asymmetry is why
   the gap measured 138px after a paragraph and 120px after a figure before any of this landed.
   So zero the trailing margin first, then let each side give half: 30 + 0 + 30 = 60px on every
   boundary, whatever the section happens to end with.

   Owner-tuned on /how-it-works (renamed /features 2026-09-19) 2026-08-20, extended to
   /platforms 2026-08-23, made the site's rhythm the same day. Global now, so there are no
   page-scoped copies to keep in step and no sibling chain anchored on one id.

   .hero and section.tight set their own padding and stay out of it, so a masthead boundary and
   a ticker boundary are deliberately not 60px. */
section > .container > *:last-child { margin-bottom: 0; }

.eyebrow {
  font-family: var(--font-head);
  font-size: 1.3rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--coral);
  margin-bottom: 14px;
}

/* The 404 numeral. Owner-set at 80px bold (2026-08-11); larger than the h1
   below it on purpose. line-height 1 keeps the tall type from opening a gap. */
.eyebrow--code {
  font-size: 80px;
  font-weight: 700;
  line-height: 1;
}

/* ------------------------------------------------------------------ buttons */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  font-family: var(--font-head);
  font-weight: 600;
  font-size: 1.125rem;
  border-radius: 10px;
  padding: 13px 26px;
  border: 2px solid transparent;
  cursor: pointer;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}
/* #C16850, a literal, NOT --coral (owner, 2026-09-17). `var(--coral)` is read in 58 places:
   every link, the nav's hover and current-page state, focus rings, the coral band. Repointing
   the token to restyle one button would have recoloured the whole site, so the button carries
   its own value and the token is untouched.
   CONTRAST: white on #C16850 is 3.92:1, under the 4.5:1 floor for this label (18px, weight 500,
   which is not large text). The --coral it replaces was 4.17:1, also under. So this is not a
   new failure, but it is a slightly deeper one. Hover is #C4573A at 4.40:1, so hover is still
   the more readable state and still misses by 0.1.
   Nothing here clears the floor. If that is wanted, #B9503A measures 4.92:1 with white; the
   palette would shift noticeably redder, which is presumably why it has never been done.
   Nothing in `npm run check` measures contrast, so this comment is the record. */
.btn--primary { background: #c16850; color: #fff; }
/* #C4573A, a literal, NOT --coral-hover (owner, 2026-09-17, as #D05A3A first). The token stays
   #a8533e because five other things read it: `a:hover` on every page, the coral band's inverted
   button pair, and the drawer's primary pill. Repointing the token to move one button would
   have taken link hover with it.
   CONTRAST: white on #C4573A is 4.40:1, a hair under the 4.5:1 floor. The button's label is
   18px at weight 500, which is NOT large text (that needs 24px, or 18.66px at 700), so 4.5:1 is
   the bar and this misses it by 0.1. Closer than the #D05A3A tried first (4.03:1) and better
   than the RESTING coral, which is 4.17:1 and has been under the floor all along, so hover
   improves on rest again. The --coral-hover this rule used to read was 5.29:1 and passed.
   #C1553A, one step darker, measures 4.54 and would clear it outright if that ever matters
   more than the exact hue.
   Nothing in `npm run check` measures contrast, so this comment is the record. */
.btn--primary:hover { background: #c4573a; color: #fff; }
/* Rests in --btn-outline, a mid grey (owner, 2026-09-14); the hover still fills with --btn-ink. */
.btn--outline { border-color: var(--btn-outline); color: var(--btn-outline); background: transparent; }
.btn--outline:hover { background: var(--btn-ink); color: #fff; }
.btn--grey { background: #6a6360; color: #fff; }
.btn--grey:hover { background: #57514d; color: #fff; }
.btn--ghost-light { border-color: rgba(255, 255, 255, 0.7); color: #fff; background: transparent; }
.btn--ghost-light:hover { background: rgba(255, 255, 255, 0.12); color: #fff; }
.btn-note { font-size: 1.125rem; color: var(--muted); margin-top: 8px; }
.btn-row { display: flex; gap: 14px; flex-wrap: wrap; align-items: center; }
/* The App Store and Google Play badges in the home hero, centred under the laptop-and-phone
   image (owner, 2026-09-19; under the CTA row for the first hour). Both files are the stores'
   official artwork and are never edited; all the fitting is here.
   `--badge-h` is the height of the VISIBLE badge. Apple's SVG is the badge edge to edge, so it
   simply takes that height. Google's PNG is a 564x168 badge inside 646x250 of transparent
   padding, 41px a side, so the image is scaled by 250/168 to bring the badge itself to
   `--badge-h`, and pulled back by 41/168 of it on every side so the padding takes no room.
   The 16px gap is more than the quarter-height clear space both sets of rules ask for. The
   height is 52px, easing down with the viewport below 416px so the pair still fits on one row
   of a phone (side by side they need 346px at 52px, and a 390px phone offers 342px); the 40px
   floor is Apple's on-screen minimum, and at it the pair fits a 320px screen. A clamp rather
   than a width query, because the stylesheet's five are fixed. */
.store-badges { --badge-h: clamp(40px, 12.5vw, 52px); display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: 16px; margin-top: 28px; }
/* The 28px top margin was tuned for the hero's pair (10 of it restored the clear space that
   `.hero-mock`'s 10px relative nudge would otherwise eat). That pair was REMOVED on 2026-09-19
   (owner: "Remove the app store and google play buttons in the hero"), and both surviving
   placings set their own margin-top (the footer 16px, the download card 0), so this value now
   reaches nothing live. It stays as the component's default for a bare storeBadges() call. */
/* The two badges are ONE SHAPE, so this block is arithmetic-free (owner, 2026-09-19, who
   supplied both as matched 840x252 PNGs after asking for buttons of equal size). Until then
   Apple's SVG was 119.664x40 (2.99:1) and Google's PNG 646x250 with 41px of transparent
   padding round a 564x168 badge (3.36:1), which is why this block used to carry a shared
   letterbox, a per-badge width and a negative-margin pull to cancel that padding. The supplied
   pair is edge to edge, same pixels, same ratio, so height alone sizes both and they match by
   construction rather than by sums that a new asset would quietly invalidate.
   REPLACING EITHER: keep them the same dimensions as each other, and keep the artwork itself
   unaltered, which is what both badge programmes require and why the footer carries the
   trademark credits. */
/* A white glow around the badge under the cursor (owner, 2026-09-19). Two earlier readings were
   tried and dropped the same day: darkening it with `filter: brightness()`, then resting the
   pair at 0.8 opacity and lifting to full. Both dimmed the badge to say "not hovered"; this one
   leaves every badge at full strength and adds light to the one being pointed at.
   A `box-shadow` outside the box, so the artwork is untouched (see the note above) and nothing
   reflows: the glow is painted beyond the border edge and the row's geometry is unchanged. It
   follows the link's own 9px radius, which is why that radius is on the link and not just in the
   picture. Both places it appears are dark grounds, the download card and the footer, so white
   reads as light rather than as a grey ring. Tuned down from 12px/2px/0.5 to 10px/1px/0.3 the
   same day ("make the glow more subtle"): the spread is what made it a halo with an edge, so
   dropping it to 1px and softening the alpha leaves light without a rim.
   NOT on `:focus-visible`, which keeps the 3px coral outline below: that is a stronger and more
   conventional focus signal than a glow, and stacking the two would double the ring.
   0.25s is the site's control-hover tier, the one the header's fill uses, and it is ungated for
   reduced motion on that rule's own reasoning: the preference is about movement, and nothing
   here moves. */
.store-badges__link {
  display: block;
  line-height: 0;
  border-radius: 9px;
  transition: box-shadow 0.25s ease;
}
.store-badges__link:hover { box-shadow: 0 0 10px 1px rgba(255, 255, 255, 0.3); }
.store-badges__link img {
  display: block;
  height: var(--badge-h);
  width: auto;
  aspect-ratio: 840 / 252;
}
.store-badges__link:focus-visible { outline: 3px solid var(--coral); outline-offset: 3px; }
/* The footer pair, under the tagline (owner, 2026-09-19). Two differences from the home page's
   pair, and nothing else: it is 20% smaller (the owner's number, applied to the whole clamp so
   the shrink holds at every width, floor and ceiling alike: 32px to 41.6px against 40px to
   52px), and it aligns left with the column rather than centring, because the footer's first
   column is left-aligned. The gap comes down with the badges so the clear space stays the same
   fraction of their height; 13px is still over the quarter-height both sets of brand rules ask
   for at this size.
   THE BADGES ARE THE SAME OFFICIAL ART, unaltered: Apple's black "Download on the App Store"
   SVG and Google's "Get it on Google Play" PNG, the DARK variants, which is what the owner's
   two reference images show and what the dark footer wants. Neither may be recoloured, redrawn
   or have its proportions changed under the two badge programmes' rules, which is also why the
   footer carries the trademark credits below (`.site-footer__marks`). 32px is under Apple's
   40px on-screen minimum at the clamp's floor, which only bites below 320px of viewport. */
.store-badges--footer { --badge-h: clamp(32px, 10vw, 41.6px); justify-content: flex-start; gap: 13px; margin-top: 16px; }
/* The app-download switch (owner, 2026-10-19; site.js, "app-download switch") hides this pair
   with the hidden attribute, which `.store-badges`'s display:flex would otherwise beat. The
   download cards need no rule: they are hidden by their <section>, which sets no display. */
.store-badges--footer[hidden] { display: none; }
/* The footer's Light/Dark pill (owner, 2026-11-04: "Add a Light/Dark pill toggle in the footer,
   below the app store buttons"). Two segments in one ring, the pressed one filled with the logo's
   coral like the header avatar and the pricing page's Monthly/Yearly pill. The footer is charcoal
   in both themes, so one set of literals serves both. THE PRESSED SEGMENT IS DRAWN FROM THE <html>
   CLASS, not from aria-pressed: theme.js puts the class there before first paint and site.js only
   syncs aria-pressed once it has loaded, so reading the class means no frame with the wrong
   segment lit.
   SIZED AT 75% (owner, 2026-11-04: "Decrease size of this button by 50%", then "40% and 10px
   labels", then "that's too small, I should have said 25% and 14px"): the ring's 4px padding
   and the segments' 7px/18px are scaled by 0.75, to 3px and 5.25px/13.5px, and the label is a
   flat 14px at every width, where it had inherited the footer's 18px (20px under 850px). It
   measured 159 x 45px at full size; it measures about 122 x 35px. */
.theme-toggle {
  display: inline-flex;
  /* 48px below the store badges: the 18px it opened at plus 20 (owner, 2026-11-04: "Move it
     down 20px from the store buttons"), then "10px more". */
  margin-top: 48px;
  padding: 3px;
  border: 1px solid rgba(255, 255, 255, 0.22);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.06);
  /* THE RING IS THE BUTTON (owner, 2026-11-04: "Clicking anywhere on the light/dark toggle
     button in the footer should make it toggle"): one <button role="switch">, so these undo the
     browser's button font and colour; the two labels inside are spans. */
  font: inherit;
  color: inherit;
  cursor: pointer;
}
/* 14px is UNDER the site's 18px floor, so it carries the dated approval the floor check wants
   (approved by the owner, 2026-11-04: "25% and 14px", for the footer's Light/Dark pill); the
   same 14px as the footer's copyright and trademark lines beneath it. */
.theme-toggle__option {
  font-family: var(--font-head);
  font-weight: 500;
  font-size: 14px;
  line-height: 1.2;
  padding: 5.25px 13.5px;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: #d8d2cc;
  cursor: pointer;
}
.theme-toggle:hover .theme-toggle__option { color: #fff; }
.theme-toggle:focus-visible { outline: 2px solid #cb5f48; outline-offset: 2px; }
html:not(.theme-dark) .theme-toggle__option[data-theme-choice="light"],
html.theme-dark .theme-toggle__option[data-theme-choice="dark"] {
  background: #cb5f48;
  color: #fff;
}
/* The download card's pair, the largest of the three (owner, 2026-09-19: "larger versions of
   the App Store and Google Play buttons"). 64px at desktop, 23% over the hero's 52, and it is
   the whole reason the card is 134px tall: 134 less the card's 1px top and bottom border and
   its 34px top and bottom padding is 64, so the badges set the row's height and the two text
   lines (59px together, see `.card--download`) sit inside it. The pair sits hard right in the
   row, but not through anything on this rule: it is content-sized (`flex: 0 0 auto`, below),
   so its own `justify-content` would have no free space to act on, and the right-justification
   comes from `justify-content: space-between` on `.card--download`, which pushes the copy to
   the left edge and the pair to the right. Once the 850px block stacks the card and stretches
   the pair to full width there IS free space, and the `justify-content: center` it inherits
   from `.store-badges` (restated in that block) is what centres the two rows there.
   THE CLAMP keeps this pair larger than the hero's at every width, which is the brief: 64px
   from 400px of viewport up, then 16vw down to a 52px floor (the hero's desktop CEILING) from
   325px down, against the hero's 12.5vw slope to 40px. Below 416px the hero pair and this one
   used to resolve to the same 12.5vw and so the same size one screen apart; now at a 390px
   phone this pair is 64px to the hero's 48.75. The card can afford it because its pair is
   already two rows below 541px (see the 850px block), so only ONE badge has to fit the card's
   inside width, 222px at 320px of viewport, and a 52px badge is 173.33px wide. Not a flat
   64px: that badge is 213.33px, 4px a side inside 222, and `img { max-width: 100% }` against
   a fixed height would squash it if anything nudged that width. A clamp rather than a width
   query because the stylesheet's five are fixed. The 28px top margin `.store-badges` carries
   is the hero's (it restores clear space under the nudged device shot) and is zeroed here,
   where the card's flex row does the centring; `flex: 0 0 auto` keeps the row from shrinking
   the pair when the heading beside it is long. At the 64px height a badge is
   64 x 840/252 = 213.33px wide, so the pair with its 16px gap is 442.67px. */
.store-badges--download { --badge-h: clamp(52px, 16vw, 64px); flex: 0 0 auto; margin-top: 0; }

/* Sales Chat (salesChat() in site-template.mjs, owner 2026-09-14). Both faces sit in ONE grid
   cell, so the button is sized by the wider label and never shifts beside the primary CTA when
   site.js flips data-sales-chat to "chat". The hidden face is visibility:hidden, not
   display:none: it keeps its width in the cell, and it drops out of the accessibility tree, so
   the link's accessible name is only ever the label on screen. */
/* The Ask Sales button's RESTING state is now IDENTICAL to its hover (owner, 2026-09-17):
     label and icon   #F1F1F1
     fill and border  #48AC9C
   Two consequences, both deliberate on the owner's word and both worth knowing before this is
   touched again.
   1. Hover fills with #0EA38B and keeps this border, so pointing at the button does something
      again; see the rule below. The two states were briefly identical and hover was a no-op.
   2. THE LABEL FAILS AA AT REST, which is the state the button is in almost all the time.
      #F1F1F1 on #48AC9C measures 2.43:1 against the 4.5:1 floor for normal text. Hover is
      better at 2.80:1 and still short. Before these two changes the resting state read 9.72:1
      and only hover failed; that is now inverted. A keyboard visitor tabbing to the button
      gets the resting state. Clearing the floor needs roughly #4a7d74 or darker behind a
      #F1F1F1 label.
   The icon is currentColor, so naming the colour once covers it and the label, and both faces
   are inside the link, so it covers "Sales Chat" too once site.js flips the button.
   THE WALK, since nothing in `npm run check` measures contrast and this comment is the only
   record: the label went #13302B, #0A493F, #052721, #08322B, #3D3835 and now #F1F1F1; the
   border went the shared `.btn--outline` grey, #0A493F, #37877A, #052721, #08322B, #3D3835 and
   now #48AC9C; the fill went none, a #5EC0B0 tint at 0.2, none again, and now #48AC9C solid.
   A mid-teal #5EC0B0 LABEL was tried on 2026-09-16 and reverted at 1.83:1, which is what
   started the habit of recording the number beside the value. */
.btn--outline.sales-chat {
  color: #f1f1f1;
  background: #48ac9c;
  border-color: #48ac9c;
}
/* Hover fills with #0EA38B (owner, 2026-09-17; briefly #3A8B7E, a two-shade step down from the
   resting colour, and #48AC9C before that). A more saturated teal than the resting #48AC9C
   rather than a straight darkening of it. The BORDER stays at the resting #48AC9C, because
   every ask here has been about the background: the effect is a lighter ring around a more
   saturated fill. Say the word and the border follows.
   CONTRAST: #F1F1F1 measures 2.80:1 on this fill, against the 4.5:1 floor for normal text.
   That is better than the 2.43:1 the resting state carries and worse than the 3.59:1 of the
   #3A8B7E it replaces, so hover is still the more readable of the two states and both still
   fail. The resting state is the one on screen almost all the time and the one a keyboard
   visitor lands on. #4a7d74 or darker is what a #F1F1F1 label needs to clear the floor.
   Scoped to the OUTLINE variant, which is only ever the homepage hero's button: /about-us and
   the blog CTA band ask for `salesChat("ghost-light")` and keep their own hover. */
.btn--outline.sales-chat:hover {
  background: #0ea38b;
  border-color: #48ac9c;
  color: #f1f1f1;
}
.sales-chat { display: inline-grid; place-items: center; }
.sales-chat__face { grid-area: 1 / 1; display: inline-flex; align-items: center; gap: 8px; }
.sales-chat__icon { flex: none; }
.sales-chat__face--chat,
.sales-chat[data-sales-chat="chat"] .sales-chat__face--ask { visibility: hidden; }
.sales-chat[data-sales-chat="chat"] .sales-chat__face--chat { visibility: visible; }
/* The wait between a Sales Chat click and Beacon's panel (site.js sets aria-busy for up to five
   seconds, then falls back to the form): the pointer says so, since the face does not change. */
.sales-chat[aria-busy="true"] { cursor: progress; }

/* Both modals below are native <dialog>s. Where the element is unsupported it is a plain block,
   and the UA stylesheet's dialog:not([open]) { display: none } that hides a closed one is missing
   with it, so the Ask Sales form would sit inline after the footer on every page carrying a
   Sales Chat button, and the Sign Out dialog on every page with the header (all but /footer).
   site.js already leaves such a browser its committed links; this restates the UA rule so the
   closed markup stays out of the page there too. */
dialog:not([open]) { display: none; }

/* The Ask Sales form modal (salesDialog() in site-template.mjs, owner 2026-09-14).
   RESTYLED TO THE LOG IN MODAL 2026-09-21 (owner: "Change the 'Ask Sales' modals on the
   marketing site to match the new modal style"). Its rules are now a port of the .login-dialog
   block further down this file and live beside it there, not here; what is left above that
   block is only the shared dark palette, which this modal no longer draws from.
   WHAT THIS REPLACED, recorded because the old design is cited elsewhere in this file: a dark
   --surface panel with the title top left and a close X top right, a full-width 56px pill Send
   button, 18px type throughout, and a backdrop of black at 70% over a 12px blur. It mirrored
   the app's SupportMessageModal on the web desktop (apps/web/src/modules/support/components/
   support-message-modal.tsx in the alpha repo).
   THAT MIRROR IS DELIBERATELY BROKEN NOW. SupportMessageModal is an authenticated in-app
   widget, not a marketing-branded surface, so the identical-surfaces rule never covered it -
   alpha's D-060 says "Ask Sales and Sign Out were not in scope" - and the owner asked for this
   modal to match the marketing site's own new style instead. Nothing on the app side changes;
   the two are simply no longer the same design, on purpose. */
/* ---- the modal palette: what SIGN OUT is made of ----
   This was "every modal on this site is dark, on every page" (owner, 2026-09-15), taken from the
   app's own dialog so the two products' modals match — apps/web/src/components/ui/dialog.tsx is
   `dark:bg-[#1a1a1a] dark:border dark:border-[#333]`, and the field, close-button and text
   values below are the app's dark-mode palette (its CLAUDE.md, "Standard Color Palette").

   THAT IS NO LONGER THE SITE'S RULE, and this block is down to ONE user. Log In arrived light on
   2026-09-21, ported rule-for-rule from the app's /pricing Log In view so the two surfaces are
   identical, and Ask Sales was restyled to match it the same day. Both declare their own values
   further down; only their HEADER ROW is dark (#1a1a1a). Sign Out was the last dark modal and
   the only selector left here; on 2026-09-23 it went light too, so what follows is a LIGHT
   palette under a name that is only historical. The dark values it used to hold are in the
   git history of this block, not in it.

   DROPPING A MODAL FROM THIS LIST IS PART OF RESTYLING IT, not a tidy-up afterwards: these are
   custom properties on the dialog element itself, so a modal left here keeps inheriting ten
   values into every descendant, and rules written elsewhere lose to them wherever the two meet.
   A change that looks applied in the diff and is wrong on screen.

   SIGN OUT IS NO LONGER DARK EITHER (owner, 2026-09-23: "Change the background and border color
   of ALL modals displayed on the marketing site and pricing page to #E6E2E0"). So this block is
   now a LIGHT palette and every modal on the site is one surface. That surface is #EDEBE8 since
   2026-09-24 (owner: pages and modals both to #EDEBE8, on this site and the app's /pricing), the
   same colour as the page behind it; the backdrop is what sets a modal apart now.
   THE PANEL COLOUR WAS NOT THE WHOLE CHANGE, and it could not be: #ffffff ink on #E6E2E0 is
   1.29:1, #e5e5e5 body text 1.02:1 and the #f2b8b5 error line 1.33:1 - repainting the surface
   alone would have rendered this modal's every word invisible. So the six values below that are
   READ by a rule moved with it, to this site's own light tokens, which is what the Log In modal
   already paints its form in. On #EDEBE8: --ink #1d1a18 (14.55:1), --body-text #3d3835 (9.72:1),
   --muted #6a6360 (4.95:1, over the 4.5:1 AA floor), --line-hover #b7b1ab for the outline button's edge
   (= .login-dialog__cancel's border), and #b3261e for the error line - which is the colour the
   note below always said belonged on a light surface.
   --modal-hover is black at 6%, not a mixed literal: a wash composites over whatever is behind
   it and so cannot drift from the surface colour on the next change. It mirrors
   .calc-dialog__close:hover, which is white at 14% for the same reason on its dark bar.
   --modal-field-bg and --modal-field-focus are READ BY NOTHING today (this modal has no fields -
   it is "Are you sure?", Cancel, and a red Sign Out). They are flipped rather than left, because
   a stale dark value here is exactly the trap this comment's first paragraph describes; they are
   the Log In modal's field values, so a field added later lands on the right ones. */
.signout-dialog {
  --modal-surface: #edebe8;
  --modal-border: #edebe8;
  --modal-ink: #1d1a18;
  --modal-body: #3d3835;
  --modal-muted: #6a6360;
  --modal-hover: rgba(0, 0, 0, 0.06);
  --modal-field-bg: #ffffff;
  --modal-field-border: #b7b1ab;
  --modal-field-focus: var(--coral);
  --modal-danger: #b3261e;
}
/* Cancel/close-style outline buttons inside a modal: .btn--outline's own hover FILLS with
   --btn-ink and puts white on it, which is a heavier press than this panel wants, so the border
   and label are restated here against the modal palette instead. */
.signout-dialog .btn--outline {
  border-color: var(--modal-field-border);
  color: var(--modal-body);
  background: transparent;
}
.signout-dialog .btn--outline:hover {
  background: var(--modal-hover);
  color: var(--modal-ink);
}

/* PORTED FROM .login-dialog BELOW (owner, 2026-09-21: "Change the 'Ask Sales' modals on the
   marketing site to match the new modal style"). Same frame, same dark header row, same light
   form, same actions row. Values are that block's; where a number differs it is called out.
   WHY THE PORT IS FROM THIS FILE AND NOT FROM THE APP, unlike Log In's: there is no Ask Sales
   surface in the app to be identical to, and alpha's D-060 excludes this modal by name. The
   reference is the Log In modal as it stands HERE. If the app's Log In view moves, Log In
   follows it and this follows Log In. */
.sales-dialog {
  padding: 0;
  /* The 10px #EDEBE8 frame at .85 alpha (owner, 2026-09-24; #E6E2E0 from 2026-09-23, #e9e9e9
     before that). The ALPHA was kept and only the colour moved - the owner was asked which, and
     chose to keep it. */
  border: 10px solid rgba(237, 235, 232, 0.85);
  /* CONCENTRIC: 22 + 10 = 32 outside, 22 inside. */
  border-radius: calc(var(--radius-lg) + 10px);
  background: #edebe8;
  /* MUST STAY BELOW `background`, which is a shorthand that resets every background-* longhand
     it does not name. Written above it, the .85 alpha composites against its own #EDEBE8 and
     the frame looks solid - a change that looks applied and is not. */
  background-clip: padding-box;
  color: var(--ink);
  /* The Log In modal's width. box-sizing is border-box, so this is the OUTER edge, frame
     included. The old dark panel was 480px, and 10px narrower matters less than the two modals
     being the same size. */
  width: min(470px, calc(100vw - 32px));
  /* The UA caps a <dialog> at calc(100% - 6px - 2em), which beats the authored width on a phone
     and lets the frame eat 20px INWARD. Restating it as a max-width puts this number in charge. */
  max-width: calc(100vw - 32px);
  max-height: 88vh;
  overflow: hidden;
}
/* THE ONE UNDERLAY, ON BOTH DOMAINS: #000000 at 80% (owner, 2026-09-22). This site had been
   running two at once - `--ink` at 55% with no blur on Ask Sales and Log In, black at 70% over a
   12px blur on Sign Out and the calculator - so which modal you opened decided how dark the page
   behind it went. The app's /pricing carried the same split and now carries this same value.
   Supersedes the 2026-09-14 and 2026-09-15 settings in docs/DECISIONS.md. */
.sales-dialog::backdrop {
  background: rgba(0, 0, 0, 0.8);
}
/* Load-bearing, not decoration: site.js shows and hides the error line, the form and the success
   panel with the `hidden` property alone. */
.sales-dialog [hidden] { display: none; }
/* The 20px is the 10px frame top AND bottom; a flat 88vh here lets the content box ask for 20px
   more than it has and `overflow: hidden` clips the foot of the form. */
.sales-dialog__shell {
  display: flex;
  flex-direction: column;
  max-height: calc(88vh - 20px);
  font-family: var(--font-body);
}
/* `min-height: 0` is what lets this shrink inside the flex column above and is required for the
   overflow to engage. No padding: the 24px comes from the head and the form separately, which is
   why they butt directly against each other. */
.sales-dialog__panel {
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow-y: auto;
}
/* DARK, BUT ONLY THIS ROW. Padding is a full 24px on all four sides, not `24px 24px 0`: a
   transparent bottom edge dissolves into the form's own 24px and the row becomes a dark box with
   no bottom edge the moment it has a fill. The outer 10px frame stays #EDEBE8 regardless.
   `align-items: baseline` and the 12px gap are the Log In row's, kept although this modal has
   nothing to the right of its title - so that if anything is ever added there it lands on the
   title's baseline without this rule having to be rediscovered. */
.sales-dialog__head {
  display: flex;
  align-items: baseline;
  gap: 12px;
  flex-wrap: wrap;
  background: #1a1a1a;
  padding: 24px;
}
/* 24px/700 white on the dark bar. Inter Tight at 1.15/-0.015em is part of the title's look and
   is NOT the body font. The old `padding-right: 44px` went with the X it was clearing. */
.sales-dialog__title {
  margin: 0;
  font-family: var(--font-head);
  font-size: 24px;
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: -0.015em;
  color: #ffffff;
}
/* The Ask Sales close X and the Log In one (owner, 2026-10-06, the second as "the same thing"),
   right-justified in each dark header: `margin-left: auto`
   pushes it to the end of the flex row. The calculator modal's bare white X (`.calc-dialog__close`):
   40x40 hit area, 24px glyph, a wash only on hover. Centred rather than baseline-aligned like
   the title, and the negative block margin keeps its 40px box from making the header taller. */
.sales-dialog__x,
.login-dialog__x {
  margin: -8px -8px -8px auto;
  align-self: center;
  flex: none;
  width: 40px;
  height: 40px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  border-radius: 10px;
  background: transparent;
  color: #fff;
  cursor: pointer;
}
/* The Sign Up promo column's head row: "Get Started" with the X at its right end. PHONES ONLY
   (owner, 2026-10-06: "Only for mobile with the dark side of the 'Sign Up' modal"): hidden here,
   shown in the 850px query where the two columns stack and the dark one is the modal's top. */
.login-dialog__promo-top { display: flex; align-items: center; gap: 12px; }
.login-dialog__promo-top .login-dialog__x { display: none; }
.sales-dialog__x:hover,
.login-dialog__x:hover { background: rgba(255, 255, 255, 0.14); }
.sales-dialog__x:focus-visible,
.login-dialog__x:focus-visible { outline: 3px solid var(--coral); outline-offset: 2px; }
/* The Ask Sales bell before the title (owner, 2026-10-06). 1.2em is the button's own ratio -
   21.6px beside an 18px label - so beside the 24px title it is 28.8px. White, as the title is. */
.sales-dialog__title--icon { display: inline-flex; align-items: center; gap: 12px; }
.sales-dialog__title-icon { flex: none; width: 1.2em; height: 1.2em; }
/* The ONLY vertical spacing between the error line, the fields and the actions row - none of
   those children carry margins. The class is on the success panel too, so that panel gets the
   same 24px padding and the same gap without a second rule. */
.sales-dialog__form {
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: 24px;
  overflow-y: auto;
  min-height: 0;
  flex: 1;
}
/* 16px, approved by the owner 2026-09-21: asked to match the new modal style, and told in the
   same breath to take its 16px rather than hold this site's 18px floor. This is the Log In
   modal's error banner (.login-dialog__error below), itself the app's plans.css:2773-2780. The
   #fdecea / #b3261e literals are the app's too. */
.sales-dialog__error {
  margin: 0;
  padding: 12px 14px;
  border-radius: 10px;
  background: #fdecea;
  color: #b3261e;
  font-size: 16px;
}
/* 5px, settled by eye in the app (owner: 6, then -2, then +1 back). It is the gap for the WHOLE
   field, so label/input and textarea/helper spacing are deliberately equal. */
.sales-dialog__field {
  display: flex;
  flex-direction: column;
  gap: 5px;
}
/* 16px and BOLD, approved by the owner 2026-09-21 with the restyle: the Log In modal's field
   label (.login-dialog__field > span below), itself the app's plans.css:2404-2408.
   THE CHILD COMBINATOR IS LOAD-BEARING: the <span> wraps the real <label for>, which inherits
   all three declarations. A bare <label> child of .sales-dialog__field matches nothing here and
   renders unstyled - the trap the Log In markup's own note records. */
.sales-dialog__field > span {
  font-size: 16px;
  font-weight: 700;
  color: var(--ink);
}
/* 16px, approved by the owner 2026-09-21 with the restyle: the Log In modal's input
   (.login-dialog__field input below), itself the app's plans.css:2411-2419 - "16px is the app's
   body-text floor and this is a real form: do not go under it".
   `width` and `box-sizing` are the one addition to that rule: a flex column stretches an <input>
   on its own, but a <textarea> carries an intrinsic `cols` width and would sit narrower than the
   field it shares a column with. */
.sales-dialog__field input,
.sales-dialog__field textarea {
  font: inherit;
  font-size: 16px;
  width: 100%;
  box-sizing: border-box;
  padding: 12px 14px;
  /* #D1CBC8, NOT --line (owner, 2026-09-23) - the reasoning is on `.login-dialog__field input`,
     which this block is a port of, and the two must not drift. */
  border: 1px solid #d1cbc8;
  border-radius: 10px;
  background: var(--field-bg);
  color: var(--ink);
}
/* The message box. 140px is the old dark modal's own height, kept: the restyle changed how the
   field looks, not how much room the visitor is given to write. */
.sales-dialog__field textarea { min-height: 140px; resize: vertical; }
/* :focus-visible, not :focus - the Log In fields' rule. A coral ring OUTSIDE the border rather
   than the old design's border recolour, which is invisible against a white field. */
.sales-dialog__field input:focus-visible,
.sales-dialog__field textarea:focus-visible {
  outline: 2px solid var(--coral);
  outline-offset: 1px;
}
/* 16px, approved by the owner 2026-09-21 with the restyle: this line sits under the message box
   the way .login-dialog__hint sits under an input, and reads as part of the same field, so it
   takes that rule's size. It is the textarea's aria-describedby target and must stay a FOLLOWING
   SIBLING of it. */
.sales-dialog__helper { margin: 0; font-size: 16px; color: var(--muted); }
/* 16px, approved by the owner 2026-09-21 with the restyle: the Log In modal's .login-dialog__sent
   below, itself the app's plans.css:2544-2551. It is .sales-dialog__error in the affirmative -
   same padding, same radius, same size - so the two read as one family of messages.
   The old rule was grey body text on the dark panel; on a light panel a success needs its own
   fill, and this is the one the site already uses for exactly that. */
.sales-dialog__sent {
  margin: 0;
  padding: 12px 14px;
  border-radius: 10px;
  background: #e8f5e9;
  color: #1b5e20;
  font-size: 16px;
}
/* The success panel is what site.js focuses after a send, so the browser draws a ring on a
   container the visitor never tabbed to. Suppressed, as the old .sales-dialog__sent suppressed
   it, for the same reason: the focus is programmatic and only there to move the announcement. */
.sales-dialog [data-sales-sent]:focus { outline: none; }
/* LEFT-JUSTIFIED, Send first and Cancel to its right - the order and alignment the owner settled
   for Log In ("the buttons should be left-justified", then "'Log In' should be to the left of
   'Cancel'"). `margin-top: auto` pins the row to the bottom of the form's flex column; the 8px
   sits on top of the form's 16px gap for 24px of clearance. */
.sales-dialog__actions {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 12px;
  margin-top: auto;
  padding-top: 8px;
}
/* The coral pill, = .login-dialog__submit. It was a full-width 56px pill with `btn btn--primary`
   on it; none of .btn's numbers are this style's, which is why Log In's buttons do not use it
   either. `position: relative` is KEPT and is load-bearing - the sending spinner below is
   positioned to it. The Close button on the success panel carries this class too. */
.sales-dialog__send {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  font: inherit;
  font-size: 18px;
  font-weight: 600;
  padding: 14px 28px;
  border: 0;
  border-radius: 10px;
  background: var(--coral);
  color: #ffffff;
  cursor: pointer;
}
/* aria-disabled, never disabled: site.js keeps the button focusable and gates the submit itself,
   so `:disabled` would never match. Hover is suppressed the same way. */
.sales-dialog__send:hover:not([aria-disabled="true"]) { background: var(--coral-hover); }
/* 0.55, not 0.3 (2026-09-22). This says it equals the app's `.lj-signup__submit:disabled`, and
   that rule is `opacity: 0.55` - so a disabled Send was visibly fainter here than the same
   disabled button is on the app's /pricing, on two surfaces that must be identical. The app is
   the reference; this side was the one that had drifted. */
.sales-dialog__send[aria-disabled="true"] { opacity: 0.55; cursor: not-allowed; }
/* The outline twin, = .login-dialog__cancel: same 18px/600/radius 10px, transparent fill, 1px
   --line-hover border. It does NOT fill on hover - border and label just darken to --ink - which
   is why it cannot reuse `.btn--outline`. With the X gone it is the modal's only visible exit. */
.sales-dialog__cancel {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font: inherit;
  font-size: 18px;
  font-weight: 600;
  padding: 14px 20px;
  border: 1px solid var(--line-hover);
  border-radius: 10px;
  background: transparent;
  color: var(--body-text);
  cursor: pointer;
  flex: none;
}
.sales-dialog__cancel:hover { border-color: var(--ink); color: var(--ink); }
/* The label stays in the DOM, transparent, so the button's accessible name never changes. */
.sales-dialog__send[aria-busy="true"] { color: transparent; }
.sales-dialog__send[aria-busy="true"]::after {
  content: "";
  position: absolute;
  inset: 0;
  width: 24px;
  height: 24px;
  margin: auto;
  border: 3px solid rgba(255, 255, 255, 0.45);
  border-top-color: #fff;
  border-radius: 50%;
  animation: sales-spin 0.8s linear infinite;
}
@keyframes sales-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .sales-dialog__send[aria-busy="true"]::after { animation-duration: 2.4s; }
}
/* The honeypot: off-screen rather than display:none, which some bots detect and skip. */
.sales-dialog__trap { position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden; }

/* ---- the Support dialog (supportDialog() in site-template.mjs, owner 2026-10-01) ----
   Ask Support and Suggestions, the two forms behind the Support card on a support article page.
   ONE <dialog> for both kinds, and it WEARS THE ASK SALES CLASSES ABOVE: the owner chose "all
   light", the forms "styled like this site's existing Ask Sales dialog", so the frame, the dark
   head row, the fields, the helper, the error and sent lines, the actions row and both buttons
   are .sales-dialog rules and are not restated. If Ask Sales moves, this moves with it, which is
   the point. The web desktop's SupportMessageModal is the reference for BEHAVIOUR AND COPY only;
   its dark look was deliberately not ported, and the note above .signout-dialog records why that
   modal was never inside the identical-surfaces rule.
   What follows is ONLY what Ask Sales lacks. NO font-size is declared in this block, on purpose:
   the owner's one sub-18px approval for this feature is the card's tile label (.support-card
   below), so every size here is a .sales-dialog rule's, under that rule's own dated approval. */
/* (d) The dialog's own copy of the load-bearing rule. site.js shows and hides the error line,
   the email field, the Suggestions label, the hint, the form and the success panel with the
   `hidden` property alone, and there is deliberately no global [hidden] rule on this site.
   `.sales-dialog [hidden]` already reaches this dialog through the shared class; this is here so
   the behaviour does not silently depend on a class borrowed for its looks. */
.support-dialog [hidden] { display: none; }
/* The programmatic focus on the success panel draws no ring, as on Ask Sales, whose rule is
   keyed on its own data-sales-sent hook and so does not reach this panel. */
.support-dialog [data-support-sent]:focus { outline: none; }
/* (a) The invalid-email line under the email field, "Enter a valid email": the Log In family's
   hint pattern (.login-dialog__field input[aria-invalid="true"] and its ~ .login-dialog__hint),
   keyed the same way on the input's aria-invalid so the attribute that tells assistive tech is
   the one that paints it. The line itself is a .sales-dialog__helper, which is where its size
   comes from; #b3261e is the error red both modal families already use on this surface. */
.support-dialog .sales-dialog__field input[aria-invalid="true"] { border-color: #b3261e; }
.support-dialog .sales-dialog__field input[aria-invalid="true"] ~ .support-dialog__hint { color: #b3261e; }
/* EXPLICIT, for the reason written on `.login-dialog__field input`: the fields take
   `font: inherit`, so their weight is whatever wraps them, and `.sales-dialog__field > span` is
   700. Nothing wraps these two today; this keeps typed text regular if a wrapper is ever added,
   instead of the field going bold with nothing in the diff to say why. */
.support-dialog .sales-dialog__field input,
.support-dialog .sales-dialog__field textarea { font-weight: 400; }
/* (b) "Send suggestion" renders "Send Suggestion", as on the web desktop, whose Button carries
   `capitalize` (apps/web/src/components/ui/button.tsx). The string stays the app's own, in
   lower case, so the two sides share one piece of copy and one way of drawing it. "Ask Support",
   "Close" and the per-word capitals they already have are unchanged by it. */
.support-dialog .sales-dialog__send { text-transform: capitalize; }
/* Send fills the row (owner, 2026-11-07: full width, as in the app). This dialog has no X, so
   Cancel, its only visible exit, stays beside it. Keyed on the form's button so the success
   panel's Close keeps its own width. */
.support-dialog [data-support-send] { flex: 1; }
/* (c) needs no rule: the Suggestions kind's visible label is an ordinary .sales-dialog__field
   > span, hidden by site.js for the Ask Support kind, whose message box is named by the title. */
/* The same off-screen honeypot for the /contact form. A generic name because it is now used by
   two forms; .sales-dialog__trap above is left alone rather than renamed, so the modal's markup
   and its CSS stay in step. Off-screen rather than display:none: a bot that skips hidden fields
   would skip it, and then it catches nothing. */
.form-trap { position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden; }
.contact-form__error { margin: 0; font-size: 1.125rem; color: #b3261e; }
/* A field's own line, shown by site.js with aria-invalid on its input, as on the Log In family. */
.contact-form__hint { margin: 0; font-size: 1.125rem; color: #b3261e; }
.contact-form .field input[aria-invalid="true"],
.contact-form .field textarea[aria-invalid="true"] {
  border-color: #b3261e;
  /* Owner, 2026-11-05: "the background of the affected field needs to become #EBD0D0". */
  background-color: #ebd0d0;
}
/* #b3261e is too dark to read on the dark card; this is the app's dark-theme validation text. */
html.theme-dark .contact-form__hint,
html.theme-dark .contact-form__error { color: #df8987; }
/* /contact's title: 30px, and 26px at the phone layout (owner, 2026-11-05: "reduce font-size of
   Contact Sales & Support title by 4px"). A class, because the inline size it replaced would
   beat this media query. */
.contact-split__title { font-size: 1.875rem; }
@media (max-width: 850px) {
  .contact-split__title { font-size: 1.625rem; }
}
.contact-form__sent { margin: 0; font-size: 1.125rem; color: var(--body-text); outline: none; }

/* The Sign Out confirmation (signOutDialog() in site-template.mjs, owner 2026-09-14): the app's
   SignOutDialog, "Are you sure?" over one line, then Cancel and a red Sign Out, dressed like the
   Ask Sales modal above (the same --surface panel) and under the same modal rule: black at 70%
   over a 12px blur, and no close on a backdrop click. The confirm button's red is the menu
   row's. */
.signout-dialog {
  width: min(calc(100% - 32px), 440px);
  max-height: calc(100% - 32px);
  padding: 0;
  border: 0;
  border-radius: var(--radius-lg);
  background: var(--modal-surface);
  color: var(--modal-body);
  border: 1px solid var(--modal-border);
  box-shadow: var(--shadow);
  overflow: auto;
}
/* 80%, not 70% (owner, 2026-09-22) - see the note on .sales-dialog::backdrop.
   THE 12px BLUR IS LEFT ALONE: the instruction named the colour and the alpha and nothing else.
   It is the one thing still making this underlay read differently from Ask Sales's and Log In's. */
.signout-dialog::backdrop {
  background: rgba(0, 0, 0, 0.8);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
}
.signout-dialog [hidden] { display: none; }
.signout-dialog__panel { padding: 28px; }
.signout-dialog__title {
  margin: 0 0 8px;
  padding-right: 44px;
  font-family: var(--font-head);
  font-size: 1.625rem;
  font-weight: 600;
  line-height: 1.2;
  color: var(--modal-ink);
}
.signout-dialog__description { margin: 0; font-size: 1.125rem; }
.signout-dialog__error { margin: 16px 0 0; font-size: 1.125rem; color: var(--modal-danger); }
.signout-dialog__actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 24px; }
/* HOVER IS #971B1C ON #F1F1F1 (owner, 2026-09-21), the same values the app's own destructive
   button takes on hover (its `destructive` variant in apps/web/src/components/ui/button.tsx),
   so the one danger button on each site answers a pointer the same way. It replaces a #97201a
   that was a shade of THIS site's resting red and matched nothing on the other side.
   The RESTING states match since 2026-10-15: the app's destructive button, and this site's own
   (.login-dialog__submit--destructive), now rest on this button's #b3261e with white text
   (owner: "change the web desktop's destructive button to the Sign Out red (#b3261e, white
   text) everywhere"). Before that the app's rested on a #422121 ground with #c08080 ink. */
.signout-dialog__confirm { background: #b3261e; color: #fff; }
.signout-dialog__confirm:hover { background: #971b1c; color: #f1f1f1; }
.signout-dialog__confirm[aria-busy="true"] { opacity: 0.7; cursor: progress; }
.signout-dialog__close {
  position: absolute;
  top: 18px;
  right: 18px;
  width: 40px;
  height: 40px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: 10px;
  background: transparent;
  color: var(--modal-muted);
  cursor: pointer;
}
.signout-dialog__close:hover { background: var(--modal-hover); color: var(--modal-ink); }

/* The Log In modal (loginDialog() in site-template.mjs). PORTED RULE-FOR-RULE from the app's
   /pricing Log In view, apps/web/src/app/pricing/plans.css (owner, 2026-09-21: "The marketing
   site and the marketing site banded pages located in the web application must always be
   IDENTICAL. Users don't know which domain they are on and they shouldn't."). The app is the
   reference; this file follows it.
   IT IS LIGHT. It is deliberately NOT in the --modal-* palette list above - only the HEADER ROW
   is dark (#1a1a1a), everything below it is the light form. Ask Sales carries the same design;
   Sign Out has its own, and since 2026-09-23 that one is light too, so nothing on this site is a
   dark modal any more.
   TOKEN MAP (app -> here, identical values): --lj-coral -> --coral, --lj-coral-hover ->
   --coral-hover, --lj-ink -> --ink, --lj-body-text -> --body-text, --lj-muted -> --muted,
   --lj-line -> --line, --lj-radius-lg -> --radius-lg, --lj-font-head -> --font-head,
   --lj-font-body -> --font-body. --lj-line-hover had no equivalent; --line-hover was added to
   :root for it. Literals (#EDEBE8, #1a1a1a, #efefef, #ffffff, #fdecea, #b3261e) are literals in
   the app too - do not tokenise them. The panel literal was #e9e9e9 on both sides until
   2026-09-23, #E6E2E0 for a day, and #EDEBE8 since 2026-09-24; each move landed on both sides
   in one change, which is what D-060 requires of it.
   NO CLOSE X and NO BOX-SHADOW: the app has neither. Cancel, Escape and a backdrop click are
   the three exits, which is also what the owner asked of every modal on this site. */
.login-dialog {
  padding: 0;
  /* A 10px #EDEBE8 frame at .85 alpha (owner, 2026-09-24; #E6E2E0 from 2026-09-23, #e9e9e9
     before that, app plans.css).
     The ALPHA was kept and only the colour moved - the owner was asked which, and chose to keep
     it, so the backdrop still shows through the frame as it always has. The border IS the frame;
     do not fold it into the background. */
  border: 10px solid rgba(237, 235, 232, 0.85);
  /* CONCENTRIC: 22 + 10 = 32 outside, 22 inside. Equal radii pinch the corners to 12px. */
  border-radius: calc(var(--radius-lg) + 10px);
  background: #edebe8;
  /* THIS LINE MUST STAY BELOW `background`. `background` is a shorthand that resets every
     background-* longhand it does not name, so written above it this computes back to
     border-box and the .85 alpha composites against its own #EDEBE8 - a frame that looks
     solid, on a change that looks applied. */
  background-clip: padding-box;
  color: var(--ink);
  /* 470 = the app's 940 halved, its `is-narrow` Log In width. box-sizing is border-box, so
     this is the OUTER edge, frame included. */
  width: min(470px, calc(100vw - 32px));
  /* The UA caps a <dialog> at calc(100% - 6px - 2em) = 333px at a 375px viewport, which beats
     the authored width and lets the frame eat 20px INWARD. Restating it as a max-width puts
     the authored number back in charge. */
  max-width: calc(100vw - 32px);
  max-height: 88vh;
  overflow: hidden;
}
/* THE TWO-COLUMN SIGN UP IS WIDER. 940, not 920: box-sizing is border-box, so the 10px frame
   is added OUTSIDE the content, and a 920 here would take 10px off each 50% column.
   DELIBERATE DEVIATION FROM THE APP, recorded rather than hidden: the app leaves its wide
   dialog with no max-height and no overflow (those live only on its narrow variant), while this
   site keeps `max-height: 88vh; overflow: hidden` on the shared base above. The rendered result
   is the same because __shell already caps at calc(88vh - 20px) and __cols does the scrolling,
   and the clip is what stops the promo column's dark fill poking past the 22px inner radius. It
   is only safe because the country panel is `position: fixed` and escapes the clip. */
.login-dialog--wide {
  width: min(940px, calc(100vw - 32px));
}
/* CHECK YOUR EMAIL IS 490 (owner, 2026-10-09: "less wide by ~350px" than the 940 Sign Up frame
   it used to keep, then 100px less again). = the app's `.lj-signup-dialog.is-done`. site.js sets
   it in showSignupDone and setSignupChrome clears it on every other view. */
.login-dialog--done {
  width: min(490px, calc(100vw - 32px));
}
.login-dialog::backdrop {
  /* #000000 at 80%, the one underlay on both domains (owner, 2026-09-22) - see the note on
     .sales-dialog::backdrop. It was `--ink` at 55% with no blur, matching the app's Sign Up
     dialog, which has moved to this same value. */
  background: rgba(0, 0, 0, 0.8);
}
.login-dialog [hidden] { display: none; }
/* = the app's .lj-signup. The 20px is the 10px frame top AND bottom; a flat 88vh here lets the
   content box ask for 20px more than it has and `overflow: hidden` clips the foot of the form. */
.login-dialog__shell {
  display: flex;
  flex-direction: column;
  max-height: calc(88vh - 20px);
  font-family: var(--font-body);
}
/* = the app's .lj-signup__single. `min-height: 0` is what lets it shrink inside the flex column
   above and is required for the overflow to engage. No padding: the 24px comes from the head
   and the form separately, which is why they butt directly against each other. */
.login-dialog__panel {
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow-y: auto;
}
/* DARK, BUT ONLY THIS ROW (owner, 2026-09-21, correcting a first pass that painted the whole
   view). BASELINE alignment, not center: the 24px title and the 16px Sign Up link share a
   baseline. Padding is a full 24px on all four sides, not `24px 24px 0` - the transparent
   version let its bottom edge dissolve into the form's own 24px and became a dark box with no
   bottom edge the moment it had a fill. The outer 10px frame stays #EDEBE8 regardless. */
.login-dialog__head {
  display: flex;
  align-items: baseline;
  gap: 12px;
  flex-wrap: wrap;
}
/* DARK, AND ONLY ON THE SINGLE-COLUMN VIEWS (Log In, Reset Password). Sign Up's head sits
   inside the light form column of a two-column layout, where a dark bar with white type is
   something nobody asked for - the app hit this and scoped it the same way
   (plans.css:2334-2340). The child combinator matters: the head is a DIRECT child of the panel.
   This was flattened onto the base rule when the Log In modal was ported on 2026-09-21, which
   rendered identically for the one view that existed and would have been wrong for the next. */
.login-dialog__panel > .login-dialog__head {
  background: #1a1a1a;
  padding: 24px;
}
.login-dialog__panel > .login-dialog__head .login-dialog__title {
  color: #ffffff;
}
/* Inter Tight at 1.15/-0.015em is inherited from the app's h1-h4 block and is part of the
   title's look - it is NOT the body font. Restated here because this site's h2 rule is scoped
   differently. 24px, 700, white on the dark bar. */
.login-dialog__title {
  margin: 0;
  font-family: var(--font-head);
  font-size: 24px;
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: -0.015em;
  color: var(--ink);
}
.login-dialog__alt-login {
  font: inherit;
  font-size: 18px;
  font-weight: 600;
  border: 0;
  background: none;
  padding: 0;
  color: var(--coral);
  text-decoration: underline;
  cursor: pointer;
}
.login-dialog__alt-login:hover {
  color: var(--coral-hover);
}
/* THE "SIGN UP" CONTROL MOVED FROM THE HEADER INTO THE LOG IN ROW (owner, 2026-10-01: "move the
   'Sign Up' link from the modal header to the row that has the buttons ... right-justified ...
   it should actually be a button, but look like the link"). It is a real <button> and keeps the
   link's look, `.login-dialog__alt-login` above - 18px, 600, coral, underlined - which is the
   step-1 link style the owner chose on 2026-09-22. What makes it a button's click target is the
   box: the same 10px 16px padding as the Log In row's Cancel, plus a 1px transparent border so
   it is exactly Cancel's height. `margin-left: auto` right-justifies it, and the row wraps (below)
   so a narrow phone drops it to its own right-aligned line rather than overflowing. THIS
   REVERSES the 2026-09-22 header placement. */
.login-dialog__alt-login.login-dialog__actions-signup {
  margin-left: auto;
  flex: none;
  padding: 10px 16px;
  border: 1px solid transparent;
  border-radius: 10px;
  /* Underlined on hover only (owner, 2026-10-01); the base link rule underlines at rest. */
  text-decoration: none;
}
.login-dialog__alt-login.login-dialog__actions-signup:hover,
.login-dialog__alt-login.login-dialog__actions-signup:focus-visible {
  text-decoration: underline;
}
.login-dialog__actions--login { flex-wrap: wrap; }
.login-dialog__form,
.login-dialog__done {
  display: flex;
  flex-direction: column;
  /* The ONLY vertical spacing between the banner, the two fields, the reset row and the
     actions row - none of those children carry margins. */
  gap: 16px;
  padding: 24px;
  overflow-y: auto;
  min-height: 0;
  flex: 1;
}
/* 16px, approved by the owner 2026-09-21: the app's own value for the error banner
   (plans.css:2773-2780), a sub-18px exception taken deliberately so this modal matches the
   app's /pricing Log In view rule-for-rule. The #fdecea / #b3261e literals are the app's too. */
.login-dialog__error {
  margin: 0;
  padding: 12px 14px;
  border-radius: 10px;
  background: #fdecea;
  color: #b3261e;
  font-size: 16px;
}
/* A Payment Methods result line (Appendix C C15, 2026-10-09): where the app raises a success
   toast, this site says it inside the dialog. The error banner's shape in the page's ink, so it
   reads as a result and not an alarm. */
.login-dialog__done {
  margin: 0;
  padding: 12px 14px;
  border-radius: 10px;
  background: var(--card);
  color: var(--ink);
  /* 16px, approved by the owner 2026-09-21 with the error banner above it, which this line
     matches so the two results are one size (D-060). */
  font-size: 16px;
}
/* THE FAILED-LOGIN PANEL (owner, 2026-09-22, to a mockup). Shown INSTEAD of the banner above
   when an email and password do not match; every other code still gets that banner. Identical to
   the app's `.lj-signup__fail` in plans.css; D-060 binds the pair.
   THE PALETTE IS SAMPLED FROM THE MOCKUP, which is a redrawn comp and not a screenshot - its
   elements do not share one scale, so the COLOURS come from it exactly and the GEOMETRY comes
   from this modal's own system rather than from measured pixels. Sampled:
     panel   #fbeae5 on a #e5c5bd border   warmer than the #fdecea/#b3261e banner above, which
                                           stays as it is for every other code
     title   #7e2f1c                       also the reset button's label
     body    #8f3520                       also "Get help"
     icon    #a8412c
     button  white on a #e0af9d border
   From the system: the modal's own 10px radius, and the reset button takes the Log In row's
   `padding: 10px 16px` exactly, so the two buttons in this view are one size.
   EVERYTHING HERE IS 18px, so the panel adds no exception to this site's floor - unlike the
   banner above it, which is a 16px one. */
.login-dialog__fail {
  display: flex;
  gap: 12px;
  margin: 0;
  padding: 16px;
  border: 1px solid #e5c5bd;
  border-radius: 10px;
  background: #fbeae5;
}
/* `margin-top: 2px` sits the glyph on the title's cap rather than its line box, which is what
   makes the two read as one line. `flex: none` stops the icon being squeezed when the body wraps
   to three lines, which at this width it does. */
.login-dialog__fail-icon {
  flex: none;
  margin-top: 2px;
  color: #a8412c;
}
.login-dialog__fail-body {
  min-width: 0;
}
.login-dialog__fail-title {
  margin: 0;
  font-size: 18px;
  font-weight: 700;
  color: #7e2f1c;
}
.login-dialog__fail-text {
  margin: 6px 0 0;
  font-size: 18px;
  color: #8f3520;
}
.login-dialog__fail-actions {
  display: flex;
  align-items: center;
  gap: 16px;
  margin-top: 14px;
}
.login-dialog__fail-reset {
  font: inherit;
  font-size: 18px;
  font-weight: 600;
  padding: 10px 16px;
  border: 1px solid #e0af9d;
  border-radius: 10px;
  background: #ffffff;
  color: #7e2f1c;
  cursor: pointer;
}
.login-dialog__fail-reset:hover {
  border-color: #7e2f1c;
}
/* An <a>, not a button, because this one really navigates - to /support, this site's own Help
   Center. It is the ONLY control in this dialog that leaves it, which needed a named exemption
   in check-auth-modal.mjs; the decision is in docs/DECISIONS.md.
   Styled as the body text underlined rather than in the site's coral, which on this panel reads
   as a smudge against #8f3520 - the same finding the banner's inline link records. */
.login-dialog__fail-help {
  font-size: 18px;
  font-weight: 600;
  color: #8f3520;
  text-decoration: underline;
}
.login-dialog__fail-help:hover {
  color: #7e2f1c;
}
/* The resend control inside the unverified-email banner (app, 2026-09-22), and a rule-for-rule
   port of the app's `.lj-signup__inline-link` (plans.css). It is a <button> because it performs
   an action rather than navigating - an <a> whose href goes nowhere is the thing screen readers
   announce wrongly.
   `font: inherit` is LOAD-BEARING and not a tidy-up: without it the UA drops a button to ~13px,
   which would both make it a different size from the sentence it sits in and put it under the
   16px the banner is already an approved exception for. It sets no font-size of its own, so it
   is not a new sub-floor rule.
   COLOUR IS THE BANNER'S, not coral: on #fdecea the coral link colour reads as a smudge against
   the #b3261e text rather than as a link. Underlined, so colour alone does not mark it.
   A SECOND USE since 2026-09-23: "Terms of Service" in the agreement line under each provider
   pair, where the app's same sentence uses `.lj-signup__inline-link` too. It takes that line's
   colour there, #c9c3bd in the promo column and --muted in the Log In view. */
.login-dialog__inline-link {
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  cursor: pointer;
  color: inherit;
  text-decoration: underline;
  font-weight: 700;
}
.login-dialog__inline-link:disabled {
  cursor: not-allowed;
  opacity: 0.6;
}
.login-dialog__field {
  display: flex;
  flex-direction: column;
  /* 5px, settled by eye in the app (owner: 6, then -2, then +1 back). It is the gap for the
     WHOLE field, so label/input and input/hint spacing are deliberately equal. */
  gap: 5px;
}
/* 17px, approved by the owner 2026-09-22: "Increase the font-size of all form field labels in
   all of the new pre-authentication modals by 2px", then "Now decrease all these updated label
   font-size values by 1px". 16 -> 18 -> 17, in two instructions the same day. BOLD is
   2026-09-21, when they were 600.
   THIS IS A SUB-FLOOR EXCEPTION AND THE INSTRUCTION ABOVE IS ITS APPROVAL. 17px is under this
   site's 18px floor, so `check:type-floor` fails the build without this dated comment. Worth
   knowing how it got here: at 16px it was already an exception, carried because this modal is
   ported from the app, whose own floor is 16px; the +2px briefly landed it ON the floor and
   retired the exception; the -1px re-arms it one pixel under. A later reader tempted to "round
   it back to 18" would be undoing the second of the two instructions, not restoring a default.
   ONE RULE COVERS EVERY LABEL IN THE DIALOG, and that is the point rather than a shortcut: Full
   Name, Email Address and Phone on Sign Up step one, Password and Retype Password on step two,
   the Log In view's two and the Reset Password view's one. They are the same control on four
   screens, and a label that changed size between them would read as a mistake.
   The app's twin carries 17px too - D-060 - which is above its own 16px floor and so needs no
   equivalent approval there.
   The child combinator is load-bearing: the <span> wraps the real <label for>, which inherits
   all three declarations. */
.login-dialog__field > span {
  font-size: 17px;
  font-weight: 700;
  color: var(--ink);
}
/* 17px, approved by the owner 2026-09-22: "Change the font-size used in all text input fields in
   all of the new pre-authorization modals to 18px, with the exception of the scrollable 'Terms
   of Service' document textarea field", then "Reduce the font-size of all text in text input
   fields for all new modals (we're walking back the previous change by 1px)". So 16 -> 18 -> 17.
   THIS IS A SUB-FLOOR EXCEPTION AND THE SECOND INSTRUCTION IS ITS APPROVAL. 17px is under this
   site's 18px floor, so `check:type-floor` fails the build without this dated comment. It was an
   exception at 16px, stopped being one at 18px, and is one again a pixel under the floor - the
   same round trip the field LABEL above it made on the same day. Do not "round it back to 18":
   that undoes the second instruction rather than restoring a default.
   THE TERMS BOX IS STILL EXEMPT, AND STILL NEEDS NO CODE. `.login-dialog__terms-body` is a
   scrollable <div> inside `.login-dialog__terms` - not an <input>, not inside a
   `.login-dialog__field` - so no selector here reaches it and its own 18px is untouched.
   This is a DESCENDANT selector, which is why it still reaches the password input inside
   .login-dialog__password - and why the `padding-right: 44px` rule for that input must stay
   BELOW this one, since both are (0,2,1) and only source order separates them. */
.login-dialog__field input {
  font: inherit;
  font-size: 17px;
  /* EXPLICIT, because `font: inherit` above makes the weight whatever the wrapper happens to be
     (owner, 2026-09-22: step two's field text must match step one's). The password wrapper
     `<span class="login-dialog__password">` is a DIRECT CHILD of `.login-dialog__field`, so it
     matches the LABEL rule `> span { font-weight: 700 }` above - and the input inherited 700.
     Step one's plain inputs have no wrapper and stayed 400, so the two steps rendered in
     different weights.
     Fixed here rather than by excluding the wrapper from the label rule, because that would only
     fix today's wrapper. Identical to the app's `.lj-signup__field input`; D-060 binds them. */
  font-weight: 400;
  padding: 12px 14px;
  /* #D1CBC8, NOT --line (owner, 2026-09-23: every form input field in these modals). A LITERAL
     rather than a token on purpose: --line is this site's page-wide hairline and is still what
     draws cards, tables and rules out on the pages, which were not asked about. The field border
     stopped being the page's hairline the day the panel under it became #E6E2E0 - --line
     (#e2dad3) is within a few points of that panel and had almost nothing left to draw against,
     which is what this value fixes. Identical to the app's `.lj-signup__field input`; D-060
     binds them. */
  border: 1px solid #d1cbc8;
  border-radius: 10px;
  background: var(--field-bg);
  color: var(--ink);
}
.login-dialog__field input::placeholder { color: var(--muted); opacity: 1; }
.login-dialog__field input:focus-visible { outline: 2px solid var(--coral); outline-offset: 1px; }
.login-dialog__field input[aria-invalid="true"] { border-color: #b3261e; }
/* 16px, approved by the owner 2026-09-21: the app's own validation-hint size
   (plans.css:2437-2441), matching the field labels and inputs above it in the same ported
   modal. `font-style: normal` is a reset, not decoration - the element is an <em>, and without
   it the errors render italic. */
.login-dialog__hint {
  font-size: 16px;
  font-style: normal;
  color: var(--muted);
}
.login-dialog__field input[aria-invalid="true"] ~ .login-dialog__hint { color: #b3261e; }
/* The "Did you mean {suggestion}?" control inside a hint (R-11, 2026-10-08). THE APP IS THE
   REFERENCE: plans.css `.lj-signup__suggest`, rule for rule - the hint's own 16px through
   `font: inherit`, coral and underlined like `.login-dialog__alt-login`. It sets no font-size of
   its own, so it is not a new sub-floor rule; the hint it sits in is the approved exception
   above. */
.login-dialog__suggest {
  font: inherit;
  font-weight: 600;
  border: 0;
  background: none;
  padding: 0;
  color: var(--coral);
  text-decoration: underline;
  cursor: pointer;
}
.login-dialog__suggest:hover {
  color: var(--coral-hover);
}
/* A saved credential populates these two fields, and without this the autofilled box renders a
   darker grey than a typed one. The app carries the same rule (plans.css:76-82) and its comment
   records that the bug was "only visible on Log In first because that view is what a saved
   credential actually populates". */
.login-dialog__field input:-webkit-autofill,
.login-dialog__field input:-webkit-autofill:hover,
.login-dialog__field input:-webkit-autofill:focus {
  -webkit-box-shadow: 0 0 0 30px var(--field-bg) inset !important;
  -webkit-text-fill-color: #1d1a18 !important;
  caret-color: #1d1a18 !important;
}
/* A <span>, so `display: block` is required, and `position: relative` is the containing block
   for the absolutely-positioned eye. */
.login-dialog__password { position: relative; display: block; }
/* SAME SPECIFICITY (0,2,1) as `.login-dialog__field input` above, so it wins ONLY because it
   comes later in the file. Do not reorder these two blocks or the eye overlaps the text. The
   `padding-right` longhand overrides the right half of that rule's `padding: 12px 14px`
   shorthand; top/bottom 12px and left 14px survive. */
/* padding-right is the eye's 44px plus the 5px it was inset by on 2026-09-22 - see the
   toggle's `right` below. The two move together or long text runs under the glyph. */
.login-dialog__password input { display: block; width: 100%; padding-right: 49px; }
/* Wrapping the input took it out of the hint's sibling chain, so the `~` rule above stopped
   matching and the error text rendered muted grey - present, but not looking like an error.
   Keyed on the wrapper's own class, which IS a sibling. Both this AND aria-invalid on the input
   are needed: the class for this rule, the attribute for the border rule and for assistive tech. */
.login-dialog__password.is-invalid ~ .login-dialog__hint { color: #b3261e; }
/* 44px square (the pointer-target floor) and full field height, so the glyph is centred on the
   input whatever the field's padding is. The app sizes a Font Awesome <i> with `font-size: 19px`;
   this site has no Font Awesome host, so the glyph is an inline SVG and must be sized explicitly. */
.login-dialog__password-toggle {
  position: absolute;
  top: 0;
  /* 5px IN FROM THE EDGE (owner, 2026-09-22: move the eye 5px to the left). Identical to the
     app's `.lj-signup__password-toggle`; D-060 binds them. The input's padding-right above moves
     with it. */
  right: 5px;
  height: 100%;
  width: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  color: var(--muted);
}
.login-dialog__password-toggle svg { width: 19px; height: 19px; }
/* TWO SHADES LIGHTER ON SIGN UP STEP TWO (owner, 2026-09-22), and IDENTICAL to the app's, which
   is the rule for this modal: a shade is a 10% step, so two toward white from --muted #6a6360
   (106/99/96) gives 134/129/126, #86817e. The app derives the same number in plans.css from the
   same definition - if one moves, move both.
   SCOPED TO THE STEP-TWO TOGGLES by their own data attribute. The Log In view's button carries
   `data-login-password-toggle` instead and keeps --muted, so this cannot reach it. */
.login-dialog__form [data-signup-password-toggle] { color: #86817e; }
.login-dialog__password-toggle:hover { color: var(--ink); }
/* NEGATIVE offset, so the ring sits INSIDE the input's border rather than crossing it. This is
   the third distinct focus-ring geometry in this one view (input +1px, this -2px, the reset
   button +2px/4px). Do not normalise them. */
.login-dialog__password-toggle:focus-visible {
  outline: 2px solid var(--coral);
  outline-offset: -2px;
  border-radius: 10px;
}
/* The NEGATIVE 8px pulls the row up out of the form's 16px gap so it reads as belonging to the
   password field above it (net 8px). */
/* LEFT-JUSTIFIED (owner, 2026-09-21: "the buttons should be left-justified", then "'Log In'
   should be to the left of 'Cancel'"). `margin-top: auto` pins the row to the bottom of the
   form's flex column; the 8px sits on top of the form's 16px gap for 24px of clearance. */
/* The Reset Password view's opening line. 18px, so it needs no exception - the app's own
   `.lj-signup__lede`, which is a class of its own there precisely so this line could move to 18
   without dragging the "check your spam folder" line with it. */
.login-dialog__lede {
  margin: 0;
  color: var(--muted);
  font-size: 18px;
}
/* 16px, approved by the owner 2026-09-21: the app's own value for this panel
   (plans.css:2544-2551), ported with the rest of the Reset Password view so the two surfaces
   are identical. It is `.login-dialog__error` in the affirmative - same padding, same radius,
   same size - so the two read as one family of messages rather than two designs. */
.login-dialog__sent {
  margin: 0;
  padding: 12px 14px;
  border-radius: 10px;
  background: #e8f5e9;
  color: #1b5e20;
  font-size: 16px;
}
.login-dialog__sent strong {
  display: block;
  font-weight: 700;
}
.login-dialog__actions {
  display: flex;
  align-items: center;
  /* flex-END is the shared default, as the app has it (plans.css:2377-2384): Sign Up's two
     steps put Cancel and Continue on the right. The left-justification below is the LOG IN
     view's instruction only (owner, 2026-09-21: "the buttons should be left-justified"), and
     scoping it is what keeps that instruction from silently reaching a view it was never
     about. */
  justify-content: flex-end;
  gap: 12px;
  margin-top: auto;
  padding-top: 8px;
}
/* LEFT-JUSTIFIED ON THE SINGLE-COLUMN VIEWS ONLY (owner, 2026-09-21, about the Log In modal:
   "the buttons should be left-justified", then "'Log In' should be to the left of 'Cancel'").
   Mirrors the app's `.lj-signup__single .lj-signup__actions` (plans.css:2393-2395). */
.login-dialog__panel .login-dialog__actions {
  justify-content: flex-start;
}
/* 4px LESS SPACE ABOVE THE PAIR, IN TWO STEPS (owner, 2026-09-22: "Remove 2px of margin above
   these buttons (so they move up 2px)", then "2px more"). Taken off this row's own
   `padding-top: 8px` rather than added as a negative margin: the space above the row is
   the form's 16px flex gap plus that 8px, and 8 - 4 is the honest way to write 4px less of it.
   WORTH KNOWING WHY IT MOVES ANYTHING. The row also carries `margin-top: auto`, which in a flex
   column eats every pixel of free space above it - so had there been any, it would have absorbed
   this and nothing would have shifted. It resolves to 0 here only because the form's content
   already fills the box.
   Scoped to the LOG IN VIEW, as that view's two buttons' own padding is: this row is also Sign
   Up step one's, step two's and Reset's, and none of those asked to move.
   The OR separator and the provider pair below it move up the same 4px, which is what "move up"
   means for a child of a flex column - the view's content ends 4px shorter. Measured across both
   steps on this surface: form scrollHeight 485 -> 483 -> 481, and the gap from the password
   field to the Log In button 25 -> 23 -> 21. */
.login-dialog__actions--login {
  padding-top: 4px;
}
/* The coral pill. Radius 10px, NOT --radius. Colour is the literal #ffffff, not --card. The
   28px side padding is the app's Log In-only +8px over its shared `padding: 14px 20px`; this
   site has only the one submit in this modal, so it is written as one shorthand. Deliberately
   NOT `.btn btn--primary`, whose 13px 26px / 2px transparent border / #c16850 fill are all
   different numbers. */
.login-dialog__submit {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  font: inherit;
  font-size: 18px;
  font-weight: 600;
  /* 20px is the shared value; the Log In view takes 28 below. The +8 was an owner instruction
     about that view, and baking it into the shared rule would have made Continue, Back, Create
     Account and Go-to-log-in all 8px wider than the app's. */
  padding: 14px 20px;
  border: 0;
  border-radius: 10px;
  background: var(--coral);
  color: #ffffff;
  cursor: pointer;
}
/* The Log In view's submit (owner, 2026-09-21 for the wider sides, 2026-09-22 for the 4px:
   "Reduce the padding inside the 'Log In' modal buttons by 4px on all sides"). Shorthand rather
   than the two longhands it used to be, because the 4px moves the TOP and BOTTOM too, and those
   came from the shared rule's `padding: 14px 20px`: 14 -> 10, and the 2026-09-21 result 28 -> 24.

   NOW KEYED ON A MODIFIER, and that is a FIX as much as a scoping choice. It read
   `.login-dialog__panel .login-dialog__submit`, and every view in this dialog lives in that
   panel - so the +8px asked for on the Log In button was also reaching Send Reset Email and the
   reset-sent Close, which the app's twin never did. The app hit the identical bug through its
   own `__single` ancestor and fixed it the same way on 2026-09-22; this is marketing catching
   up, so those two buttons go back to the shared 20px sides and now match the app again.
   Mirrors `.lj-signup__single .lj-signup__submit--wide` in plans.css; D-060 binds the pair. */
.login-dialog__submit--wide {
  padding: 10px 24px;
}
.login-dialog__submit:hover:not(:disabled) { background: var(--coral-hover); }
.login-dialog__submit:disabled { opacity: 0.55; cursor: not-allowed; }
/* The outline twin: same 18px/600/radius 10px, transparent fill, 1px --line-hover border,
   --body-text label. `flex: none` stops it being squeezed. It does NOT fill on hover - both
   border and label just darken to --ink - which is why it cannot reuse `.btn--outline`, whose
   hover fills with --btn-ink. It is the modal's only visible exit. */
.login-dialog__cancel {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font: inherit;
  font-size: 18px;
  font-weight: 600;
  padding: 14px 20px;
  border: 1px solid var(--line-hover);
  border-radius: 10px;
  background: transparent;
  color: var(--body-text);
  cursor: pointer;
  flex: none;
}
.login-dialog__cancel:hover { border-color: var(--ink); color: var(--ink); }
/* MOVED BELOW THE BASE RULE ON PURPOSE. Both selectors are the same specificity, so ONLY source order
   separates them - placed above the base rule this modifier silently lost the tie and the
   button kept 14px 20px. Caught by measuring the rendered button, not by reading the diff.
   CANCEL'S HALF OF THE SAME 4px (owner, 2026-09-22): the shared `padding: 14px 20px` becomes
   10px 16px on the LOG IN VIEW'S Cancel alone. On a modifier for the reason directly above -
   an ancestor selector here reaches Reset's Cancel too. `--login` matches
   `.login-dialog__social--login`: in this dialog that suffix already means "the Log In view's
   variant of this thing". Mirrors `.lj-signup__cancel--login` in plans.css. */
.login-dialog__cancel--login {
  padding: 10px 16px;
}
/* PAYMENT METHODS' "Add New Card" (owner, 2026-10-12): the outline twin above, the full width of
   the modal, taking the primary button's colours on hover. Below the base rule for the same
   source-order reason as --login; the extra .login-dialog also outranks its (0,2,0) :hover. */
.login-dialog .methods-add-new {
  flex: 1 1 auto;
  justify-content: center;
  border-color: var(--line-hover);
}
.login-dialog .methods-add-new:hover:not(:disabled) {
  background: var(--coral);
  border-color: var(--coral);
  color: #ffffff;
}
.login-dialog .methods-add-new:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}



/* THE COUNTRY PHONE FIELD, ported from the app (plans.css:2592-2812) with tokens remapped.
   THREE SPECIFICITY TRAPS, all load-bearing:
   (a) `.login-dialog__phone .login-dialog__phone-input` is (0,2,0) and beats
       `.login-dialog__field input` (0,1,1) on class count, so it wins wherever it sits;
   (b) `.login-dialog__phone-search input` is (0,1,1), a TIE with `.login-dialog__field input`,
       so it must come BELOW it in the file - which it does, this block being appended;
   (c) the flag needs its explicit 22x16 and its own background-colour, because it is a <span>
       in a flex row with no intrinsic size.
   The panel rule sets NO position, left, width, max-height, top or bottom: site.js owns all six,
   and it is `position: fixed` from its first render so it escapes the dialog's overflow clip and
   the scrolling column both. */
.login-dialog__phone.is-invalid ~ .login-dialog__hint {
  color: #b3261e;
}
.login-dialog__phone {
  position: relative;
  display: flex;
  align-items: stretch;
  /* #D1CBC8 with the other fields (owner, 2026-09-23). THE BORDER IS ON THIS WRAPPER, not on
     `.login-dialog__phone-input`, which carries `border: 0` - the country button and the number
     share one box, so the box is the control. Changing only the input rule would have left this
     field on the old colour while every field around it moved. */
  border: 1px solid #d1cbc8;
  border-radius: 10px;
  background: var(--field-bg);
}
.login-dialog__phone:focus-within {
  outline: 2px solid var(--coral);
  outline-offset: 1px;
}
.login-dialog__phone.is-invalid {
  border-color: #b3261e;
}
/* 17px, approved by the owner 2026-09-22, with every other text input in these modals (16 -> 18
   -> 17 across two instructions the same day). A SUB-FLOOR EXCEPTION: under this site's 18px
   floor, and this dated comment is what `check:type-floor` reads.
   It carries its OWN size rather than inheriting the rule above, because
   `.login-dialog__phone .login-dialog__phone-input` is (0,2,0) and outranks
   `.login-dialog__field input`'s (0,1,1) - so every move of the shared input size has to be
   repeated here or the phone field is left behind, reading a different size from the two fields
   above it, which looks like a defect. */
.login-dialog__phone .login-dialog__phone-input {
  flex: 1;
  min-width: 0;
  border: 0;
  background: transparent;
  border-radius: 0;
  padding: 12px 14px;
  font: inherit;
  font-size: 17px;
  color: var(--ink);
}
.login-dialog__phone .login-dialog__phone-input:focus-visible {
  outline: none;
}
/* 16px, approved by the owner 2026-09-21: the country trigger sits INSIDE the phone field and
   must match the number beside it. The app's value, ported with the Sign Up view. */
.login-dialog__phone-trigger {
  order: -1;
  display: flex;
  align-items: center;
  gap: 6px;
  flex: none;
  padding: 0 10px 0 12px;
  border: 0;
  border-right: 1px solid var(--line);
  background: transparent;
  font: inherit;
  font-size: 16px;
  color: var(--ink);
  cursor: pointer;
  border-radius: 10px 0 0 10px;
}
.login-dialog__phone-trigger:hover {
  background: #f7f7f7;
}
.login-dialog__phone-flag {
  display: block;
  width: 22px;
  height: 16px;
  flex: none;
  border-radius: 3px;
  background-color: var(--line);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}
.login-dialog__phone-dial {
  font-variant-numeric: tabular-nums;
}
.login-dialog__phone-caret {
  flex: none;
  color: var(--muted);
}
.login-dialog__phone-panel {
  /* Placed by use-country-select.ts, which sets position, left, width, max-height and the
     top-or-bottom edge from a measurement. Nothing here may set those. */
  display: flex;
  flex-direction: column;
  z-index: 2;
  background: #ffffff;
  border: 1px solid var(--line);
  border-radius: 12px;
  box-shadow: 0 12px 28px rgba(29, 26, 24, 0.18);
  overflow: hidden;
}
.login-dialog__phone-search {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 12px;
  border-bottom: 1px solid var(--line);
  color: var(--muted);
}
/* 17px, approved by the owner 2026-09-22, with every other text input in these modals (16 -> 18
   -> 17 across two instructions the same day). A SUB-FLOOR EXCEPTION: under this site's 18px
   floor, and this dated comment is what `check:type-floor` reads.
   It IS a text input - the country panel's search box is a field a visitor types into - so it
   moves with the rest rather than being left as the odd one out.
   STILL ABOVE THE iOS FLOOR. 16px is the size iOS needs to not zoom the page on focus; 17 clears
   it. Do not take this one to 16 or under even if the others ever go there. */
.login-dialog__phone-search input {
  flex: 1;
  min-width: 0;
  border: 0;
  background: transparent;
  font: inherit;
  font-size: 17px;
  color: var(--ink);
}
.login-dialog__phone-search input:focus-visible {
  outline: none;
}
.login-dialog__phone-list {
  margin: 0;
  padding: 4px;
  list-style: none;
  flex: 1;
  min-height: 0;
  overflow-y: auto;
}
/* 16px, approved by the owner 2026-09-21: a row in the 245-country list, the app's own value.
   At 18px the panel shows noticeably fewer countries per screen for no gain. */
.login-dialog__phone-option {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border-radius: 8px;
  cursor: pointer;
  font-size: 16px;
  color: var(--ink);
}
.login-dialog__phone-option.is-last-pinned {
  margin-bottom: 4px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--line);
  /* NO `border-bottom-*-radius: 0` here, deliberately: plans-css-scope.test.ts
     scans the WHOLE file for that longhand and requires exactly two, both keyed
     on the comparison table's foot, so squaring this row's corners would fail a
     test about the table. The row only has a fill while it is highlighted, so a
     straight rule across a lightly rounded box is not visible anyway. */
}
.login-dialog__phone-option.is-active {
  background: #f1f1f1;
}
.login-dialog__phone-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.login-dialog__phone-code {
  text-align: right;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}
/* 16px, approved by the owner 2026-09-21: the "no country matches" line, sized to the option
   rows it replaces. The app's own value. */
.login-dialog__phone-empty {
  padding: 12px 10px;
  font-size: 16px;
  color: var(--muted);
}

/* THE TERMS BOX. A BOUNDED, SCROLLING BOX - which is the whole point of it: without the
   max-height and the overflow the document flows to its full length, and the agree tick and the
   Back / Create Account row are pushed off the bottom of the modal with no way to reach them.
   `overflow-y: scroll`, not `auto` (owner, 2026-09-21): the track is always drawn, so the box
   reads as scrollable before anyone touches it.
   THE DOCUMENT TYPOGRAPHY IS RESTATED HERE because the server sends real headings, and this
   site's own h1-h4 rule would otherwise render "LotJitsu Terms of Service" at page-title size
   inside a 180px box. The app does not have to do that; this site does. */
.login-dialog__terms-body {
  max-height: 135px;
  /* 135px, down from 180 in three steps (owner, 2026-09-22: "Reduce the height of the TOS box
     by 20px", then "remove 15px more", then "10px more"). It is
     a CAP, not a height: the box is only this tall when the document is longer than it, which
     the Terms always are. Shrinking it moves in the safe direction for the constraint the
     comment below records - the agree checkbox under this box has MORE room to stay reachable,
     not less. */
  /* ALWAYS SHOW THE SCROLLBAR (owner, 2026-09-21), so it is visible that there is
     more of the document than the box shows.
     `scroll` rather than `auto` reserves the track even when the content fits, but
     on its own that is not enough: macOS defaults to OVERLAY scrollbars that fade
     out when idle, so the track would be reserved and empty-looking. The
     `::-webkit-scrollbar` rules below opt this element out of overlay behaviour and
     give it a permanent, visible thumb.
     DO NOT ADD `scrollbar-width` OR `scrollbar-color` HERE. They look like the
     standards-track way to say the same thing, but in current Chrome the standard
     property WINS and the `::-webkit-scrollbar` rules are ignored entirely - which
     puts the overlay scrollbar back. Measured with `scrollbar-width: thin` set: the
     element reserved 2px, its border and nothing else. Without it: 12px, a drawn
     track. Firefox draws a classic scrollbar from `overflow-y: scroll` anyway. */
  overflow-y: scroll;
  padding: 12px 14px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: #ffffff;
  font-size: 18px;
  line-height: 1.5;
  color: var(--body-text);
}
/* AGREED (owner, 2026-10-17; #EDDAD5 from 2026-10-09): the box takes light green #DDF4DC once
   "I agree" is ticked, and goes back to white if it is unticked. site.js toggles the class. The
   app's `.lj-signup__terms.is-agreed` copy went with its Sign Up modal in alpha v3.34.0, so this
   site is the only one. */
.login-dialog__terms.is-agreed .login-dialog__terms-body {
  background: #ddf4dc;
}
.login-dialog__terms-body h1 {
  margin: 20px 0 12px;
  font-size: 22px;
  font-weight: 700;
  color: var(--ink);
}
.login-dialog__terms-body h1:first-child,
.login-dialog__terms-body h2:first-child,
.login-dialog__terms-body h3:first-child {
  margin-top: 0;
}
.login-dialog__terms-body h2 {
  margin: 20px 0 8px;
  font-size: 18px;
  font-weight: 600;
  color: var(--ink);
}
.login-dialog__terms-body h3 {
  margin: 16px 0 8px;
  font-size: 18px;
  font-weight: 600;
  color: var(--ink);
}
.login-dialog__terms-body h4 {
  margin: 12px 0 6px;
  font-weight: 600;
  color: var(--ink);
}
.login-dialog__terms-body p {
  margin: 0 0 12px;
}
.login-dialog__terms-body p:last-child {
  margin-bottom: 0;
}
.login-dialog__terms-body ul {
  margin: 0 0 12px;
  padding-left: 24px;
  list-style: disc;
}
.login-dialog__terms-body ol {
  margin: 0 0 12px;
  padding-left: 24px;
  list-style: decimal;
}
.login-dialog__terms-body li {
  margin-bottom: 4px;
}
.login-dialog__terms-body a {
  color: var(--coral);
  text-decoration: underline;
}
.login-dialog__terms-body hr {
  margin: 16px 0;
  border: 0;
  border-top: 1px solid var(--line);
}
.login-dialog__terms-body blockquote {
  margin: 0 0 12px;
  padding-left: 12px;
  border-left: 2px solid var(--line);
}
.login-dialog__terms-body table {
  margin: 0 0 12px;
  width: 100%;
  border-collapse: collapse;
}
.login-dialog__terms-body th,
.login-dialog__terms-body td {
  border: 1px solid var(--line);
  padding: 4px 8px;
  text-align: left;
}
.login-dialog__terms-body th {
  font-weight: 600;
}
.login-dialog__terms-body::-webkit-scrollbar {
  -webkit-appearance: none;
  width: 10px;
}
.login-dialog__terms-body::-webkit-scrollbar-track {
  background: var(--lj-band);
  border-radius: 10px;
}
.login-dialog__terms-body::-webkit-scrollbar-thumb {
  background: var(--line-hover);
  border-radius: 10px;
  border: 2px solid var(--lj-band);
}
.login-dialog__terms-body::-webkit-scrollbar-thumb:hover {
  background: var(--muted);
}
.login-dialog__terms-body:focus-visible {
  outline: 2px solid var(--coral);
  outline-offset: 1px;
}

/* ========================================================== THE SIGN UP VIEWS
   Ported rule-for-rule from the app's /pricing Sign Up modal (plans.css), owner 2026-09-21:
   "I need for the the exact same modal experience to happen the marketing site." Everything
   above this point is shared with the Log In and Reset Password views and is NOT restated. */

/* ONE SCROLLER FOR THE PAIR. Nested scrollers were measured opening step two already 140px
   down, because the outer column kept the scroll position the inner form had left. */
.login-dialog__cols {
  display: grid;
  grid-template-columns: 50% 50%;
  min-height: 0;
  overflow-y: auto;
}
.login-dialog__cols .login-dialog__form {
  overflow: visible;
}

/* THE PROMO COLUMN IS DARK. It went light and then dark again the same day in the app; this is
   the live value, not the superseded one. */
.login-dialog__promo {
  background: #1a1a1a;
  color: #ffffff;
  padding: 28px 24px;
  display: flex;
  flex-direction: column;
  gap: 22px;
}
/* Also resets this site's own h3 rule, which the app has no equivalent of. */
.login-dialog__promo-head {
  margin: 0;
  font-family: var(--font-head);
  font-size: 30px;
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: normal;
  color: #ffffff;
}
.login-dialog__promo-list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 16px;
}
.login-dialog__promo-list li {
  display: flex;
  flex-direction: row;
  align-items: flex-start;
  gap: 10px;
}
/* THE BELT ROW CENTRES its "<Belt> is <price>" line on the knot (owner, 2026-10-12: "vertically
   centered/aligned with the yellow belt icon"); the tick rows keep flex-start. The wrapper is a
   flex box so its height is the knot's 38px, not a line box around an inline-block. */
.login-dialog__promo-list li.login-dialog__promo-belt-row {
  align-items: center;
}
.login-dialog__promo-belt-row > .login-dialog__belt {
  display: flex;
}
.login-dialog__benefit {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
/* CORAL, not green. */
.login-dialog__tick {
  color: var(--coral);
  font-size: 22px;
  line-height: 1.2;
  flex: none;
  font-weight: 700;
}
/* THE 3px THE TICKED ROWS TAKE, ported from the app's `.lj-signup__tick + .lj-signup__benefit`
   (2026-09-22). The promo list's rows share a 10px gap, but the belt-knot row and the tick rows
   do not present the same left edge - the knot is a masked SVG with its own internal padding and
   already carries a transform of its own, while the tick is a glyph with none. The app nudges the
   TEXT beside a tick 3px right to line the two up; this side had the knot's counterpart rule but
   not this one, so the checklist read as very slightly ragged against the app's. */
.login-dialog__tick + .login-dialog__benefit {
  margin-left: 3px;
}
.login-dialog__promo-list strong {
  font-size: 18px;
  font-weight: 400;
  color: #ffffff;
}
/* THE TIGHT SELECTOR IS LOAD-BEARING: `.login-dialog__promo-list span` would be (0,2,1) and
   would beat the tick's (0,2,0), painting the tick this grey instead of coral. */
.login-dialog__benefit span {
  font-size: 18px;
  color: #c9c3bd;
}
.login-dialog__alt {
  margin: auto 0 0;
  font-size: 18px;
  color: #c9c3bd;
}

/* THE PROVIDER BUTTONS. 61px is DERIVED: 12 + 34.65 + 12 + 1 + 1 = 60.65, rounded up, and it
   exists because Apple's 34.65 mark against Google's 23 would otherwise leave the two buttons
   different heights. Recompute it if either mark is resized. #dadce0/#757575 are Google's own
   values; the 25px radius belongs to these two buttons only. */
.login-dialog__social {
  display: flex;
  flex-direction: column;
  margin-top: 7px;
  gap: 14px;
}
/* THE AGREEMENT LINE under each provider pair (owner, 2026-10-06: "By signing-in you agree to
   the LotJitsu terms & conditions."). 16px and centred since 2026-10-12 (the approval is on the
   rule below). Its colour follows where it sits:
   the Sign Up rail's light grey on the dark promo column, --muted on the Log In view's light
   panel. The "link" is a <button> wearing `.login-dialog__alt-login`'s look - coral, 600, underlined - the modal's
   one link style. Identical to the app's `.lj-signup__consent`; D-060 binds the pair. */
/* 16px, 2px under the floor, and centred under the provider buttons: approved by the owner
   2026-10-12, first for the Log In view ("reduce the font-size ... by 2px"), then the Sign Up
   rail ("centered and 2px smaller font-size"). */
.login-dialog__consent {
  margin: 0;
  font-size: 16px;
  text-align: center;
  color: #c9c3bd;
}
.login-dialog__social--login .login-dialog__consent {
  color: var(--muted);
}
.login-dialog__consent-link {
  font: inherit;
  font-weight: 600;
  border: 0;
  background: none;
  padding: 0;
  cursor: pointer;
  color: var(--coral);
  text-decoration: underline;
}
.login-dialog__consent-link:hover { color: var(--coral-hover); }
.login-dialog__social-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  width: 100%;
  font: inherit;
  font-size: 18px;
  font-weight: 500;
  padding: 12px 16px;
  border-radius: 25px;
  cursor: pointer;
  text-align: center;
  min-height: 61px;
}
/* THE "OR" SEPARATOR between the actions row and the provider pair (owner, 2026-09-22, to a
   mockup: "Add a 'OR' between two lines separator between the Login/Cancel buttons and the
   social Login buttons. It should look exactly (pixel perfect) like this mockup"). Identical to
   the app's `.lj-signup__or` in plans.css, which is the reference; D-060 binds the pair.
   LOG IN VIEW ONLY. The Sign Up rail's pair sits in the promo column, which the instruction did
   not name, so it keeps its undivided layout.

   18px, SO THIS ONE IS NOT A FLOOR EXCEPTION - worth saying out loud, because every other rule
   in this ported modal is. It lands on the floor by measurement rather than by choice: see below.

   EVERY VALUE IS MEASURED OFF THAT MOCKUP, which was captured at 2x. The scale is not assumed,
   it is proved twice: the Cancel button's 1px border lands on exactly 2 device pixels, and the
   social button's `min-height: 51px` on exactly 102.
     colours  #a4a4a4 rules / #9a9a9a text  TWO SHADES DARKER than the #cacaca / #bebebe the
                       owner gave directly when this was built (owner, 2026-09-23: "make the
                       separator ('OR' flanked by 2 lines) 2 shades darker"). A shade is a 10%
                       step down in each channel - this file's own definition, the one it wrote
                       down for the footer's copyright line and that plans.css cites back to it
                       for --lj-line-hover. So 202/202/202 x 0.81 = 164 (#a4a4a4) and
                       190/190/190 x 0.81 = 154 (#9a9a9a). DERIVED, NOT EYEBALLED, so the next
                       "two shades" lands the same way. The other values below are still the
                       mockup measurements and did not move.
     type     18px     the R's cap measures 26 device px, and this modal's field label
                       calibrates the font's cap height at 0.719em, so 13 / 0.719 = 18.08
     weight   700      the R's stem measures 5 device px = 0.139em, which is Bold, not 600
     gap      16px     33-34 device px from each rule's end to the glyph ink, less the side
                       bearing the glyph carries inside its own box
     rules    2px      the mockup draws 5 device px, i.e. 2.5 CSS px. ROUNDED DOWN to 2, because
                       2.5px resolves inconsistently on a 1x display and half a device pixel at
                       2x is not visible. 2.5px is the literal match if it is ever wanted.

   NO MARGINS, deliberately, and this is the part not to "tidy" later. `.login-dialog__form` is a
   16px-gap flex column and this is one of its children, so the ~29px of clear space the mockup
   shows above and below the rule is that 16px plus the text's own half-leading - 18px at the
   body's 1.65 line-height is a 29.7px box with the ink in the middle of it. A margin added here
   would be added ON TOP of the gap, not instead of it, and would double what was signed off.
   Measured on the built page afterwards: 16px above, 16px below, a 29.7px box. */
.login-dialog__or {
  display: flex;
  align-items: center;
  gap: 16px;
  font-size: 18px;
  font-weight: 700;
  color: #9a9a9a;
}
/* The two rules, as pseudo-elements rather than real elements: they are decoration with nothing
   to announce, so a screen reader hears the word alone. `flex: 1` on both is what makes them
   equal without either being measured - the mockup has them at 343 device px each, to the pixel,
   which is what an even split of what is left comes to. */
.login-dialog__or::before,
.login-dialog__or::after {
  content: "";
  flex: 1;
  height: 2px;
  background: #a4a4a4;
}
/* THE LOG IN VIEW'S PAIR, below the actions row (owner, 2026-09-22: "Add 'Log In' versions of
   these two buttons below the Login and Cancel buttons. The font-size of the social buttons
   needs to be 2px smaller and the padding used in the buttons needs to be 5px less on all size
   of each button"). Identical to the app's `.lj-signup__social--login` in plans.css, which is
   the reference; D-060 binds the pair.
   The 7px above is a correction for the promo column's 22px gap and there is no such gap here,
   so it goes back to 0 and the form's own spacing separates this from the buttons above it. */
.login-dialog__social--login {
  margin-top: 0;
}
/* 16px, approved by the owner 2026-09-22: an exception to the site's 18px floor, and the same
   collision CLAUDE.md names - this pair is ported rule-for-rule from the app, whose own floor is
   16px, so matching it means going under this site's floor. The owner's identical-surfaces rule
   is what approves it; this comment is the record the check reads.
   ALL THREE VALUES ARE DERIVED from the Sign Up pair above rather than picked:
     the type   18px -> 16px        the asked-for 2px
     padding    12px 16px -> 7px 11px   the asked-for 5px, on every side
     min-height 61px -> 51px        NOT asked for, and required for the padding to do anything
   That last one is the only judgement here. 61px is not a chosen height: it is what the Sign Up
   button already measures - Apple's 34.65px mark, plus 12px of padding top and bottom, plus the
   1px border each side - written as a floor so the two providers come out equal. Left at 61 it
   swallows the whole 10px this instruction takes out of the vertical padding: the button keeps
   its height and only the content re-centres, so the change would be invisible. 51px is that sum
   recomputed (34.65 + 7 + 7 + 2 = 50.65). It stays above the 44px minimum tap target, which is
   the one floor a shorter button could have crossed.
   NOT TOUCHED, because the instruction named the type and the padding and nothing else: the
   marks and their sizes, the colours, the 25px radius, the 14px gap between the two buttons and
   both of Apple's optical nudges. All shared with the pair above. */
.login-dialog__social--login .login-dialog__social-btn {
  font-size: 16px;
  padding: 7px 11px;
  min-height: 51px;
}
.login-dialog__mark {
  flex: none;
}
.login-dialog__mark--google {
  margin-left: 4px;
}
/* THE LABEL IS #313131 - TWO SHADES LIGHTER THAN APPLE'S #000000 (owner, 2026-09-22), identical
   to the app's copy of this button. A shade is a 10% step, this site's own definition, so from
   0/0/0 the steps are 26/26/26 and then 49/49/49.
   It was #757575, Google's published grey for this variant, so this remains a deliberate
   departure from their spec, not an oversight: two sign-in buttons of the same shape in one
   dialog, reading in two different greys, looked like one was disabled. */
/* THE APPLE MARK SITS 1px HIGHER (owner, 2026-09-22), identical to the app's copy of this
   button. A transform, so it moves the glyph alone and leaves the gap to its label and the
   button's height untouched. */
/* Both of the mark's moves in ONE declaration - a transform is a single property, so the pair's
   3px left shift below would otherwise replace this glyph's 1px lift. */
.login-dialog__mark--apple { transform: translate(-6px, -1px); }
/* 5px between Apple's mark and its label, 7 less than the shared 12 (owner, 2026-09-22), and
   identical to the app's copy. The GAP rather than a margin: it is the space between the two
   items that changes, so the pair stays centred on the button. Google keeps the 12. */
.login-dialog__social-btn.login-social--apple { gap: 5px; }
/* The whole Apple pair sits 6px left of centre (owner, 2026-09-22), identical to the app's copy.
   On the CHILDREN, not the button: the button keeps its box and its centring, and the 5px gap
   between mark and label is untouched. */
.login-dialog__social-btn.login-social--apple > span { transform: translateX(-6px); }
.login-dialog__social-btn.login-social--google {
  background: #ffffff;
  border: 1px solid #dadce0;
  color: #313131;
}
.login-dialog__social-btn.login-social--google:hover {
  background: #f8f9fa;
}
/* The white border is invisible and deliberate: it keeps this button the same height as
   Google's, which has a real one. */
.login-dialog__social-btn.login-social--apple {
  background: #ffffff;
  border: 1px solid #ffffff;
  color: #000000;
}
.login-dialog__social-btn.login-social--apple:hover {
  background: #f1f1f1;
  border-color: #f1f1f1;
}
/* LIGHTER ON HOVER, LOG IN VIEW ONLY (owner, 2026-09-22: "Change the hover background color of
   the 'Log In with Apple' button displayed inside the 'Log In' modal to 1 shade lighter"). The
   Sign Up rail's Continue-with-Apple keeps #f1f1f1 above; the instruction named one modal.

   #f8f8f8 IS NOT THIS REPO'S "ONE SHADE" AND THE OWNER CHOSE IT KNOWING THAT. A shade here is
   the 10% channel step defined at --line-hover, mirrored for lightening as 255 - (255 - c) x 0.9
   - the same ladder that put Apple's label at #000000 -> #1a1a1a -> #313131. From #f1f1f1 that
   is #f2f2f2: 241 -> 242, one part in 255, invisible. The ladder compresses to about 1.4 per
   step this close to white, so no small number of shades reads as lighter and the room between
   #f1f1f1 and white is nearly spent.
   #f8f8f8 is the neutral-grey twin of #f8f9fa, which is GOOGLE'S OWN published hover for the
   button directly below this one. So the two providers' buttons now lighten by the same amount
   on hover, which is a reason that survives the next palette change - unlike a value picked by
   eye. Asked and answered before shipping, with the invisible-but-correct option on the table. */
.login-dialog__social--login .login-dialog__social-btn.login-social--apple:hover {
  background: #f8f8f8;
  border-color: #f8f8f8;
}
/* NO AGREEMENT LINE UNDER EITHER PAIR (owner, 2026-09-23: "By continuing, you agree to the
   LotJitsu Terms of Service" removed completely, Sign Up and Log In, on both sites; D-060). The
   line and the Terms box its link opened are gone from the markup, and their two rules with
   them. The pairs still send terms_version_id - see site.js. */
/* DISABLED UNTIL THE ACTIVE TERMS VERSION HAS LOADED, which is usually before anyone can reach
   for them. If it never does, the pair stays in view and visibly unavailable rather than taking
   a press it cannot complete. The same treatment `.login-dialog__inline-link:disabled` uses. */
.login-dialog__social-btn:disabled {
  cursor: not-allowed;
  opacity: 0.6;
}

/* margin-LEFT auto: the counter is pushed to the right-hand end of the head row. */
.login-dialog__step {
  margin: 0 0 0 auto;
  font-size: 18px;
  color: var(--muted);
}
.login-dialog__label-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}
/* 14px, approved by the owner 2026-09-21 ("reduce the font-size of '*for security' on both
   modals it is used by 2px"); it was 16px earlier the same day, which was the app's value when
   this modal was ported. The app's copy moved to 14 in the same change, so the two still agree.
   It sits on the Phone label's row, right-justified against it, and is a note rather than a
   second label - which is why it is allowed to read quieter than the field it annotates.
   This is now the SMALLEST type in this modal and one of only three 14px exceptions on the
   whole site, the others being the help aside's search field and the account menu's email. */
.login-dialog__note {
  /* Nudged down 2px (owner, 2026-09-21), after the size came down. `position: relative` + `top`
     rather than a margin: this is a flex item on an `align-items: baseline` row, so a margin
     would move the row's shared baseline and carry the "Phone" label down with it. A relative
     offset shifts the paint only. */
  position: relative;
  top: 2px;
  font-style: normal;
  font-size: 14px;
  font-weight: 400;
  color: var(--muted);
}
/* 16px, approved by the owner 2026-09-21: the app's own value (plans.css:3031-3034). This is the
   Check Your Email view's "check your spam folder" line. Deliberately NOT raised to 18 - the app
   split `__lede` (18px, already here) out of this class precisely so the Reset Password view's
   opening line could move and this one could not. */
.login-dialog__muted {
  color: var(--muted);
  font-size: 16px;
}
.login-dialog__agree {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  font-size: 18px;
  color: var(--body-text);
}
.login-dialog__agree input {
  margin-top: 3px;
  width: 18px;
  height: 18px;
  flex: none;
}
.login-dialog__submit-arrow,
.login-dialog__back-arrow {
  flex: none;
  transition: transform 0.18s ease;
}
.login-dialog__submit:hover:not(:disabled) .login-dialog__submit-arrow {
  transform: translateX(3px);
}
.login-dialog__cancel:hover .login-dialog__back-arrow {
  transform: translateX(-3px);
}
@media (prefers-reduced-motion: reduce) {
  .login-dialog__submit-arrow,
  .login-dialog__back-arrow {
    transition: none;
  }
}
.login-dialog__done {
  text-align: center;
}
/* The ADDRESS only, by its hook: the resend row's "Sent - check your inbox." is a <strong> in this
   view too and must stay inline beside its countdown. */
.login-dialog__done [data-signup-done-email] {
  display: block;
  margin-top: 4px;
  word-break: break-all;
  /* 26px (owner, 2026-10-09: the line above it, 30px, less 4). */
  font-size: 26px;
}
/* LARGER TEXT (owner, 2026-10-09). The opening line: 18 + 4, then 8 more = 30 (the address in it
   is 26, above). The muted lines (Resend Verification Email and the spam line): 16 + 4 = 20.
   Scoped to this view so the shared .login-dialog__muted keeps its 16 elsewhere. = the app's
   `.lj-signup-dialog.is-done` rules. */
.login-dialog__done > p:first-child {
  font-size: 30px;
  line-height: 1.3;
}
.login-dialog__done .login-dialog__muted {
  font-size: 20px;
}


/* ------------------------------------------------------------------- header */
.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  /* 8px above the row (owner, 2026-10-27: "add 8px above the headers, this should push the rest
     of the page down 8px"), kept in the bar while it is stuck, by the owner's choice the same
     day, so the logo and the nav sit 8px lower at every scroll position and every width. The
     header is 82px: this, the 72px row and the 2px rule. Everything that clears it moved 8px in
     step (.sec-bar, .post-aside, .help-aside and .help-rail, #faq, .guide-section). The app's
     marketing-layout header (alpha, components/layout/header.tsx) carries the same 8px. */
  padding-top: 8px;
  background: rgba(237, 235, 232, 0.94);
  backdrop-filter: blur(8px);
  /* The rule shows only once the page has scrolled off the top, and is 2px when it
     does (owner, 2026-09-14): site.js adds is-scrolled. Transparent at rest, never
     removed, so the header is 82px either way and nothing that clears it (.sec-bar,
     the TOC) shifts when the class flips. With JS off the rule simply never shows. */
  border-bottom: 2px solid transparent;
  /* The fill and the rule FADE in and out with that class rather than snapping (owner,
     2026-09-17). 0.25s is the site's control-hover tier, not a new value. Colour only, so it
     is left ungated like the button colour transitions: `prefers-reduced-motion` is about
     movement, and nothing here moves. Named properties rather than `all`, or the header's
     other states would start easing too. It applies in both directions, so returning to the
     top fades back without a second rule. */
  transition:
    background-color 0.25s ease,
    border-bottom-color 0.25s ease;
}
/* Scrolled off the top: the rule appears AND the bar darkens to #E1DDDC (owner, 2026-09-17),
   on the one class site.js already toggles, so the two can never disagree. Opaque, where the
   resting bar is the page surface at 94%: once the header is over content rather than the top
   of the page, a solid bar is what makes it read as a layer. `backdrop-filter` above still
   applies but has nothing to show through while this is opaque; it is left alone because it is
   what the resting state needs. A literal, since the site has no token between --surface and
   --band. With JS off the class never lands and the header simply stays as it rests. */
/* #E1DDDC at 0.9 alpha (owner, 2026-09-17), not opaque: the scrolled bar stays translucent, a
   touch more so than the 0.94 it rests at, so the `backdrop-filter` above keeps doing its job
   and content reads as passing UNDER the header rather than behind a solid slab. Written rgba
   rather than hex-with-alpha so the transition from the resting rgba is a plain colour
   interpolation with no format change in the middle.
   #C8C8C8 for the rule rather than `--line` (owner, 2026-09-17, walked down from #959595 to
   #BFBFBF to this): it has to carry against the darker fill, where --line was effectively
   invisible. Measured against the bar as it composites over the page (#e3dedd): --line 1.04,
   this 1.26, the #BFBFBF before it 1.38, the #959595 first tried 2.25. A soft divider by
   choice, well under the 3:1 an interface boundary is usually held to. */
.site-header.is-scrolled {
  border-bottom-color: #c8c8c8;
  background: rgba(225, 221, 220, 0.9);
}
.site-header__inner {
  display: flex;
  align-items: center;
  gap: 28px;
  min-height: 72px;
}
/* The logo must NEVER distort (owner, 2026-08-09): flex-shrink 0 stops the
   header row from compressing its slot, and max-width none exempts it from
   the global img max-width clamp that squished width while height stayed
   pinned. Height + width:auto keeps the intrinsic aspect at every size. */
.site-header__logo { flex-shrink: 0; }
.site-header__logo img { height: 46px; width: auto; max-width: none; }
/* The header's two wordmarks (owner, 2026-11-04: "For dark theme use the header logo where the
   'LotJitsu' text is white", then the owner's on-dark SVG): the template emits the light-mode
   PNG and images/LotJitsu-logo-on-dark.svg, in the header and in the drawer's brand, and the
   theme class on <html> shows one and hides the other. CSS rather than a script swap, so the
   dark logo is on the first frame like the dark page; the hidden one is a 20KB file. */
.site-header .logo--dark { display: none; }
html.theme-dark .site-header .logo--light { display: none; }
html.theme-dark .site-header .logo--dark { display: inline; }
/* 45px since 2026-09-22 (owner: "we need 15px more spacing between the menu links"); 30px from
   the v0.3.0 rebuild until then. This is the DESKTOP BAR only - below 850px `.site-nav` becomes
   the drawer, which sets its own `gap: 26px` in that query and is untouched by this. The app's
   ported copy of this header moved by the same 15px on the same day. */
.site-nav { display: flex; gap: 45px; margin: 0 auto; }
/* 20px since 2026-09-21 (owner: "increase the font-size of the header links by 2px"). It was
   18px, the site's body floor, from the v0.3.0 rebuild until then. The drawer's own override
   inside the 850px query moved by the same 2px, so the drawer stays a shade larger than the bar
   the way it always has - see `.site-nav a` down there. */
.site-nav a {
  color: var(--ink);
  font-family: var(--font-head);
  font-weight: 500;
  font-size: 20px;
}
.site-nav a:hover, .site-nav a[aria-current="page"] { color: var(--coral); }
/* Drawer-only elements (mobile slide-out); the desktop nav is an inline row. */
.site-nav__login, .site-nav__brand, .site-nav__foot, .site-nav__account-head, .site-nav__account, .drawer-overlay, .nav-close { display: none; }
.site-header__actions { display: flex; align-items: center; gap: 33px; }
.site-header__login { color: var(--ink); font-weight: 500; font-family: var(--font-head); }
.site-header__cta { padding: 7px 16px; white-space: nowrap; }

/* Signed in (owner, 2026-09-14): Log In is hidden and the account chrome the template
   emitted hidden is shown, by the session-hint block in site.js. See docs/SESSION-HINT.md.
   The hidden attribute is the only switch, and it has to beat every display these carry:
   .btn's inline-flex, .account's flex, and the drawer's pill rules two classes deep. Hence
   one !important, scoped to exactly these hooks rather than a global [hidden] reset. */
[data-session-login][hidden],
[data-session-account][hidden],
[data-account-list][hidden],
[data-session-email][hidden],
[data-session-signout][hidden] { display: none !important; }

/* The desktop account menu: Manage Catalogs, then the avatar that opens the app's account
   menu. Geometry follows the app's header menu (bottom-end placement 8px below the avatar,
   8px radius, 4px padding, 6px row radius); colours are this site's tokens rather than the
   app's dark chrome, and the rows sit at this site's 18px floor, not the app's 14px. No
   z-index: the list is inside the sticky header, which is already 50 on the ladder. */
.account { display: flex; align-items: center; gap: 12px; }
/* Nudged 6px left (owner, 2026-09-17, as 5px, then 2px more, then 1px back). Margin rather
   than a relative offset, because the actions block is the last flex item in .site-header__inner
   and .site-nav's `margin: 0 auto` pins its right edge to the container: extra width on this
   button therefore walks the BUTTON left and leaves the avatar exactly where it was, which
   is what was asked. A relative offset would have moved it without reserving the space, and
   at the 851px end the gap to the nav is only ~10px. The .account gap above stays 12px, so this reads as what it is, a nudge
   on one control, and reverts in one line. Only ever visible >=850px; below that .account is
   display:none and the drawer's full-width pill takes over, so this never reaches mobile.
   This note sits ABOVE the approval block, never between it and the rule: check-type-floor
   reads only the comment block directly above a rule, so splitting the approval off turns
   the 14px label into a sub-floor rule with no dated sign-off and the suite goes red. It
   did. This note deliberately avoids the word the check greps for, so it can never stand in
   for the real block: proved by deleting that block and watching type-floor go red. */
/* A compact button, smaller than the header's 48px Log In (owner, 2026-09-14): about 42px
   tall with 10px 12px of padding. Its 14px label is a deliberate, owner-approved exception to
   the 18px text floor (2026-09-14, the site's third), overriding the owner's own earlier
   choice to keep it at 18px. It applies to this label only. */
.account__catalogs { padding: 10px 12px; margin-right: 6px; font-size: 0.875rem; line-height: 1.25; white-space: nowrap; }
/* Signed in, the actions take about 135px more than Log In did, and from the 850 collapse up
   to ~1,050px the nav wrapped onto two lines. No new width query (check:breakpoints allows
   five): the logo and the row's gaps scale with the viewport instead, back to the logged-out
   values by ~1,100px (logo) and ~1,300px (gaps). Only while signed in, so the logged-out
   header is untouched; site.js adds is-signed-in. These selectors outrank the drawer's, so
   the 850px block restates those. Re-measured 2026-09-17, after the nav went from four items to
   five (Blog added, "Catalog Platforms" shortened to "Compatibility"): still no wrap and no
   overflow, with 61px of slack left at 851px, the tightest point (71px before "Plans" became "Pricing"). Originally measured
   2026-09-14 at 851, 875,
   900, 950, 1000, 1040, 1100 and 1280px. */
.site-header.is-signed-in .site-header__inner { gap: clamp(10px, 4vw - 24px, 28px); }
.site-header.is-signed-in .site-header__logo img { height: clamp(32px, 5.6vw - 16px, 46px); }
.site-header.is-signed-in .site-nav { gap: clamp(12px, 5vw - 30px, 30px); }
.site-header.is-signed-in .site-nav a { white-space: nowrap; }
.account__anchor { position: relative; }
.account__trigger {
  display: block;
  padding: 0;
  border: 0;
  background: none;
  border-radius: 50%;
  line-height: 0;
  cursor: pointer;
}
.account__trigger:focus-visible { outline: 2px solid var(--coral); outline-offset: 2px; }
.account__list {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  display: flex;
  flex-direction: column;
  /* The app's header menu, `min-w-[180px] p-1` (owner, 2026-11-05: same element sizes as the
     web app). It grows to its content, as the app's does. */
  min-width: 180px;
  padding: 4px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 8px;
  box-shadow: 0 10px 15px -3px rgba(29, 26, 24, 0.1), 0 4px 6px -4px rgba(29, 26, 24, 0.1);
}
.account__item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px;
  border-radius: 6px;
  /* The menu's text and icons (currentColor) are #232323 rather than --ink (owner, 2026-09-15).
     Sign Out keeps its own danger red below; the email keeps --muted. */
  color: #232323;
  font-family: var(--font-head);
  font-weight: 500;
  /* 14px on a 20px line, the app's `text-label-lg`, an approved exception to the 18px floor
     (owner, 2026-11-05: "The account menu on the homepage (lotjitsu-marketing) should have the
     same element sizes as when the account menu is displayed on an web app (alpha) page"). The
     rows' 44px height, 12px padding and 8px gap are the app's `px-3 py-3 gap-2`. The app is the
     reference (apps/web/src/components/layout/header.tsx). */
  font-size: 0.875rem;
  line-height: 20px;
  white-space: nowrap;
}
.account__item:hover,
.account__item:focus { background: var(--band); color: #232323; outline: none; }
/* The app's identity row: the avatar over a rule that separates it from the rest. */
.account__item--identity {
  gap: 12px;
  padding: 8px 12px 12px;
  margin-bottom: 4px;
  border-bottom: 1px solid var(--line);
  border-radius: 6px 6px 0 0;
  font-weight: 600;
}
/* The app's Font Awesome glyphs (see ACCOUNT_ICON in site-template.mjs). Visible overflow:
   gear and lock reach past the em box, and an inline <svg> clips to its box by default. */
.account-icon { flex: none; overflow: visible; }
/* The app's row icon: Font Awesome at `text-[19px]`, whose box is 1.25em wide, so wide glyphs fit
   and every label starts on one line. Popover only; the phone drawer keeps its 20px. */
.account__list .account-icon { width: 23.75px; height: 19px; }
/* The identity row's text: "Profile" as committed, then the name over the email once site.js
   has them from the app (owner, 2026-09-14). The email truncates rather than widening the menu
   without limit. The name sits at the 18px floor, where the app sets 14px. */
.account__who { display: flex; flex-direction: column; min-width: 0; line-height: 1.25; }
.account__name { font-weight: 600; color: #232323; }
/* The email's 14px, 4px under the name, is a deliberate, owner-approved exception to the 18px
   text floor (2026-09-15, the site's sixth). It applies to this line only; the app sets 12px. */
.account__email {
  display: block;
  max-width: 240px;
  overflow: hidden;
  text-overflow: ellipsis;
  font-size: 0.875rem;
  font-weight: 400;
  color: var(--muted);
}
/* The identity row in the popover, at the app's sizes: the name inherits the row's 14px on a 20px
   line (`text-label-lg`), both lines truncate at the app's `max-w-[200px]`. Scoped to the popover
   because the phone drawer shares these two classes and is not part of this change. */
.account__list .account__name {
  display: block;
  max-width: 200px;
  overflow: hidden;
  text-overflow: ellipsis;
  line-height: 20px;
}
/* 12px on a 16px line, the app's `text-label-md`, an approved exception to the 18px floor (owner,
   2026-11-05: same element sizes as the web app's account menu). */
.account__list .account__email {
  max-width: 200px;
  font-size: 0.75rem;
  line-height: 16px;
}
/* Sign Out is a <button>, so it sheds the button chrome to read as one more row. */
button.account__item { width: 100%; border: 0; background: none; text-align: left; cursor: pointer; }
/* Sign Out: the app's danger row in this site's error red, #b3261e (the Ask Sales form's error
   colour), rather than the app's red-600 (owner, 2026-09-14). 6.6:1 on white, 6.1:1 on its
   hover tint. */
.account__item--danger { color: #b3261e; }
.account__item--danger:hover,
.account__item--danger:focus { background: #fbeceb; color: #b3261e; }
/* The wrapper site.js fills with the avatar; line-height 0 stops an inline box adding a
   descender gap under the circle. */
.site-avatar-slot { display: block; flex: none; line-height: 0; }

/* 48px diameter (owner, 2026-08-14), 36px in the menu's Profile row. The <img>
   width/height attributes in site.js must match, or the layout shifts when the image
   finishes loading. */
.site-avatar {
  width: 48px;
  height: 48px;
  border-radius: 50%;
  object-fit: cover;
  display: block;
}
/* The initials circle: shown when the visitor has no photo, and swapped in by the
   img's onerror when one fails to load. Coral rather than the app's blue, because a
   lone blue element in a coral-and-ink header reads as a bug; the two surfaces agree
   on the BEHAVIOUR (initials as the fallback), not on this colour. Owner-noted
   2026-08-14 in docs/SESSION-HINT.md. */
.site-avatar--initials {
  background: var(--coral);
  color: #fff;
  font-family: var(--font-head);
  font-weight: 600;
  font-size: 18px;
  letter-spacing: 0.01em;
  display: flex;
  align-items: center;
  justify-content: center;
  /* The glyphs are uppercase A-Z0-9 only, so optical centring needs no descender room. */
  line-height: 1;
}
.site-avatar--sm { width: 36px; height: 36px; }
/* The close control lives INSIDE the drawer panel (.nav-close), so the header
   button is burger-only and hides itself while the drawer is open. */
.nav-toggle[aria-expanded="true"] { display: none; }
.nav-toggle {
  display: none;
  background: none;
  border: 0;
  font-family: var(--font-head);
  font-weight: 600;
  font-size: 1.125rem;
  color: var(--ink);
  padding: 8px;
  cursor: pointer;
}

/* --------------------------------------------------------------------- hero */
.hero { padding: 84px 0 72px; overflow: hidden; }
/* The home page's dark platforms band. It began as 30px more air than a hero's 84/72 top and
   bottom (owner, 2026-09-15); the top gave 10px back on 2026-09-16, then both sides took 20px
   more on 2026-09-17, so it now runs 40px more on top and 50px more underneath, and the band
   is not symmetrical (124 over 122). Its OWN modifier
   rather than `.hero.inverted`, because /sections' invert switch puts `.inverted` on all eight
   of the catalog's hero sections and they would each pick this up. Restated as flat values, so
   a future change to the `.hero` base is a deliberate decision here too rather than a silent
   drift. Above 850px only: the mobile block below sets `.hero`'s padding shorthand, which has
   equal specificity and comes later, so the phone keeps its 44px. */
.hero--platforms {
  padding-top: 124px;
  padding-bottom: 122px;
  /* A faint glow BEHIND THE HEADING (owner, 2026-09-17 "subtle", then "more towards the
     centre", then 2026-09-18 down to the ticker and straight back up to here). A white veil LAYERED over the band's
     `.inverted` background-colour rather than replacing it, so the ground stays on the
     --surface token. It ramps across a 40-point band and peaks at 4.5% opacity, which lifts
     #232323 to about #2d2d2d.
     The peak is at 40%, on the HEADING. Measured on the rendered band at 1440px: the heading
     occupies 31.5% to 49% and the ticker 57.6% to 69%, and 40% is the heading's own centre
     line. It started at 50%, which sat in the gap between the two and lit neither; it spent a
     few minutes at 63%, on the ticker, before the owner moved it up here.
     The ramp keeps its width, 20 points either side, so it runs 20% to 60% and leaves 20% of
     untouched #232323 above and 40% below. The top margin is the thinner one now, which is the
     mirror of what moving the glow down cost at the bottom.
     The image longhand only, for the same reason `.inverted` uses the colour longhand. */
  background-image: linear-gradient(
    180deg,
    rgba(255, 255, 255, 0) 0%,
    rgba(255, 255, 255, 0) 20%,
    rgba(255, 255, 255, 0.045) 40%,
    rgba(255, 255, 255, 0) 60%,
    rgba(255, 255, 255, 0) 100%
  );
}
/* This band's h2 only, at 60px against the global 34px (owner, 2026-09-16; 70px first, then
   60). Scoped rather than moved onto the `h2` element, which every other section shares. px,
   matching the scale's own `h2 { font-size: 34px }` rather than introducing a rem here. */
.hero--platforms h2 { font-size: 60px; }
/* Homepage hero sits 45px closer to the header (owner, 2026-08-09). */
.hero--snug {
  /* 19px: the base hero's 84px less the 45px this variant has always taken,
     less 30px more (owner, 2026-09-16, as 20 then 10), plus 10px back (owner,
     2026-09-17). It is the section's own padding, so the whole hero moves
     together and the page below it follows. At 9px the hero's first line
     cleared the sticky header by only 9px; this is that clearance too.
     Desktop only by construction: below 850px the block further down sets
     `.hero`'s padding shorthand, which has equal specificity and comes later,
     so it wins there and the phone keeps its 44px. */
  padding-top: 19px;
  /* 12px: the base hero's 72px less 60 (owner, 2026-09-17, as 20 then 20 then 20).
     Scoped here, not on `.hero`, which every other page's hero shares; and
     desktop only, by the same 850px override as the top.
     It is one of three things between the hero's copy and the first step icon:
     this, the steps section's top padding (an inline one-off on the section,
     overriding the shared 30px `--section-pad`), and the flow list's 8px top
     margin. That inline padding was 15px from 2026-09-17, after two tighter
     tries the same day were reverted (5px, which left the first step icon 2px
     under the hero mockup's bottom edge, and 11px); the owner took the last
     15 off on 2026-09-19 ("Remove 15px of spacing below the hero"), so it is
     0 and the run is this 12 plus the list's 8.
     DO NOT take more off THIS one: `.hero` is `overflow: hidden` and
     `.hero-mock` is nudged 10px down, and at 12px the image clears the hero's
     bottom edge by 2px (measured at 1440). Anything less clips the laptop.
     (The store badges that sat in flow under the image for part of 2026-09-19
     were removed the same day, so the image is against that edge again and the
     constraint is live.) Every reduction so far has come off the other two,
     which are scoped to that section and are where any further trim must come
     from as well: 5 of that 8 went on 2026-09-19 ("Remove 5px of spacing below the hero"),
     leaving the list's margin at 3px and this run at 15px in total. The section's inline top
     padding went back up to 20px on 2026-11-01 (owner: "Add 20px of space below the hero"),
     at every width, once the flow's visible head was gone, then down to 16 the same day
     ("Remove 4px of spacing below hero"); the run is 31px.
     The same section's BOTTOM padding is an inline one-off too, and is now 0:
     the shared 30 less 20 on 2026-09-19 ("Remove 20px of spacing below the 4
     step illustrated icons section") and then the last 10 the same day. Both
     ends of this band are hand-set, neither tracks `--section-pad` any more,
     and neither has anything left to give: the gap under the steps is now the
     list's own 26px bottom padding plus whatever the next section brings. */
  padding-bottom: 12px;
  /* Brand whirl behind the hero (owner, 2026-08-09). Alpha WebP, so it sits
     straight on the page surface with no white box. */
  background-image: url("/images/hero-whirl.webp");
  background-repeat: no-repeat;
  /* 5px further left than it began (owner, 2026-10-01, as 20 left, 25 more left, 35 back right,
     then 5 more right): the horizontal nudge was 120px right of centre, now 115px. Desktop only; the phone block drops this background and places its own whirl. */
  background-position: calc(50% + 115px) calc(50% + 60px);
  background-size: min(1000px, 100%) auto;
}
/* The home hero's H1 in #232323 (owner, 2026-09-17), the flat neutral the step titles and
   the account menu rows also took today, rather than the warmer --ink the shared
   `h1, h2, h3, h4` rule hands every other heading on the site. Scoped to THIS hero: the
   other pages' heroes are `.hero tight` and keep the token, so if the owner wants the new
   grey site-wide it is a change to --ink or to that shared rule, not a second copy here.
   `:not(.inverted)` is load-bearing. /sections' invert switch adds `.inverted` to all eight
   of its hero section roots, entry 31 among them, and that is this exact `hero hero--snug`
   root. `.inverted` recolours headings by REDEFINING --ink, which a hardcoded literal like
   this one steps straight over, so without the guard the specimen would render near-black
   on charcoal. The inverted block itself stays untouched, which is why the guard lives here
   rather than as a counterpart rule inside it that a later edit could forget. */
.hero--snug:not(.inverted) h1 { color: #232323; }
/* The home hero's H1 at 50px (owner, 2026-11-14: 6px up from the 44px set 2026-10-28, with the
   new "Quality auction catalogs in minutes." line). On the explicit home hook, not the base
   `h1` rule, so any other h1 that takes the base size keeps its 48px. Line height 2px over the
   base heading's 1.15 (owner, 2026-11-14): 57.5px to 59.5px at 50px. */
.hero--home h1 { font-size: 50px; line-height: calc(1.15em + 2px); }
/* The hero's lead paragraph takes --body-text (owner, 2026-09-17), the same token the four
   step descriptions use, instead of the lighter --muted that `.lead` carries by default.
   Scoped to this hero because `.lead` is shared by fourteen pages, blog headers included,
   and only this one was asked for. The TOKEN, not #3d3835: --body-text is redefined under
   `.inverted` (to #cfc9c4) and restored in the light island, so this follows the dark mode
   on its own and needs none of the `:not(.inverted)` guarding the H1 above requires. */
.hero--snug .lead { color: var(--body-text); }
.hero__grid {
  display: grid;
  grid-template-columns: 1.05fr 0.95fr;
  gap: 56px;
  align-items: center;
}
.hero .accent { position: relative; white-space: nowrap; display: inline-block; }
.accent__art { position: absolute; left: 1%; right: 1%; bottom: -0.16em; display: block; pointer-events: none; }
.accent__art .line { width: 100%; height: auto; display: block; transform-origin: left center; }
/* The pencil that sat at the end of the underline is GONE (owner, 2026-09-16):
   the `.pen` img, its rule and its `pen-in` / `pen-out` keyframes were all
   removed, and `/images/hero-pen.svg` is now referenced by nothing. The
   underline keeps its draw animation untouched. `.accent__art` still needs
   `position: absolute` and `pointer-events: none` above for the line itself,
   so only the pencil's own geometry went. */
@media (prefers-reduced-motion: no-preference) {
  /* PAGE TITLES DO NOT ANIMATE, ANYWHERE BUT THE HOME PAGE (owner, 2026-09-21). This was
     `.hero h1`, which caught every page whose title sits in a hero - /about, /contact,
     /privacy, /terms, /features, /platforms and its four children, and the /sections catalog:
     eleven titles rising on load that the owner does not want rising.
     `.hero--home` is an EXPLICIT hook on the home page's hero and not a reuse of `hero--snug`,
     which reads like a homepage marker and is not - `sections.html` carries it too, so keying
     off it would have left the catalog animating and quietly re-broken the rule the first time
     another page wanted snug spacing. The home page's own underline draw (`.accent__art .line`,
     below) is INSIDE this h1 and is deliberately left alone: it is part of the exception.
     Knock-on, accepted: the /sections catalog's specimen heroes no longer rise either. It is an
     unlinked, noindexed internal reference (check-sitemap.mjs keeps it out of the sitemap), so a
     0.5s entrance there is worth less than one selector that cannot drift.
     `check:title-animation` enforces this; add a page here and it fails. */
  .hero--home h1 { animation: rise 0.5s ease both; }
  .hero .hero__copy > *:not(h1) { animation: rise 0.55s ease 0.25s both; }
  @keyframes rise { from { opacity: 0; transform: translateY(34px); } to { opacity: 1; transform: none; } }
  .accent__art .line { animation: underline-draw 0.55s ease 0.55s both; }
  @keyframes underline-draw { from { transform: scaleX(0); } to { transform: scaleX(1); } }
}
/* Trust points: their own section, 3 columns, a large check left of each
   statement (owner, 2026-08-09; replaced the hero's inline trust-bar). */
.trust-cols {
  display: grid;
  /* EQUAL WHITE SPACE BETWEEN THE THREE ITEMS (owner, 2026-09-19: "There needs to be equal
     white-space between each of the checkmark+labels"). Content-sized tracks with
     `space-between`, so the free space is split into two identical gaps whatever the three
     phrases happen to be; the outer two keep their 70px insets as margins, and the row's
     `gap` below is now only a floor under those gaps.
     THIS REVERSES 2026-09-17, and deliberately. That day ran through first flush left / middle
     centred / last flush right, then the outer two inset 70px, then the longest phrase into
     the first slot and back to the middle; equal-gap spacing was tried in that sweep and was
     not what was meant. The shape it settled on, `1fr auto 1fr`, held the middle item exactly
     on the container's centre line, which is precisely what makes the two visible gaps
     unequal once the phrases differ in width. Equal gaps and a centred middle item cannot both
     be had unless the outer two are the same width. The owner has now asked for the gaps.
     Three equal thirds remain wrong for a different reason, so do not reach for them: a third
     is 347px at the container's cap, the 70px inset leaves 277, and "No credit card required"
     with its check needs about 290, so it wrapped at every desktop width. `auto` tracks cannot
     wrap that way, because each is sized to its own content.
     The 1000px block already used this exact shape and is now a no-op repeat of it; the 850px
     block stacks the row and returns every item to the left. */
  grid-template-columns: auto auto auto;
  justify-content: space-between;
  gap: 26px;
  margin: 0;
  padding: 0;
  list-style: none;
  color: var(--ink);
  /* 1.325rem is 21.2px: the 1.2rem (19.2px) it rested at plus the 2px the owner asked for
     (2026-09-17). Bold was tried in the same breath and taken back, so the weight is the
     inherited normal. Written in rem to stay on the same scale as the rest of the hero. The
     check glyph keeps its own size below. */
  font-size: 1.325rem;
  /* 26px (owner, 2026-10-06: every ~30px line to 24px, then to 26px). It was 29px: the
     then-inherited 34.98px (the body's old 1.65 at this size) less the 6px the owner asked off on
     2026-09-17. */
  line-height: 26px;
}
.trust-cols li { display: flex; align-items: center; gap: 16px; }
/* The outer two step in 70px from the container's edges (owner, 2026-09-17, as 30 then 40
   more). `space-between` counts these margins as part of the items, so the two gaps it
   distributes stay equal. The `justify-self` rules that used to sit here (centre on the
   middle, end on the last) went with the `1fr auto 1fr` tracks on 2026-09-19: with
   content-sized tracks there is no spare room inside a track to justify within. */
.trust-cols li:first-child { margin-left: 70px; }
.trust-cols li:nth-child(3) { margin-right: 70px; }
.trust-cols li::before {
  content: "✓";
  color: var(--green);
  font-weight: 700;
  font-size: 2.6rem;
  line-height: 1;
  flex: 0 0 auto;
}

.shot { border-radius: var(--radius-lg); box-shadow: var(--shadow); border: 1px solid var(--line); }
/* A screenshot with a dark-mode twin toggles on click (site.js, "screenshot light/dark toggle"). */
.shot[data-dark-src] { cursor: pointer; }
.shot[data-dark-src]:focus-visible { outline: 3px solid var(--coral); outline-offset: 3px; }
figure.shot-fig { margin: 0; }
figure.shot-fig figcaption { font-size: 1.125rem; color: var(--muted); margin-top: 10px; text-align: center; }

/* Homepage hero device mockup (owner, 2026-09-15): ONE supplied image of the
   Catalogs page on a laptop with a phone beside it. It replaced the device PAIR
   this rule used to build (a `.shot` desktop capture with a `.phone-frame`
   rotated -8deg over its bottom-right corner, `.hero-devices` +
   `.hero-devices__phone`, owner 2026-08-09), so both of those are gone.
   No border, radius or shadow here, unlike `.shot`: the artwork carries its own
   device bezels on a TRANSPARENT ground, so any of the three would draw a box
   around the empty corners rather than around the devices. */
/* 15% larger than its grid column (owner, 2026-09-16). The extra width has to
   come out of the hero's own margins, since the column is a `fr` share and
   widening THAT would squeeze the headline column and rewrap approved copy.
   The growth is centred rather than spilled to one side: half goes left into
   the grid's 56px gap, half right into the container's 24px padding and the
   page margin past it, so neither edge lands on text. `.hero` is
   `overflow: hidden`, so anything wider than the viewport is CLIPPED rather
   than scrolled - the 850px block below returns this to 100% because the
   single-column layout has no side room to spill into. */
/* `max-width: none` is load-bearing: the base `img` rule (line 66) caps every
   image at 100%, which silently held this at its column width and let only the
   negative margin through. The header and drawer logos escape the same cap the
   same way. */
/* The cap is what keeps the overspill from becoming a CLIPPED edge. Measured
   without it: between about 851px and 1188px wide the right edge lost up to
   13px, and only 3px of that is the artwork's transparent margin, so it was
   cutting the laptop. The image may spill by the room that actually exists to
   its right at the current width: the container's 24px padding, plus half the
   page margin once the viewport clears the 1140px container. Doubled, because
   the growth is centred. 1180 rather than 1140 is a deliberate 40px of slack,
   since `100vw` counts the scrollbar that `clientWidth` does not; it costs
   nothing above 1188px, where `width` is the smaller value and wins outright.
   Measured result: exactly 15% from about 1188px up, and never clipped at any
   width. Between 851 and 1188 it gives what fits, which is least at 1140 (9.8%,
   where the container is at its cap but the page margin has just run out) and
   recovers to 13.4% by 860 as the column itself shrinks. Below 850 the block
   further down returns it to its column, since one column has no side room. */
/* Nudged 10px down (owner, 2026-09-16). Deliberately `top`, not `margin-top`:
   `.hero__grid` is `align-items: center`, so a top margin would make this grid
   item taller AND get re-centred, landing about half the distance asked for.
   A relative offset moves the painted image exactly 10px and leaves the row
   geometry alone. The hero's 72px of bottom padding absorbs it, so it stays
   clear of `.hero`'s own overflow clip. */
.hero-mock {
  display: block;
  position: relative;
  top: 10px;
  width: 115%;
  max-width: calc(100% + 48px + max(0px, 100vw - 1180px));
  margin-left: -7.5%;
  height: auto;
}
/* The `<picture>` interposed 2026-09-18 so the poster can be offered as AVIF first and
   WebP second. It is a wrapper and nothing else: every dimension above stays on the
   `<img>`, because that is what the 115% overspill and the -7.5% pull are measured
   against, and moving any of it up here would change what the percentages resolve from.
   `display: block` is the whole rule. `<picture>` is inline by default, and an inline box
   holding a block-level child gets split around it, which can leave an empty line box
   carrying the parent's line-height. Measured in this pane at 1440, 820 and 390 the
   wrapper cost 0px with or without this line, but `.hero` is `overflow: hidden` with only
   2px of clearance under the artwork at 1440 (see the padding note above), so a stray line
   box in some other engine would clip the laptop rather than merely shift it. One
   declaration is cheap insurance against that. */
.hero-mock-pic {
  display: block;
}
.phone-frame {
  border-radius: 34px;
  border: 8px solid #1a1a1a;
  box-shadow: var(--shadow);
  overflow: hidden;
  max-width: 300px;
}

/* -------------------------------------------------------------------- bands */
/* Owner decision 2026-08-10: tinted section bands now match the page surface,
   so light sections no longer stripe. Deliberately changed HERE rather than by
   retinting the --band token, because --band is also nine components' fill
   (note cards, table headers, proof quotes, meter tracks, avatars, glyph
   tiles, the plans scrollbar track, and one stripe of the .ph placeholder
   hatch). Moving the token would erase all of those against the page: the
   placeholder hatch in particular is --band against --surface, so equal values
   render it as flat nothing. .band--dark and .band--coral are separate classes
   and unaffected. The class is kept, now a no-op on light sections, so the
   generator's markup does not change and the stripe can be restored in one
   line. */
.band { background: var(--surface); }
/* The home flow's section clips the phones' glows (see .flow__phone::before). Across only
   since 2026-11-02 (it was `overflow: hidden`), so a phone enlarged by a click
   (.flow__step.is-zoomed) can rise past the section's top edge instead of losing its top. The
   glows never reach it. */
.band--flow { overflow-x: clip; }
.band--dark { background: var(--dark); color: #cfc9c4; }
.band--dark h2, .band--dark h3 { color: #fff; }
/* The coral CTA band carries 65px MORE air than the site's section rhythm, top and bottom
   (owner, 2026-09-15; 80 until 2026-09-20, when the owner took 15px off every "Get your time
   back." section), so it reads as a closing statement rather than another row. Expressed as
   the token plus 65 rather than a flat 95px, so it still tracks --section-pad if the site's
   rhythm is retuned. Class-level, so it also outranks the 850px query's `section` padding and
   the band keeps its air on a phone. */
.band--coral {
  background: var(--coral);
  color: #fff;
  text-align: center;
  padding: calc(var(--section-pad) + 65px) 0;
}
.band--coral h2 { color: #fff; }
/* The closing CTA band's heading, on every page (ctaBand() in site-template.mjs). 60px, as it
   was inline until 2026-09-30; a class so the phone can take it down. */
.cta-band__title { font-size: 60px; }
.band--coral .btn--primary { background: #fff; color: var(--coral); }
.band--coral .btn--primary:hover { background: #f3e9e6; color: var(--coral-hover); }
/* text-align above centres the heading and the copy, but .btn-row is a flex
   container so it never reaches the buttons: they packed to the left edge of
   the band's content box, which reads as an off-centre pair.
   Every coral band carried style="justify-content:center" inline to cover
   this, and the blog's band (added 2026-08-12) did not, so /blog shipped
   uncentred. Owner-reported 2026-08-14. One rule now covers every use of the
   section and the inline copies are gone. */
.band--coral .btn-row { justify-content: center; }

/* Platform logo ticker (owner, 2026-08-09): continuous marquee of the
   supported platforms below the /platforms hero, and since 2026-08-15 in the
   homepage's inverted platforms section too. Both bands carry `inverted` as of
   2026-08-23, so the component reads the same on both pages: charcoal ground,
   --ink flipped to #f1f1f1, wordmarks at 15:1 against it. The component itself
   is theme-agnostic and needed no change; the band class does all of it. The
   track holds TWO copies
   of the wordmark set; each set carries the inter-item gap as padding-right,
   so translating the track exactly -50% loops seamlessly. Swap the <span>s
   for <img> wordmarks later without touching the mechanics. */
.ticker {
  overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent);
}
.ticker__track { display: flex; width: max-content; animation: ticker-scroll 24s linear infinite; }
/* The home ticker names six platforms since 2026-10-19 (owner: BidWrangler and Wavebid). One
   loop is one set's width, so the same 24s over the longer set would scroll about 42% faster.
   34s keeps the four-name speed: 989px of set in 24s before, 1401px in 34s now, ~41px a second
   (measured at desktop; the ratio holds at every width, the names being rem-sized). */
.ticker--six .ticker__track { animation-duration: 34s; }
.ticker__set { display: flex; align-items: center; gap: 74px; padding-right: 74px; }
.ticker__set span {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: 1.7rem;
  color: var(--ink);
  white-space: nowrap;
}
@keyframes ticker-scroll {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}
@media (prefers-reduced-motion: reduce) {
  .ticker__track { animation: none; }
}

.platform-strip { text-align: center; padding: 40px 0; }
.platform-strip__names {
  font-family: var(--font-head);
  font-weight: 600;
  font-size: 1.15rem;
  color: var(--ink);
  display: flex;
  gap: 12px 34px;
  justify-content: center;
  flex-wrap: wrap;
}
.platform-strip__names .lesser { color: var(--muted); font-weight: 500; }

/* Close 60px of the gap below the platform strip (owner, 2026-08-15). Removing the "real
   costs" section left the strip stacked directly on the Five Steps band, and their padding
   summed to 134px of dead space between the strip's last line and the eyebrow (18px
   paragraph margin + 44px + 72px). Taking it from the following band's top padding brings
   that to 74px.
   Scoped to the adjacency, so a .band anywhere else keeps its normal 72px. Two neighbouring
   sections can afford this because .band resolves to --surface, the same colour as the page
   ground; if a contrasting band background is ever introduced, revisit this number. */
.platform-strip + .band { padding-top: 12px; }

/* The home page used to tighten one run of three sections here, with rules sibling-chained off
   #how-it-works, because 60px was a per-run exception. The base rule is 60px site-wide as of
   2026-08-23, so the chain said nothing the cascade did not already say and is gone. Its own
   comment warned that inserting a section into that run would land the rules on the wrong
   element; that fragility went with it. */

/* -------------------------------------------------------------------- cards */
.cards { display: grid; gap: 22px; }
.cards--2 { grid-template-columns: repeat(2, 1fr); }
.cards--3 { grid-template-columns: repeat(3, 1fr); }
.cards--4 { grid-template-columns: repeat(4, 1fr); }

/* /contact is ONE card holding two invisible columns (owner, 2026-09-21): the form on the left,
   the ways to reach us on the right. It shipped as two separate .card boxes until then. The
   columns have no background of their own - the card is the only visible box.
   A 1px RULE DIVIDES THEM (owner, 2026-10-19: "Place a vertical rule divider between the two
   columns"): .contact-split__rule, its own 1px grid column in the middle, so it sits exactly
   centered and runs the height of the taller column. The two gaps either side of it are 47px
   (owner, 2026-10-27: "Add 20px of space to the left and right of the vertical separator
   line", then "5px more"); they were 22px, which kept the 44px between the columns (.cards two-card gap doubled,
   because these columns share one padding box instead of having a border and 26px of padding
   each). Only the COLUMN gap grew: the row gap stays 22px, so the stacked layout below, where
   the rule is a horizontal line, keeps its spacing. It collapses to one column
   at 850 with the other two-column grids (line ~3394), NOT at a breakpoint of its own: there are
   exactly five width queries on this site and check:breakpoints fails a sixth. Stacked, the
   rule is a row of its own, 1px tall, so it becomes a horizontal line between the form and the
   ways to reach us. */
.contact-split {
  display: grid;
  grid-template-columns: 1fr 1px 1fr;
  gap: 22px 47px;
}
.contact-split__rule {
  min-height: 1px;
  background: var(--line);
}
.contact-split__col > h2 + p { margin-top: 0; }
/* 36px inside the card, .card's 26px plus 10 (owner, 2026-10-27: "Add 10px of padding to entire
   card/container"), then 6px off the top alone ("Remove 4px of padding from the top only", then
   "2px more"): 30px top, 36px right, bottom and left. At every width, as .card's own padding is. (0,2,0), so it
   beats the .card rule below it in this file without depending on the order. */
.card.contact-split { padding: 30px 36px 36px; }

/* The reach-us rows. A list, because that is what it is, with the marker taken off. The icon is
   flex: none so a long address wraps against the text rather than squeezing the plane. */
.contact-ways {
  list-style: none;
  margin: 0 0 30px;
  padding: 0;
  display: grid;
  gap: 14px;
}
.contact-ways a {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  font-weight: 600;
}
.contact-ways svg { flex: none; }
.card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 26px;
}
.card h3 { margin-top: 0; }

/* The two picture cards at the top of the home page benefits grid, "Capture Photos"
   and "Scan Barcodes", with their white fill and their border taken away (owner,
   2026-09-18, in that order, the second card following the first). They keep `.card`'s
   padding so their pictures stay on the same grid as the copy in the filled cards below.
   The border is made TRANSPARENT rather than removed, which is not fussiness. Everything here
   is `box-sizing: border-box` (line 50), so `border: 0` hands those 2px back to the content
   box: the picture would grow to 483px and start 1px left of and above the text in the card
   next to it. Keeping the border and hiding it holds the geometry exactly and costs nothing.
   `border-radius` is left alone too. With no fill and no visible border it paints nothing, and
   it is what would round the corners again if either ever came back. */
.card--bare {
  background: none;
  border-color: transparent;
}
/* ---------------------------------------------------------- the note card
   The honesty callout, and a deliberate voice device rather than a stray box:
   state the limitation plainly instead of burying it. Four uses, and every one
   of them is a sentence the reader is meant to trust. The heaviest is the Rule 1
   denial on /platforms ("LotJitsu does not sign in to your platform account"),
   which docs/COPY-RULES.md calls the most load-bearing claim on the site; the
   others are the phone-camera limits on /mobile, and the catalog
   preamble on /sections. A fifth use, the "Read this first" note on
   /platforms/proxibid and /platforms/auctionninja, was removed on 2026-08-25
   (owner) along with the most-tested/less-tested qualifier it carried.

   It used to be --band with a dashed border, which was the wrong dress for all
   of them: dashed reads as a draft annotation, and --band over the --surface ground
   is a 3% step, so on a wide screen the box all but dissolves into the page.
   Owner asked for a better treatment, 2026-08-23, and then for it everywhere.

   Deliberately not a new visual language. It reuses .proof-quote's idiom (line
   717), which is what this site already puts beside a sentence that matters: a
   4px coral rule, square on the ruled edge and rounded away from it. What is new
   is the lift. The card ground and the shadow are what separate a note from the
   band behind it; the rule alone could not, because the rule is only 4px of the
   box's edge.

   The coral is a RULE, never a word. #be624b on white is 4.17:1, under the
   4.5:1 AA floor for 18px body copy, so it carries no text; the lead-in sentence
   takes --ink instead, at 17.3:1. Every value here is a token, so .inverted
   restyles it for free and needs no companion rule. */
.card--note {
  background: var(--card);
  border: 1px solid var(--line);
  border-left: 4px solid var(--coral);
  border-radius: 0 var(--radius) var(--radius) 0;
  box-shadow: var(--shadow);
  padding: 24px 28px;
}
.card--note strong { font-family: var(--font-head); color: var(--ink); }

/* -------------------------------------------------------------- pull stat */
.pull-stat { text-align: center; }
.pull-stat__num {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: clamp(3.4rem, 9vw, 6rem);
  color: var(--coral);
  line-height: 1;
}
.pull-stat__calc {
  color: var(--coral);
  font-weight: 700;
  font-size: 26px;
  margin-top: 8px;
}

/* ---------------------------------------------------------------- steps */
.steps { counter-reset: step; display: grid; gap: 20px; }
.step { display: grid; grid-template-columns: 52px 1fr; gap: 18px; align-items: start; }
.step::before {
  counter-increment: step;
  content: counter(step);
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: var(--coral);
  color: #fff;
  font-family: var(--font-head);
  font-weight: 700;
  font-size: 1.15rem;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-top: 2px;
}
.step h3 { margin-bottom: 4px; }
/* Single-line step lists (the platform pages' upload steps): text sits on the circle's
   center line. The default stays top-aligned for the tall h3+paragraph steps on the home
   and platforms-index pages, where a centered circle would float mid-paragraph. */
.steps--centered .step { align-items: center; }
.steps--centered .step::before { margin-top: 0; }

/* ----------------------------------------------------------- flow (home) */
/* REBUILT AGAIN 2026-11-01 to the owner's second Claude Design handoff (the "How it works"
   artboard): each step now leads with a FRAMED PHONE showing that step's app screen
   (`.flow__phone`, images/flow-phone-*.webp, all four from the Drum Sets catalog), the flat
   icons move down into the title row at 56px, and the stopwatch badge (`.flow__time`) and the
   times it carried are retired from the markup and from here. The section clips its own
   overflow (`.band--flow`) because each phone paints a soft brand-coloured glow wider than its
   column. (It had a visible head, `.sec-head--flow`, for part of that day; the owner removed it
   from the home page, and the rule stays for the /sections entry that still shows one.)
   The notes below describe the 2026-09-17 build this grew out of; what still applies is kept
   and what the phones replaced is marked where it changed. */
/* The home page's four-step flow, rebuilt 2026-09-17 to the owner's Claude Design handoff
   ("How It Works Steps"): four columns, each a large flat icon, then a title row of a
   STOPWATCH badge carrying the step's estimated time beside the heading, then the copy. It
   superseded the same day's first version (tinted discs, numbered badges on a rail): the rail
   and the number badge are gone. `.steps`, the CSS-counter list, stays for /features, the
   platform pages and /sections.
   Where this departs from the handoff, deliberately:
   - (Not a departure any more.) The icons ARE the handoff's, as the owner directed after an
     in-house line-art redraw was rejected: its PNGs, converted losslessly to WebP at their
     native 173 to 175px (images/flow-*.webp). The handoff's README calls them soft screenshot
     crops to be swapped for vector originals, so on a 2x screen they are upscaled and a little
     soft; if hi-res or vector originals ever exist, they drop into the same slots.
   - Type is the site's own: --font-head for the heading and the badge where the handoff shows
     DM Sans, the body face for the copy where it shows Helvetica. Its colours map onto the
     tokens almost exactly (#F0EAE8 was --surface until 2026-09-24, when the page moved to
     #EDEBE8 and this section with it; #1E1A19 and #3D3634 are one unit off --ink
     and --body-text), so the tokens are used. The one new literal is the stopwatch's #505050.
   - The badge label is 18px, not the handoff's 17px: the site's floor, and the check that
     enforces it. One pixel, and "30s" still fits the 48px dial with room.
   - The section keeps the site's 1140px container rather than the handoff's 1264px canvas, so
     it lines up with every other section. The handoff's section padding (48 top, 56 bottom)
     is met by a 3px top MARGIN (18 until the owner asked for 10px less between the hero and
     these steps on 2026-09-17, then 8, then 3 when they asked for 5px more off the same run
     on 2026-09-19; the section's own 30px top padding went to 0 between those two, so this
     margin is now the whole of the list's contribution) and a 26px bottom PADDING:
     padding, not margin, on the bottom because the site zeroes the bottom margin of a
     section container's last child (line 123), which silently ate the margin version.
   The list is still an <ol>, so the step order reaches assistive tech from the markup even
   though nothing visible numbers it any more. */
.flow { list-style: none; margin: 3px 0 0; padding: 0 0 26px; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); column-gap: 10px; }
/* Each step is one of the site's cards (owner, 2026-09-17): the same white `--card` fill,
   1px `--line` border, 14px `--radius` and 26px padding as `.card` itself, so it reads as the
   component this site already has rather than a new box. `.card` is not reused as a class
   because these need the flex column and centred icon.
   The card's padding REPLACED the handoff's `0 20px` cell padding, and with it the earlier
   edge-flush rules (first column no left padding, last no right): with a card, the outer edge
   of the CARD is what lines up with the container and the header, and the copy is inset by the
   card's own padding. That is the normal card relationship and it holds at every width, so the
   two media blocks no longer restate which columns are on an edge.
   Grid items stretch by default, so every card in a row shares a height without being told to.
   `.card`'s 26px is kept top and bottom but the sides run 20px, matching the cell padding the
   card replaced. At 26 a column's content box lost 14px against the old cell and "Review
   Content" wrapped to two lines where all four headings had been one (measured at 1440); 20
   restores the width the heading was fitted to. */
.flow__step {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 26px 20px;
  /* No fill and no border (owner, 2026-09-17, walked back in that order: white card, then
     #F7F3F1, then #FAF8F7, then the fill off, then the border). So nothing of the card renders
     any more and each step is just a padded cell on the section's own ground; the padding and
     the equal-height row are all that survive of it. `border-radius` went with the border,
     having nothing left to round. To bring a card back, this rule needs a fill or a border
     again, and a fill wants to clear `--surface` by more than the 1.19:1 white managed. */
}
/* THE PHONE (2026-11-01), where the 158px icon box was. A 250px-wide box (the 2026-09-17
   geometry notes that sized the icon box, and the --flow-icon/--flow-pull pair the divider
   read, went with it): the image fills the box and sizes its own height from its 600x1231
   intrinsic ratio, so there is no px height to keep in step with anything. It sits BETWEEN the
   title row and the description (owner, 2026-11-01, later the same day: "Move the icon +
   headings to above the phones"; it opened above the title row): 24px under the row and 24px
   over the copy. The phone's own frame is the full picture, with no transparent padding to pull
   anything into, so the 2026-09-17 pull is gone too. Centred by the step's `align-items: center`.
   226px, not the 250 the handoff draws at its 1264px canvas: at the 1140 container the four
   tracks are 266px and the two INNER columns pad 20px on both sides, so their content box is
   226 (measured 2026-11-01), and a cap every column can reach keeps all four phones one width
   and their title rows on one line across the row. At 250 the outer two, with an outer edge
   unpadded, rendered wider and taller than the inner two and the titles stepped; at 236, the
   same by 10px. On the 1000px and 850px blocks, where the columns are wider, the cap rises.
   The glow behind it is the handoff's: a 520px disc of one brand colour at about a fifth of
   its strength, fading to nothing at its edge, drawn by ::before under the picture (the
   picture is `position: relative` to sit above it). The colour comes in through --flow-glow,
   set per step below so the row reads coral, teal, sky, coral as the handoff does; the disc is
   wider than the column on purpose and `.band--flow` clips it at the section's edge, which at
   any width wider than the container is off the page, and on a 390px phone is at the viewport
   edge where the glow is already near zero. Tokens, not literals, for the three colours:
   --coral, --teal and --sky at the handoff's alphas. */
.flow__phone { position: relative; width: 100%; max-width: var(--flow-phone-max); margin: 0 0 24px; }
.flow__phone::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 46%;
  width: 520px;
  height: 520px;
  margin: -260px 0 0 -260px;
  border-radius: 50%;
  background: radial-gradient(closest-side, var(--flow-glow), rgba(255, 255, 255, 0));
  pointer-events: none;
}
.flow__step:nth-child(4n + 1) { --flow-glow: rgba(190, 98, 75, 0.22); }
.flow__step:nth-child(4n + 2) { --flow-glow: rgba(72, 172, 156, 0.24); }
.flow__step:nth-child(4n + 3) { --flow-glow: rgba(105, 165, 203, 0.26); }
.flow__step:nth-child(4n + 4) { --flow-glow: rgba(190, 98, 75, 0.22); }
/* Two drop shadows, the handoff's: a long soft one that sets the phone off the ground and a
   short tight one at its base. `filter` rather than `box-shadow` so the shadow follows the
   frame's rounded corners and the transparent pixels around it rather than the picture's
   rectangle. */
.flow__phone img {
  position: relative;
  display: block;
  width: 100%;
  height: auto;
  filter: drop-shadow(0 26px 38px rgba(20, 16, 12, 0.3)) drop-shadow(0 6px 10px rgba(20, 16, 12, 0.12));
}
/* The hairline between the steps (#c2c2c2, `.flow__step + .flow__step::before`, 2026-09-17 to
   2026-11-02) is gone (owner, 2026-11-02: "Remove the vertical lines between the phones"). The
   arrows below still sit where it ran, in the middle of the 10px gutter. */
/* THE OUTER COLUMNS LOSE THEIR OUTER PADDING (owner, 2026-09-17), so the first step's text
   starts on the container's left edge and the fourth's ends on its right, level with the
   header, the hero copy and every other section. The 20px above is what sits BETWEEN columns;
   at the two ends it was pushing the row inside the page's own margins by that much.
   It is the CONTENT that has to line up, not the step box: each box already spans the full
   grid track, so only the padding was ever wrong.
   Restated per breakpoint because "outer" changes meaning as the grid reflows: four across it
   is children 1 and 4, two across it is the odd ones on the left and the even ones on the
   right, and one across every step is both. The 850px block comes after the 1000px one in this
   file and matches its specificity, which is what lets it win. */
.flow__step:first-child { padding-left: 0; }
.flow__step:last-child { padding-right: 0; }
/* A grey chevron between each pair of phones (owner, 2026-11-02, to a screenshot), centred in
   the gutter where the hairline divider ran and level with the middle of the phone. The step is a size container so
   the arrow can find that middle: the phone is min(--flow-phone-max, the column's content width)
   wide and 1231/600 of that tall, and it starts under the 26px top padding, the 48px title row
   and its 16px margin. Change any of those and this offset moves with them only if it is
   changed here too. ::after because ::before was the divider (removed 2026-11-02). #A5A4A2 (owner, 2026-11-02, after #9B9A93), a
   literal: nothing else on the site uses it. */
.flow { --flow-phone-max: 226px; }
.flow__step { container-type: inline-size; }
/* EVENLY BETWEEN THE PHONES (owner, 2026-11-04: "they need to be evenly centered between each of
   the phones in all themes"). The arrow used to sit at the middle of the 10px gutter, but the
   phones are not evenly spaced around the gutters: each phone is centred in its own step's
   content box, and the outer steps drop their outer padding, so the space between two phones
   differs from one pair to the next. So the arrow now finds the midpoint between the
   right edge of the previous phone and the left edge of this one, both measured from this step's
   left edge. The tracks are all one width, so the previous step's content width follows from
   this one's (100cqw) and the four paddings below; the 10px is `.flow`'s column-gap and the 17px
   is half the arrow's 34px width. Change the step padding, the gap or the arrow width and the
   --arrow-* values or these literals must change with them. */
.flow__step + .flow__step::after {
  --arrow-pl: 20px;   /* this step's padding-left */
  --arrow-pr: 20px;   /* this step's padding-right */
  --arrow-ppl: 20px;  /* the previous step's padding-left */
  --arrow-ppr: 20px;  /* the previous step's padding-right */
  --arrow-prev-c: calc(100cqw + var(--arrow-pl) + var(--arrow-pr) - var(--arrow-ppl) - var(--arrow-ppr));
  content: "";
  position: absolute;
  left: calc(
    (
      var(--arrow-pl) + (100cqw - min(var(--flow-phone-max), 100cqw)) / 2
      - 10px - var(--arrow-ppr)
      - (var(--arrow-prev-c) - min(var(--flow-phone-max), var(--arrow-prev-c))) / 2
    ) / 2 - 17px
  );
  top: calc(26px + 48px + 16px + min(var(--flow-phone-max), 100cqw) * 1231 / 1200);
  width: 34px;
  height: 84px;
  margin-top: -42px;
  background: #a5a4a2;
  clip-path: polygon(0 0, 48% 0, 100% 50%, 48% 100%, 0 100%, 52% 50%);
  pointer-events: none;
}
/* Four across: step 2 follows the first step (no left padding), and the last step has no right
   padding. */
.flow__step:nth-child(2)::after { --arrow-ppl: 0px; }
.flow__step:last-child::after { --arrow-pr: 0px; }
/* The flat icon, in the title row since 2026-11-01 (owner: "small versions of the 4 original
   icons"), where the stopwatch badge was, and at the badge's 48px: the headings were fitted to
   one line beside a 48px dial plus the gap (12px then, 6 now), and "Review Content" wrapped beside anything
   wider. The vectors scale cleanly; `object-fit` keeps the one or two that are not quite
   square inside the box without stretching. The markup's width/height attributes say 56 (the
   handoff's size, which the 850px block restores beside the 34px titles there); this rule is
   what paints. */
.flow__head img { flex: none; width: 48px; height: 48px; object-fit: contain; }
/* The PHONE grows 3% while the cursor is anywhere in its step (2026-11-01; it was the icon
   growing 5%, owner, 2026-09-19), and settles back when it leaves. The trigger is the whole `.flow__step` cell, not the picture, so the
   copy and the empty space under it count as hovering too.
   It scales the PICTURE rather than the step, because `[data-reveal-children]` puts `will-reveal`
   on each step and animates its `transform` on entry: one element, two rules, and whichever
   ran last would win. The picture is untouched by the reveal, so the two cannot collide.
   A transform, not width/height, so nothing reflows: the box keeps its size and the text
   below it does not move. The glow (the box's ::before) stays put under the lifted picture. 0.3s is the site's hover tier, the one .card--tool and .tier use,
   and it is left ungated for reduced motion exactly as `.logo-grid .ph:hover`'s own scale(1.05)
   is: a brief scale the visitor asks for by pointing at it. */
.flow__phone img { transition: transform 0.3s ease; }
.flow__step:hover .flow__phone img { transform: scale(1.03); }
/* CLICK TO ENLARGE (owner, 2026-11-02): a click on a phone grows it 37% (50%, 35%, 40% and 38% earlier that day), OVER its neighbours
   rather than pushing them aside, and a second click puts it back (site.js toggles
   `.is-zoomed` on the step). A transform, so nothing reflows. The step comes up a layer so the
   picture paints over the other phones and the arrows. It wins over the hover lift because it
   comes after it at the same specificity, so hovering an enlarged phone keeps it at 137%.
   The layer is 52, over the sticky header's 50, so an enlarged phone covers the header too
   (owner, 2026-11-02); the consent bar (90) and the drawer stay over it. Nothing between the
   step and the page makes a stacking context, which is what lets 51 reach the header. The
   phone takes its own layer inside the step so the step's own arrow (its ::after, which paints
   after the phone) goes under it. `.is-unzooming` holds the layers while a phone shrinks back,
   so it does not drop behind its neighbours halfway down, one under the phone growing in its
   place at the same time (52), which is the one that ends up in front.
   The two end phones grow inward from their outer edge, so neither reaches past the page's
   margin where the band clips across. Off in the 850px block, where a stacked phone has no
   neighbour to cover and 137% would run off the screen. */
.flow__phone { cursor: zoom-in; }
.flow__step.is-unzooming { z-index: 51; }
.flow__step.is-zoomed { z-index: 52; }
.flow__step.is-zoomed .flow__phone,
.flow__step.is-unzooming .flow__phone { z-index: 1; }
.flow__step.is-zoomed .flow__phone { cursor: zoom-out; }
.flow__step.is-zoomed .flow__phone img { transform: scale(1.37); }
.flow__step:first-child .flow__phone img { transform-origin: 0 50%; }
.flow__step:last-child .flow__phone img { transform-origin: 100% 50%; }
.flow__body { width: 100%; }
/* The title row, first in the step since 2026-11-01 (it was under the phone, in `.flow__body`,
   for part of that day): icon and heading centred in the column at every width (owner,
   2026-11-01; until then the row took the full width to hold them on the column's left edge,
   centred only in the 850px block). 16px down to the phone at every width (owner, 2026-11-01:
   8px less than the 24 it had). 6px between the icon and the heading at every width (owner,
   2026-11-01: the 12 it had, less 4, then 2 more).
   4px left of true centre (owner, 2026-11-01: "shift each of them to the left by 4px"): the
   icons carry more transparent margin on their left than their right, so a box-centred row
   read 3.4 to 5.6px right of the phone below it (measured on the pixels at 1440). A transform,
   so nothing reflows. */
.flow__head { display: flex; align-items: center; justify-content: center; gap: 6px; width: 100%; margin-bottom: 16px; transform: translateX(-4px); }
/* The stopwatch badge (`.flow__time`, 2026-09-17 to 2026-11-01: a 48px #505050 dial drawn in
   plain CSS, its crown and side button pseudo-elements, the step's time at the 18px floor) is
   gone with the times it carried. Nothing emits the class any more, the home page or the
   /sections catalog, so no rule remains. */
/* #232323 rather than --ink (owner, 2026-09-17). A flat neutral, where --ink (#1d1a18) is
   warm and a shade darker; the same value the account menu's rows took, so the two
   owner-picked greys agree. A literal, not a token: nothing else on the page uses it yet. */
.flow__head h3 { margin: 0; font-size: 22px; font-weight: 700; line-height: 1.25; letter-spacing: -0.015em; color: #232323; }
/* No top margin since 2026-11-01: the phone above it carries the 24px. */
.flow__body p { margin: 0; font-size: 1.125rem; line-height: 1.4444; color: var(--body-text); text-wrap: pretty; }

/* ------------------------------------------------------------ gallery */
.gallery { display: grid; grid-template-columns: repeat(3, 1fr); gap: 26px; }
/* Multi-line captions read left-aligned (owner, 2026-08-09); the single-line
   hero caption keeps the base rule's centering. The figure.shot-fig prefix is
   for specificity: it must outrank the centering base rule below the hero. */
.gallery figure.shot-fig figcaption { text-align: left; }
.gallery figcaption h3 { margin-bottom: 4px; }

/* ---------------------------------------------------------------- FAQ */
/* ONE card look, TWO variants, and which one you get is decided by the element.

   STATIC - `<div class="faq-item">` (owner, 2026-09-15). Question and answer both always shown.
   The four platform pages and the /sections catalog render this. The rules in THIS block are
   the shared card plus that variant's question line; nothing here is interactive, so there is
   no cursor, no list-style reset and no marker to hide.

   EXPANDING - `<details class="faq-item" name="faq">` (owner, 2026-09-20). The accordion this
   file carried until 2026-09-15, restored value for value from c223b5a^ for the FAQ at the
   bottom of /support. Everything scoped to <details> is in the block immediately below. */
.faq-item {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--card);
  margin-bottom: 12px;
  padding: 18px 22px;
}
.faq-item__q {
  font-family: var(--font-head);
  font-weight: 600;
  color: var(--ink);
  margin: 0 0 12px;
}
.faq-item p:last-child { margin: 0; }

/* The EXPANDING variant (owner, 2026-09-20). Question first, click to open the answer, one card
   open at a time.

   `details.faq-item` is an element+class selector, (0,1,1), so it outranks the static .faq-item
   rules above, (0,1,0), whatever the source order: no !important, nothing to reorder. It also
   matches NOTHING the platform pages or the /sections catalog render, because those emit
   `<div class="faq-item">`, which is not a <details>. Both variants share one card look and
   neither can reach the other. Deliberately a type selector rather than a modifier class: a
   class can be pasted onto a <div> by mistake, and `details` cannot be wrong about what it is
   styling.

   ONE PRECEDENCE ASYMMETRY, recorded because nothing enforces it: `.faq-item p:last-child`
   above is (0,2,1) and outranks `details.faq-item p` below, (0,1,2), for `margin`. Both say
   `margin: 0` today, so nothing differs - but change that one and the expanding card follows it
   silently.

   NO TRANSITION AND NO ANIMATION, so prefers-reduced-motion genuinely does not apply here: a
   <details> opens instantly. Adding ::details-content or interpolate-size height animation would
   make it apply, and would need the reduce query the rest of this file uses. */
/* The FAQ is the first thing on this site anyone reaches by a CROSS-PAGE fragment: before
   2026-09-20 no page linked `/somewhere#anchor` at all, and the closing band and the footer now
   put `/support#faq` on all 32 pages that carry them. .site-header is `position: sticky; top: 0`
   and 82px tall at every width (74px until 2026-10-27), and a fragment scrolls its target to y=0, so without this the
   visitor who clicks "Read FAQ" lands with the whole "Frequently Asked Questions" heading behind
   the header and meets an unexplained list of cards. 98px is the header plus 16px of air.

   Scoped to this anchor rather than set as `scroll-padding-top` on `html`, which would fix it
   everywhere at once: the site's same-page anchors (the home page's section links, a help
   article's "On this page" rail) land under the same header and have done since the header
   became sticky, but changing where every one of them lands is not this change's to make. */
#faq { scroll-margin-top: 98px; }

details.faq-item { padding: 0 22px; }
details.faq-item summary {
  cursor: pointer;
  list-style: none;
  font-family: var(--font-head);
  font-weight: 600;
  color: var(--ink);
  padding: 18px 30px 18px 0;
  position: relative;
}
/* Safari before 16.4 ignores list-style on a summary and draws its own triangle regardless. */
details.faq-item summary::-webkit-details-marker { display: none; }
/* Generated content, so it is decorative by construction: a ::after is not exposed as content a
   screen reader announces, it cannot take aria-hidden, and the open/closed state is already
   carried by the <details> element itself. There is nothing to add for assistive tech. */
details.faq-item summary::after {
  content: "+";
  position: absolute;
  right: 2px;
  top: 50%;
  transform: translateY(-50%);
  font-size: 1.4rem;
  color: var(--coral);
}
details.faq-item[open] summary::after { content: "−"; }
details.faq-item p { padding-bottom: 18px; margin: 0; }

/* --------------------------------------------------- platform wordmarks */

/* --------------------------------------------------------------- tables */
.table-wrap { overflow-x: auto; }
table.plain {
  width: 100%;
  border-collapse: collapse;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}
table.plain th, table.plain td {
  padding: 14px 16px;
  border-bottom: 1px solid var(--line);
  text-align: left;
  vertical-align: top;
}
table.plain thead th {
  font-family: var(--font-head);
  color: var(--ink);
  background: var(--band);
  font-size: 1.125rem;
}
table.plain tbody tr:nth-child(even) { background: #fdfbfa; }
table.plain tbody tr:last-child td { border-bottom: 0; }

/* ---------------------------------------------------------------- plans */
/* Used by /sections only since the plans page was removed (2026-08-12); plans live in the
   app. Matches the app's /account/plans belt grid (BillingV2Section + BeltKnot):
 * 6px belt strip, name + tagline with the masked belt-knot graphic top-right,
 * 32px price, hr, bold-count feature list with green checks, full-width
 * belt-colored pill. Light-theme values from the app's light mode. */
.plans {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: min(calc(88% - 8px), 265px);
  justify-content: safe center;
  gap: 16px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-padding-inline: 4px;
  /* The pricing page's plans strip sits directly in the section (no .container wrapper),
     so the container's metrics live here instead. 30px each side, owner-set 2026-08-12, up
     from the container's 24px: at desktop the strip runs the full 1440px and the outer cards
     would otherwise sit almost against the viewport edge. It is padding rather than margin on
     purpose, because the strip scrolls: padding stays inside the scroll port, so the first and
     last card keep their gutter when the row is swiped, which a margin would not do. */
  max-width: 1140px;
  margin: 0 auto;
  padding: 4px 30px 14px;
  -webkit-overflow-scrolling: touch;
}
.plans::-webkit-scrollbar { height: 8px; }
.plans::-webkit-scrollbar-track { background: var(--band); border-radius: 999px; }
.plans::-webkit-scrollbar-thumb { background: #cfc5bb; border-radius: 999px; }
.plan { scroll-snap-align: start; }
@media (min-width: 560px) {
  .plans { grid-auto-columns: min((100% - 16px) / 2 - 10px, 265px); }
}
@media (min-width: 800px) {
  .plans { grid-auto-columns: min((100% - 2 * 16px) / 3 - 10px, 265px); }
}
@media (min-width: 1100px) {
  /* Five columns, and the strip runs WIDER than the site's 1140px container (owner,
     2026-08-12). /pricing ships FIVE plans (white, yellow, green, blue, black) and the rule
     here used to size for four, so the fifth sat outside the container and the strip became a
     horizontal scroller at desktop.
     Fitting all five inside 1140px was the obvious fix and it looked terrible: it forces each
     card to 206px against the ~265px these were designed at, and the feature lines wrap three
     deep. So the strip gets its own wider measure instead. It already owns its metrics rather
     than sitting in a .container (see the note on the base rule), which is what makes this
     legitimate rather than a rhythm break.
     1440px is chosen so the cards land back on their design width at a 1440 desktop: content
     is 1392 after padding, and (1392 - 4 gaps) / 5 is 265.6px. Wider screens cap there;
     narrower ones fill 100% and the cards shrink gracefully. */
  .plans {
    max-width: 1440px;
    grid-auto-columns: calc((100% - 4 * 16px) / 5);
  }
}
.plan {
  position: relative;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border-radius: 16px;
  border: 1px solid #e5e5e5;
  background: #f9f8f6;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
  text-align: left;
}
.plan__strip { height: 6px; width: 100%; flex-shrink: 0; }
.plan--white .plan__strip { background: #d9d9d9; }
.plan--yellow .plan__strip { background: #f0b429; }
.plan--green .plan__strip { background: #16a34a; }
.plan--blue .plan__strip { background: #2563eb; }
.plan--black .plan__strip { background: #111827; }
.plan__body { display: flex; flex-direction: column; gap: 12px; padding: 16px; flex: 1; }
.plan__head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
/* h2, not h3 (2026-08-11): each plan card is a top-level section of /pricing and these sat
   directly under the page h1, so h3 skipped a level. Size is unchanged - the level is a
   document-structure fact, the 20px is the design one. */
.plan__head h2 { font-size: 20px; font-weight: 700; line-height: 28px; margin: 0; color: #171717; }
.plan__tag { color: #737373; margin: 0; font-size: 18px; line-height: 1.5; }
.plan__price { color: #171717; margin: 0; }
.plan__price strong { font-size: 32px; font-weight: 700; line-height: 36px; }
.plan__price span { font-size: 18px; }
.plan hr { border: 0; border-top: 1px solid #e5e5e5; margin: 0; width: 100%; }
/* 16px for the tier feature list only (not the tag, price, or button) is an owner-approved
   exception to the 18px floor (2026-08-10), matching the app's own 16px body floor that
   these cards mirror. */
.plan__facts { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 8px; color: #404040; font-size: 16px; line-height: 1.5; }
.plan__facts .check { display: flex; gap: 8px; align-items: flex-start; }
.plan__facts .check::before { content: "\2713"; color: #16a34a; font-weight: 700; flex-shrink: 0; }
.plan__cta { margin-top: auto; padding-top: 4px; }
.plan__pill {
  display: block;
  width: 100%;
  text-align: center;
  border-radius: 999px;
  padding: 14px 16px;
  font-family: var(--font-head);
  font-size: 18px;
  font-weight: 600;
  border: 1px solid transparent;
  transition: background 0.15s;
  white-space: nowrap;
}
.plan--white .plan__pill { background: #f3f4f6; color: #111827; border-color: #d4d4d4; }
.plan--white .plan__pill:hover { background: #fff; color: #111827; }
.plan--yellow .plan__pill { background: #f0b429; color: #111827; }
.plan--yellow .plan__pill:hover { background: #f6c14b; color: #111827; }
.plan--green .plan__pill { background: #16a34a; color: #fff; }
.plan--green .plan__pill:hover { background: #1cb455; color: #fff; }
.plan--blue .plan__pill { background: #2563eb; color: #fff; }
.plan--blue .plan__pill:hover { background: #3572f2; color: #fff; }
.plan--black .plan__pill { background: #111827; color: #fff; }
.plan--black .plan__pill:hover { background: #1f2937; color: #fff; }

/* The belt-knot graphic, ported from the app's BeltKnot component: a knot-shaped
 * SVG mask over pinstripe + shade gradients in the belt color, with an offset
 * shadow and a top sheen. */
:root {
  --knot-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 76'><path d='M4 12c18 3 34 8 48 12 5 2 11 2 16 0 14-4 30-9 48-12l-3 16c-17 1-31 3-44 7-6 3-12 3-18 0-13-4-27-6-44-7z'/><path d='M44 22c7-3 25-3 32 0l10 11-10 13c-9 3-23 3-32 0L34 33z'/><path d='M48 43L28 72l-13-3 21-30z'/><path d='M72 43l20 29 13-3-21-30z'/></svg>") center / contain no-repeat;
}
.belt-knot {
  position: relative;
  display: inline-block;
  width: 62px;
  height: 38px;
  flex: 0 0 auto;
  filter: drop-shadow(0 1px 1px rgba(15, 23, 42, 0.22)) drop-shadow(0 0 1px rgba(255, 255, 255, 0.28));
}
.belt-knot i { position: absolute; inset: 0; -webkit-mask: var(--knot-mask); mask: var(--knot-mask); }
.belt-knot .k1 { background: rgba(15, 23, 42, 0.14); transform: translate(1px, 1px); }
.belt-knot .k2 {
  background:
    repeating-linear-gradient(90deg,
      color-mix(in srgb, var(--belt) 84%, white) 0 2px,
      color-mix(in srgb, var(--belt) 96%, black) 2px 4px,
      color-mix(in srgb, var(--belt) 86%, white) 4px 6px),
    linear-gradient(180deg,
      color-mix(in srgb, var(--belt) 88%, white) 0%,
      var(--belt) 45%,
      color-mix(in srgb, var(--belt) 82%, black) 100%);
}
.belt-knot .k3 { background: linear-gradient(180deg, rgba(255, 255, 255, 0.34), rgba(255, 255, 255, 0.05) 44%, rgba(255, 255, 255, 0) 68%); }
.plan--white .belt-knot { --belt: #e2e8f0; }
.plan--yellow .belt-knot { --belt: #eab308; }
.plan--green .belt-knot { --belt: #16a34a; }
.plan--blue .belt-knot { --belt: #2563eb; }
.plan--black .belt-knot { --belt: #111827; }

/* ----------------------------------------------------------- calculator */
.calc { background: var(--card); border: 1px solid var(--line); border-radius: var(--radius-lg); padding: 30px; }
/* The teal title bar across the top of the home page calculator (owner, 2026-09-18). It is
   an element INSIDE the 30px-padded panel pulled out to the panel's edges by negative
   margins, so a `.calc` without one (the /sections specimen) is untouched. The -1px takes it
   over the panel's border as well, and the panel's radius less that pixel rounds its top
   corners to match. #404040 (owner, 2026-09-19; it was #666, then #50988a, then #d15e41),
   a shade darker than the Before row's #454545. White on it is 10.37:1, so unlike its two predecessors it passes
   AA at any size and weight. */
/* Typography only since 2026-09-19. The surface it used to paint for itself (the fill, the
   radius and the -31px margins that bled it to the card's edges) moved to `.calc-dialog__bar`,
   which now holds this heading and the close button on one row. */
.calc__title {
  margin: 0;
  flex: 1 1 auto;
  color: #fff;
  font-size: 1.5rem;
  font-weight: 700;
  line-height: 1.3;
  letter-spacing: 0.02em;
  text-align: center;
}
.calc__grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 18px; margin-bottom: 22px; }
.calc label { font-family: var(--font-head); font-weight: 600; font-size: 1.125rem; color: var(--ink); display: block; margin-bottom: 6px; }
/* The fields are 8px taller and their text 2px larger than the 18px they began at (owner,
   2026-09-19): 20px text adds 2.5px of line box on its own, and the vertical padding, up from
   10px, supplies the other 5.5px, so a field measures 55.5px where it measured 47.5px. */
.calc input[type="number"] {
  width: 100%;
  padding: 12.75px 12px;
  font-size: 1.25rem;
  font-family: var(--font-body);
  border: 2px solid #e3ddd7;
  border-radius: 8px;
  background: var(--field-bg);
}
.calc input:focus { outline: 2px solid var(--coral); outline-offset: 1px; border-color: var(--coral); }
/* A standing "$" inside the rate field (owner, 2026-09-19: "123 should have a '$' before it,
   always", and "($)" came off the label in the same breath).
   It is drawn beside the input rather than put IN it, because the input stays `type="number"`:
   a number input rejects a value like "$125", and site.js reads `.value` and multiplies it. So
   the sign is a sibling span and the field's left padding is opened up to seat it. 14px is
   12px of the input's own padding plus its 2px border, which lands the sign exactly where the
   first digit used to start; the digits then begin at 30px, 4px clear of it.
   aria-hidden, because it is the same fact the label already carries for a screen reader (the
   visually-hidden "in dollars"); announced as well it would read "Hourly labor in dollars,
   dollar, 125". pointer-events: none so a click on the sign still lands in the field. */
.calc__money { position: relative; }
.calc__money-sign {
  position: absolute;
  left: 14px;
  top: 50%;
  transform: translateY(-50%);
  font-size: 1.25rem;
  font-family: var(--font-body);
  /* The digits beside it are the UA's own field text, which is pure black; --body-text (#3d3835)
     read a shade lighter than them and the sign looked like a placeholder rather than part of
     the value. Matched deliberately rather than tokenised: the token to match is the browser's. */
  color: #000;
  pointer-events: none;
}
.calc input[type="number"].calc__money-input { padding-left: 30px; }
.calc__out { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; text-align: center; }
.calc__out div { background: #232323; border-radius: var(--radius); padding: 18px 10px; }
/* 1.45rem is 23.2px: four pixels off the 1.7rem (27.2px) it read until 2026-09-19, at the
   owner's direction. The base rule is what changed, not a `.calc-dialog` override, so the
   /sections specimen keeps showing the component at the size it actually ships. The `<b>`
   inside the Savings tiles inherits it; `.calc__out span`, the sub-label under each figure,
   is a separate rule and is untouched. */
.calc__out strong { font-family: var(--font-head); font-size: 1.45rem; color: #f1f1f1; display: block; }
.calc__out span { font-size: 1.125rem; color: #f1f1f1; }
/* The calculator's three result rows (owner, 2026-09-18): Before LotJitsu, After LotJitsu,
   Savings, each a `.calc__label` heading over a `.calc__out` row. The Savings tiles take the
   #50988a the title bar wore for a few hours before it changed colour. White on it is
   3.39:1, which passes AA only as LARGE text, so the label under the figure is 19.2px bold
   there rather than the 18px regular the dark tiles use: below 18.66px, or lighter than
   bold, it fails. */
.calc__label { font-family: var(--font-head); font-size: 1.5rem; font-weight: 700; color: var(--ink); margin: 22px 0 10px; }
.calc__grid + .calc__label { margin-top: 0; }
/* The Before row is #454545 (owner, 2026-09-18), lighter than the After row's #232323.
   #f1f1f1 on it is 8.49:1. */
.calc__out--before div { background: #454545; }
.calc__out--savings div { background: #50988a; }
.calc__out--savings strong, .calc__out--savings span { color: #fff; }
.calc__out--savings span { font-size: 1.2rem; font-weight: 700; }

/* A tile flashes when its own figure changes (owner, 2026-09-19). site.js compares each tile's
   text before and after a recalculation and marks only the ones that actually moved, so
   retyping "125" over "125", or a change that leaves a rounded figure where it was, lights
   nothing. Nine tiles, three rows, three different fills (#454545, #232323, #50988a), which is
   why the flash is an OVERLAY rather than a background animation: one white wash at 0.32 works
   on all three, where animating `background-color` would need a keyframe per row and
   would break the moment a fill changed. `border-radius: inherit` keeps the wash inside the
   tile's corners, and it never intercepts a click.
   Gated on prefers-reduced-motion, unlike the header's colour fade. That one is a single
   transition a visitor provokes once; this one re-fires on every keystroke, so a fast typist
   would get a sustained shimmer across nine tiles, which is the kind of repeated luminance
   change the preference exists to stop. Under it the figures simply change, which is the real
   feedback anyway. */
/* The wash paints UNDER the figures, not over them. A positioned `::after` with `z-index: auto`
   paints in the positioned-descendant step, which is ABOVE the tile's in-flow text: measured,
   the flash bleached the numbers along with the fill and took the Savings row to 2.19:1 and the
   Before row to 3.57:1 for the full 1600ms, failing WCAG 1.4.3 on a tile whose whole job is to
   be read. `isolation: isolate` gives the tile its own stacking context and `z-index: -1` puts
   the overlay between the tile's background and its text, so the fill tints and the figures stay
   at full strength. Alpha could not have fixed this: the Savings row needs about 0.10 to clear
   3:1, which is no longer a flash. */
.calc__out > div { position: relative; isolation: isolate; }
.calc__out > div::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  background: #fff;
  opacity: 0;
  pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
  .calc__out > div.is-changed::after { animation: calc-tile-flash 1600ms ease-out; }
}
/* Slow, at the owner's direction (2026-09-19); it ran 620ms first and read as a blink.
   The wash HOLDS at full for the first 18%, about 290ms, before it starts to go. A plain fade
   from 0.32 spends most of a long duration almost transparent, so lengthening it alone made the
   flash fainter rather than slower. The hold is what the eye reads as slow; the 1,310ms tail
   after it is what makes the tile settle rather than snap. */
@keyframes calc-tile-flash {
  0%,
  18% {
    opacity: 0.32;
  }
  100% {
    opacity: 0;
  }
}

/* ---- the calculator's dialog, and the card that opens it (owner, 2026-09-19) ----
   The calculator was a section of the home page until today. Everything above this comment is
   unchanged and still draws the calculator itself; what follows only puts it in a modal.

   The dialog is deliberately NOT styled like .signout-dialog. That one owns a dark --modal-*
   palette because its panel is its own to draw; here the panel is `.calc`, which is a white card
   with its own border and radius, so the dialog element is given no surface at all and simply
   frames it. Painting a second surface behind a card that already has one is how you get a
   border inside a border.
   (Ask Sales was the other example here until 2026-09-21, when it was restyled to the Log In
   modal's light frame. The reasoning is unchanged: it now draws its own light panel, this one
   still draws none.)

   880px, not the 440-480px those modals use: the calculator is a 4-column input grid over three
   3-column output rows, and below about 840px `.calc__grid` and `.calc__out` collapse to one
   column (line 3005) and the whole thing becomes roughly 1,800px tall. So the SCROLL lives on
   an inner element, not on the dialog: with `overflow: auto` on the dialog itself the close
   button, which is positioned against it, would scroll up out of reach on exactly the narrow
   screens that need it most. The dialog clips, the inner div scrolls, the X stays put. */
/* A flex COLUMN: a fixed header bar over a scroller that takes whatever height is left. The
   `display` here never shows a closed dialog, because `dialog:not([open])` (line 352) is the
   more specific selector and keeps winning. */
.calc-dialog {
  width: min(calc(100% - 32px), 880px);
  max-height: calc(100% - 32px);
  display: flex;
  flex-direction: column;
  padding: 0;
  border: 0;
  background: none;
  overflow: hidden;
  border-radius: var(--radius-lg);
}
/* 80%, not 70% (owner, 2026-09-22) - see the note on .sales-dialog::backdrop. The 12px blur is
   left alone for the reason given on .signout-dialog::backdrop. */
.calc-dialog::backdrop {
  background: rgba(0, 0, 0, 0.8);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
}
/* The header bar. `position: relative` is what the close button centres against, and the 44px
   side padding is symmetrical so the heading stays optically centred in the bar rather than
   being pushed off-centre by clearing the X on one side only. */
.calc-dialog__bar {
  flex: none;
  position: relative;
  display: flex;
  align-items: center;
  padding: 14px 44px;
  background: #1a1a1a;
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}
/* `min-height: 0` is load-bearing: a flex item's floor is its content, so without it a tall
   calculator refuses to shrink, the scroller never scrolls and the dialog grows past the
   viewport instead. */
.calc-dialog__scroll { flex: 1 1 auto; min-height: 0; overflow: auto; }
/* The card sits UNDER the header bar, so it rounds at the bottom only and drops its top border:
   the bar already draws that edge, and two of them stack into a hairline seam. `.calc`'s own
   30px padding is the panel's padding, as it always was.
   #EDEBE8 HERE AND NOT ON `.calc-dialog` (owner, 2026-09-23: every modal's background and border
   to #E6E2E0; #EDEBE8 since 2026-09-24). The dialog element itself is `background: none;
   border: 0` and always has been - it only frames the card, because painting a second surface
   behind a card that already has one is how you get a border inside a border. So the card IS this modal's background, and it is the
   thing that had to move; repainting the dialog would have changed nothing a visitor can see.
   SCOPED TO THE DIALOG, deliberately. `.calc` is also a plain section on /sections, where it is
   a specimen of the calculator as a page section and keeps --card (#ffffff) on the page's own
   ground. Only the modal was asked about, so only the modal moves.
   The border matches the fill, so the card's edge no longer draws against its own surface. It is
   left in place rather than removed: the bar above still needs something to butt against, and
   `border-top: 0` below is what stops the two stacking into a seam. */
.calc-dialog .calc {
  border-top: 0;
  border-radius: 0 0 var(--radius-lg) var(--radius-lg);
  background: #edebe8;
  border-color: #edebe8;
}
/* The four number fields, #D1CBC8 with every other field in every other modal (owner,
   2026-09-23). SCOPED TO THE DIALOG for the same reason the fill above is: `.calc` is also a
   plain section on /sections, where the base rule's 2px #e3ddd7 still reads correctly against
   that page's white card, and only the modal was asked about.
   It replaces the border COLOUR only - the 2px width is the base rule's and stays, so these
   fields keep reading heavier than the 1px ones in the Log In and Ask Sales modals, as they
   always have.
   THE FOCUS RULE HAD TO BE RESTATED, and this is the whole reason there are two selectors here
   rather than one. `.calc input:focus` (line ~3320) wins over the BASE rule `.calc
   input[type="number"]` only on source order - the two are both (0,2,1). This selector is
   (0,3,1), so it outranks that focus rule outright and would have pinned the border at #D1CBC8
   even while the field was focused, silently deleting the coral focus border on four fields.
   The outline in that rule is untouched either way, so the loss would have been a partial one
   and easy to miss on screen. Restated at matching specificity below instead. */
.calc-dialog .calc input[type="number"] {
  border-color: #d1cbc8;
}
.calc-dialog .calc input[type="number"]:focus {
  border-color: var(--coral);
}
/* A bare X, exactly as .signout-dialog__close is (owner, 2026-09-19: "an actual X, like other
   modals have. Not a square button"): no fill, no border, a 40x40 hit area around the 24px
   glyph, and a background only on hover, which is what that one does. Ask Sales had the third
   such X until 2026-09-21; the restyle to the Log In modal's design removed it, since that
   design has no close button at all - Cancel, Escape and the backdrop are its exits.
   WHITE (owner, 2026-09-19: "No glow, just a white X"), which is also the colour the heading
   beside it is set in. It works because the button lives in the header bar rather than floating
   over the scroller, so #1a1a1a is always what is behind it: white on that is 17.4:1, and the
   hover wash is white at low alpha so it reads on the same bar.
   It briefly carried a fill, a border and a white halo, all of which existed only to survive
   output tiles scrolling underneath. Taking it out of the scroller removed that problem.
   Centred on the bar rather than pinned 14px down (owner, 2026-09-19), which is the one way
   that holds for a heading of one line and of two. */
.calc-dialog__close {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  right: 14px;
  width: 40px;
  height: 40px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: 10px;
  background: transparent;
  color: #fff;
  cursor: pointer;
}
.calc-dialog__close:hover { background: rgba(255, 255, 255, 0.14); }

/* THE TERMS MODAL (owner, 2026-10-06), opened from the agreement line under either provider
   pair. The savings calculator's frame (`.calc-dialog`: the dark bar, the white X, an inner
   scroller) at a reading width, over the Log In dialog. The bar carries only the X - the
   document's own h1 is the title, as it is on /terms - so it takes a height the X fits in.
   The document is styled as /terms styles it: h1 34px, h2 24px (`.prose--legal`), 18px body.
   Identical to the app's `.lj-terms-dialog`; D-060 binds the pair. */
.terms-dialog {
  width: min(calc(100% - 32px), 760px);
  /* THE LOG IN DIALOG'S FRAME (owner, 2026-10-12, in place of a #eee shadow: "a thick translucent
     border around this modal - exactly like we have on this modal"): the same 10px of #EDEBE8 at
     .85, and corners 10px wider so they stay concentric with the bar and page inside, which this
     dialog's own overflow clip rounds to --radius-lg. The dialog paints no background, so the
     underlay shows through the frame as it does on Log In. */
  border: 10px solid rgba(237, 235, 232, 0.85);
  border-radius: calc(var(--radius-lg) + 10px);
}
/* Its underlay is 40%, not the frame's 80% (owner, 2026-10-12): it opens over the Log In dialog,
   whose own 80% underlay is already there. Same selector weight as .calc-dialog::backdrop and
   later in the file, so it wins; the blur is kept. */
.terms-dialog::backdrop { background: rgba(0, 0, 0, 0.4); }
.terms-dialog__bar { min-height: 56px; }
.terms-dialog__doc {
  padding: 30px;
  background: #edebe8;
  color: var(--ink);
  font-size: 18px;
  line-height: 1.4444; /* 26px, with the body (2026-10-06); the app's twin is still 1.6 */
  border-radius: 0 0 var(--radius-lg) var(--radius-lg);
}
.terms-dialog__doc h1 { font-size: 34px; margin: 0 0 24px; }
.terms-dialog__doc h2 { font-size: 24px; margin: 32px 0 12px; }
.terms-dialog__doc h3 { font-size: 20px; margin: 24px 0 10px; }
.terms-dialog__doc p { margin: 0 0 1em; }
.terms-dialog__doc ul,
.terms-dialog__doc ol { padding-left: 22px; margin: 0 0 1em; }
/* Stated rather than inherited, to match the app, whose base reset takes list markers off. */
.terms-dialog__doc ul { list-style: disc; }
.terms-dialog__doc ol { list-style: decimal; }
.terms-dialog__doc a { color: var(--coral); }
.terms-dialog__doc > :last-child { margin-bottom: 0; }

/* All six cards in the home benefits grid take a coral outline and the site's soft shadow under
   the cursor (owner, 2026-09-19: the calculator card had it first, and the owner asked for it on
   "the other 5 similar cards"). It hangs off `.cards--hover` on the GRID, not off `.card`,
   because `.cards--2 > .card` is also on /contact, /platforms and /sections, which were not
   asked for.
   WHAT IT IS NOT: a lift. The calculator card's first rule also said translateY(-6px), and it
   never ran. These cards carry `data-reveal`, so they wear `.will-reveal.is-revealed`, whose
   `transform: none` is the same specificity and comes later in the file; the owner approved
   what that left on screen, the outline and the shadow, so the dead transform is gone rather
   than fought for. It would also have dragged the card against its own entrance animation.
   THE TRANSITION is restated in full for the same reason: `.will-reveal` sets `transition` to
   opacity and transform only, which made the outline and shadow SNAP on the calculator card.
   Two classes outrank its one, so this list wins wherever it sits, and it has to repeat the
   entrance's own 0.55s pair or the cards would stop sliding in. 0.3s is the site's hover tier.
   Colour and shadow only, nothing moves, so it is left ungated for reduced motion like the
   header's fade. */
.cards--hover > .card {
  transition:
    opacity 0.55s ease,
    transform 0.55s ease,
    box-shadow 0.3s ease,
    border-color 0.3s ease;
}
.cards--hover > .card:hover { box-shadow: var(--shadow); border-color: var(--coral); }
/* The five illustrated benefit cards (owner, 2026-09-19): heading across the top, then the copy
   on the left with its drawing on the right. The same wrapping row the calculator card uses,
   and it wraps for the same reason: the stylesheet's five width queries are fixed and
   `check:breakpoints` refuses a sixth, so the paragraph asks for 220px and the drawing drops
   below it on a card too narrow to give it that, instead of the two grinding down together.
   `margin-left: auto` keeps the drawing against the right edge on that dropped line too.
   Deliberately NOT merged with `.calc-card__row`: that card puts its heading inside the left
   column so "Try Me!" can sit level with it, so the two rows hold different children and one
   selector covering both would have to describe two shapes at once. The values are shared by
   hand, and the height below is the number that ties all six cards together. */
.card__split { display: flex; flex-wrap: wrap; align-items: center; gap: 16px 24px; }
.card__split > p { flex: 1 1 220px; margin: 0; }
/* 182px tall, the calculator's own rendered height, so the six drawings carry equal weight.
   The SVGs are 114x164, a hair under the calculator's 391x560, so `width: auto` lands them all
   at ~127px without a second number to keep in step. */
.card__visual { display: block; flex: none; height: 182px; width: auto; max-width: 100%; margin-left: auto; }

/* Copy on the left, the calculator on the right (owner, 2026-09-19). A wrapping flex row rather
   than a grid with a breakpoint, because the stylesheet's five width queries are fixed and
   `check:breakpoints` refuses a sixth: the paragraph asks for 220px, and where a card cannot
   give it that beside a 127px picture (a phone, where the card's content box is 288px) the
   picture drops to its own line instead of squeezing the copy into a fifteen-character column.
   `margin-left: auto` keeps it against the RIGHT edge on that line too, so "on the right" holds
   at every width. */
/* TOP-aligned since 2026-09-19 (owner: the title "needs to move up, so it is the same distance
   from the top border of the card as the same type of title text on other cards"). It was
   centred while the row held only the paragraph and the picture; once the heading joined the
   left-hand group, centring that group against the taller right-hand column pushed the heading
   down off the line every other card's heading sits on. */
.calc-card__row { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 16px 24px; }
/* The heading and the paragraph are one group since "Try Me!" arrived (see CALC_CARD in
   site-pages-1.mjs): the group is what flexes and asks for 220px, and the button is the whole
   right-hand column beside it. */
.calc-card__text { flex: 1 1 220px; min-width: 0; }
/* This card's title is the only one long enough to WRAP (it shares its row with the picture), and
   `.card__head` centres the icon against the title: with two lines the icon dropped 11px and
   the first line rose 3px, so neither matched the cards beside it. Here the icon is pinned to
   the top and the title is given the offset a single centred line gets, half of what is left of
   the 33px icon after one line box. On one line that is exactly where centring put it; on two,
   the FIRST line is where every other card's title is. 3px is the fallback for a browser
   without the `lh` unit, and is the same number to the nearest pixel. */
.card--calc .card__head { align-items: flex-start; }
.card--calc .card__head h3 { margin-top: 3px; margin-top: calc((33px - 1lh) / 2); }
.calc-card__copy { margin: 0; }
/* "Calculate savings" (owner, 2026-09-21, to a supplied mockup): a second way into the dialog,
   under the paragraph, drawn as a link and behaving as a button. See CALC_CARD_LINK in
   site-pages-1.mjs for why it is a <button> and not an <a>.
   The mockup's formatting is coral, bold, an arrow after the text, no underline. Coral and the
   no-underline are what the base `a` rule already gives every link on this site, so the two
   read as one thing; they are restated here because a <button> inherits NEITHER - it comes with
   the UA's own colour, font and chrome, which `font: inherit` plus the three resets below undo.
   font-family/size follow `.btn` (Inter Tight at 1.125rem, the site's control convention).
   WEIGHT 700 is the mockup's, against the 600 `.btn` and `.arrow-link` use: the drawing has it
   distinctly heavier than the copy above it, and matching the drawing was the instruction.
   Deliberately NOT `.arrow-link`, which is a /sections catalog component - site-pages-1.mjs
   records why the home page does not borrow it. */
.calc-card__link {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin: 14px 0 0;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-family: var(--font-head);
  font-size: 1.125rem;
  font-weight: 700;
  line-height: 1.3;
  color: var(--coral);
  cursor: pointer;
  text-align: left;
}
.calc-card__link:hover { color: var(--coral-hover); }
.calc-card__link:focus-visible { outline: 3px solid var(--coral); outline-offset: 3px; border-radius: 6px; }
/* The arrow nudges on hover, the same 2px-ish move `.arrow-link` makes, so the two arrows on
   this site behave alike even though they are drawn differently. `flex: none` keeps it from
   being squeezed when the label wraps on a narrow card. */
.calc-card__link svg { flex: none; transition: transform 0.18s ease; }
.calc-card__link:hover svg { transform: translateX(3px); }
@media (prefers-reduced-motion: reduce) {
  .calc-card__link svg { transition: none; }
}
/* Hidden by site.js where <dialog> is unsupported, exactly as `.calc-open` is: a control that
   visibly does nothing is worse than no control. `!important` because the UA's `[hidden]` rule
   loses to any author `display`, and this one sets `inline-flex`. */
.calc-card__link[hidden] { display: none !important; }
/* The owner's "Try Me!" artwork, above the calculator and inside the same button. 96px wide on
   a 127px calculator and set 6px in from its left edge, which is where the mockup has it: the
   arrow then lands just right of the calculator's centre. The -2px pulls the arrowhead down to
   within a few pixels of the calculator's top edge, as drawn. */
/* `top: -4px` is the owner's nudge (2026-09-19, "needs to move up 4px"). A relative offset and
   not a margin, so ONLY the artwork moves: a margin would have pushed the calculator down with
   it and re-centred the whole column against the copy. */
.calc-open__cue { display: block; position: relative; top: -4px; width: 96px; height: auto; margin: 0 0 -2px 6px; }
/* The button is exactly the picture now, no wider: it used to span the card, but a full-width
   button beside a paragraph would make the copy's own column clickable by accident. */
/* `top: -5px` lifts "Try Me!" and the calculator together (owner, 2026-09-19: "up 5px, without
   moving anything else in the card"). Relative, so the button's layout box stays where it was:
   the copy, the heading and the card's height are all computed from the unmoved box. */
/* `[hidden]` has to be restated with `!important` because the `display: block` below would
   otherwise beat it. `[hidden] { display: none }` is a USER-AGENT declaration, and any author
   `display` on the same element wins no matter how weak its selector, so site.js's
   `calcOpen.hidden = true` fallback (the no-<dialog> branch) was a measured no-op: the picture,
   the "Try Me!" arrow and the copy's "Click the calculator to try it." all stayed on screen with
   nothing behind them, which is the dead control this file keeps refusing. Same fix and the same
   reason as the session hooks near line 660. */
.calc-open[hidden] { display: none !important; }
.calc-open {
  display: block;
  position: relative;
  top: -5px;
  flex: none;
  margin-left: auto;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}
.calc-open:focus-visible { outline: 3px solid var(--coral); outline-offset: 4px; border-radius: 10px; }
/* Sized by HEIGHT, so the row's height is a number set here rather than a consequence of the
   card's width: the picture is 391x560 and at a card's full width it would be 560px tall and
   tower over the four text cards below it.
   182px is 30% off the 260px it opened at (owner, 2026-09-19). That now puts the card BELOW the
   text cards beside it rather than above them, which is what the owner asked for and is why the
   empty placeholder next to it shrinks to match: the two are one grid row and stretch together.
   The asset is untouched and still 391px wide against a rendered 127px, so it has resolution to
   spare on a 3x screen.
   It was left-aligned under the heading for an hour; it now sits on the RIGHT of the card with
   the explanatory copy on its left (see `.calc-card__row` above). */
.calc-open__img {
  display: block;
  height: 182px;
  width: auto;
  max-width: 100%;
  margin: 0;
}

/* ------------------------------------------------------------- checklist */
.checklist { list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: 1fr 1fr; gap: 10px 30px; }
.checklist li { padding-left: 26px; position: relative; }
.checklist li::before { content: "✓"; position: absolute; left: 0; color: var(--green); font-weight: 700; }
/* One column instead of two, for a list inside a card (owner's /compatibility cards,
   2026-09-22). `.checklist` is a 2-up grid that collapses to 1 at 1000px: right for a full-width
   list under a heading, wrong inside a half-width card, where two columns leave about 150px a
   side and every bullet wraps three times.
   IT HAS TO SIT HERE, immediately after the rule it overrides. Both selectors are one class, so
   specificity is a tie and SOURCE ORDER decides. Written earlier in the file it silently loses
   and the list renders 2-up anyway, which is exactly what happened first time round. */
.checklist--1 { grid-template-columns: 1fr; }

/* ---------------------------------------------------------------- quote */
.proof-quote {
  border-left: 4px solid var(--coral);
  background: var(--band);
  border-radius: 0 var(--radius) var(--radius) 0;
  padding: 18px 22px;
  font-family: var(--font-head);
  font-weight: 500;
  color: var(--ink);
  font-size: 1.125rem;
}

/* -------------------------------------------------------------- timeline */
.timeline { border-left: 2px solid var(--line); padding-left: 26px; display: grid; gap: 20px; }
.timeline > div { position: relative; }
.timeline > div::before {
  content: "";
  position: absolute;
  left: -33px;
  top: 7px;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--coral);
}
.timeline strong { color: var(--ink); font-family: var(--font-head); }

/* ---------------------------------------------------------------- footer */
.site-footer { background: var(--dark); color: #b9b2ac; padding: 56px 0 28px; font-size: 1.125rem; }
.site-footer a { color: #d8d2cc; }
.site-footer a:hover { color: #fff; }
/* ONE TRACK PER COLUMN, and the count is load-bearing: the brand at 1.4fr then one 1fr per
   link column. It read 1.4fr + FOUR 1fr until 2026-09-21, when the owner removed the Devices
   column; the fifth track had no child, so it sat empty on the right and the three remaining
   columns bunched to the left, ~210px short of the container edge at 1440px. A track count
   that outlives its column is invisible in the markup and only shows as drift in the layout,
   so if a column is ever added or removed, change this line in the same edit. The 1000px and
   850px overrides below are counts of their own (2 then 1) and do not need to match. */
.site-footer__grid { display: grid; grid-template-columns: 1.4fr 1fr 1fr 1fr; gap: 32px; margin-bottom: 36px; }
/* A PURELY OPTICAL NUDGE (owner, 2026-09-21): the tracks are mathematically even, but the
   Product column holds two short words where Compatibility holds four long ones, so the eye
   reads the middle of the row as drifting right. 25px left corrects what you see, not what
   the grid computes. It is a CLASS on the column rather than :nth-child(3), because the
   Devices removal earlier the same day proved how quietly a positional selector rots when a
   column count changes - this one follows the Compatibility column wherever it sits.
   Reset at 1000px, where the row becomes two columns and then one: stacked columns share a
   left edge and the pull would break that alignment rather than improve it. */
.site-footer__col--pull { margin-left: -25px; }
/* The wordmark, a link to the home page since 2026-10-06 (owner), so the size and the margin
   sit on the link and the img fills it. Never a bare `.site-footer__brand img`: the store badges
   are in this column too (since 2026-09-19), and a descendant img rule forced both to 128px
   wide, squashing the Play badge (the wider of the two) horizontally. */
.site-footer__logo { display: block; width: 128px; margin-bottom: 12px; }
.site-footer__logo img { width: 100%; }
/* The tagline under the footer wordmark, taken down twice on 2026-09-19 (owner, in two asks):
   the SIZE is 16px, the 18px it inherits from `.site-footer` less 2, and the LINE-HEIGHT is
   25.7px. That second number is this rule's own earlier 27.7px less 2, and 27.7 was itself the
   inherited rhythm (18px x `body`'s 1.65 = 29.7px) less 2. So the leading has come down 4px in
   total while the type came down 2. Both are written in px, not as ratios, because both asks
   were in PIXELS: as a ratio the leading would be 1.606 of the new size and would silently
   stop being the owner's number the moment either value moved.
   16px is UNDER the site's 18px floor, so it carries the dated approval the floor check wants
   (approved by the owner, 2026-09-19: "Reduce the font-size of ... by 2px"); it is the ninth
   such exception. The line wraps to two in the 1.4fr column at desktop, so the leading change
   is visible there rather than academic. */
.site-footer__brand p { font-size: 16px; line-height: 25.7px; }
/* The four footer column headings are h2, not h4 (2026-08-11): the page bodies above them
   stop at h2, so h4 skipped a heading level on every page. The level is a document-structure
   fact and the size is a design one, so they keep their own size rather than the 34px h2:
   20px, owner-set 2026-08-12, up from 1.125rem (18px). */
.site-footer h2 { color: #fff; font-size: 20px; margin-bottom: 12px; }
.site-footer ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
/* ~2px tighter than the body's 1.65 rhythm so a footer link that wraps to two lines
   reads as one item (owner, 2026-08-10). */
.site-footer ul a { line-height: 1.54; }
.site-footer__honesty { border-top: 1px solid var(--line-dark); padding-top: 20px; color: #cfc9c4; }
/* #827d79, ONE SHADE LIGHTER than the #746f6a it carried (owner, 2026-10-23: "This footer text
   needs to use a font color that is a shade lighter"), for this line and the trademark credits
   below it, which the instruction quoted together. A shade lightens by this site's 10% step,
   255 - (255 - c) x 0.9: 116/111/106 -> 130/125/121.
   History: #8f8983 until 2026-09-17, when the owner took it two shades darker to #746f6a.
   RECORDED BECAUSE IT COSTS SOMETHING: #8f8983 sat at 4.55:1 on the footer's #232323, just over
   the 4.5:1 AA floor, and looks deliberately placed there. #746f6a was 3.16:1 and #827d79 is
   3.86:1, better but still under that floor. The owner asked for both with the trade named.
   Nothing in `npm run check` enforces contrast, so no gate will catch a later drift here; the
   numbers above are the record. Revert is one value. */
/* 14px, approved by the owner 2026-09-19 ("Change the font-size for the footer disclaimers to
   14px"; it was 12px for a few minutes the same day, and 18px before that), an exception to the site's 18px floor. It applies to this line and to the
   trademark credits below it, which the same instruction covered; both are boilerplate a
   reader scans for rather than reads, and the drawer's own copyright line has been 12px on the
   same grounds since 2026-08-11.
   Contrast, already named in README gap 0 and not helped by the size: #827d79 on the footer's
   #232323 is 3.86:1, where AA asks 4.5:1, and 14px is well under the large-text threshold that
   would soften the ask. Nothing in `npm run check` measures contrast, so this comment and that
   gap are the record. Reverting is two values. */
.site-footer__legal { display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap; margin-top: 10px; font-size: 14px; color: #827d79; }
/* The Apple and Google trademark credits both badge programmes require, on every page under
   the copyright line (owner, 2026-09-19, footer rather than the home page).
   It wears the COPYRIGHT LINE'S colour, #827d79 since 2026-10-23 (#746f6a before; see the
   copyright rule above), not the footer's body #b9b2ac it carried for
   its first hours (owner, 2026-09-19: "text style of [the trademark line] needs to match the
   style we use for Copyright"). That is the whole of the ask: both lines were already 18px in
   the body face, so colour was the only difference between them.
   THE TRADE, NAMED: #827d79 on the footer's #232323 is 3.86:1 (#746f6a was 3.16:1), under the
   4.5:1 that AA asks of 18px regular text, where #b9b2ac was 7.50:1. The sub-floor exception recorded in README gap 0
   for the copyright line now covers this line too, and it is the owner's call both times.
   Nothing in `npm run check` measures contrast, so no gate will catch a drift here or a third
   line joining them; this comment and that gap are the record. Revert is one value. */
/* 14px, approved by the owner 2026-09-19, the same instruction as the copyright line above;
   see the note there for the contrast trade the size carries with it. */
.site-footer__marks { margin: 8px 0 0; font-size: 14px; color: #827d79; }

/* ----------------------------------------------------------- consent bar */
.consent-bar {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 90;
  background: var(--dark);
  border-top: 2px solid #4e4e4e;
  color: #d8d2cc;
  padding: 14px 24px;
  display: none;
  gap: 18px;
  align-items: center;
  flex-wrap: wrap;
  font-size: 1.125rem;
}
.consent-bar.is-open { display: flex; }
.consent-bar p { margin: 0; flex: 1 1 380px; }
.consent-bar .btn { padding: 9px 20px; font-size: 1.125rem; }
.consent-bar a { color: #fff; text-decoration: underline; }

/* ---------------------------------------------------------------- prose */
/* No margin-top override here on purpose (owner, 2026-08-12). `.prose h2` used
   to carry `margin-top: 2em`, which is 71.76px, and it was the only rule on the
   site that gave an h2 its own top margin. It made the headings on /terms and
   /privacy float: measured, they sat 72px below the preceding paragraph while
   every h2 on every other page sits 14px below its eyebrow. The legal pages were
   the only two that read differently, which is what the owner reported.
   Without it an h2 here takes the global `h2 { margin: 0 0 0.5em }` and clears
   the paragraph above by that paragraph's own 1em bottom margin, so about 18px,
   which is the site's heading rhythm. Blog posts are unaffected: their rules are
   scoped to `.post-layout .prose` near the end of this file. */

/* `:not(.tags)` for the same reason as the blog list rules further down: this indents PROSE
   bullets, and .tags is a component list of pills that sets its own padding: 0. At (0,1,1) it
   outranks .tags at (0,1,0), so without the exclusion the pill row still sat 22px in from its
   own heading even after the blog rules were scoped away from it. */
.prose ul:not(.tags) { padding-left: 22px; }
/* ============================================================================
   SECTION CATALOG
   Two different things live under this heading, and the distinction matters.

   1. Catalog chrome (.sec-toc, .sec-entry, .sec-bar, .sec-meta, .sec-note,
      .sec-stage). Used ONLY by /sections. Nothing else should ever reach for
      these; if a real page grows a numbered strip, something has gone wrong.

   2. Section components (.sec-head, .card--tool, .arrow-link, .collage,
      .accord, .split, .counters, .ph). These are the reusable parts. They ship
      site-wide precisely so that pasting a catalogued section into any page
      works with no further CSS. Rebuilt from the retired Webflow template's
      components; see scripts/site-sections.mjs.

   Responsive rules for both live in the existing 1000px / 850px blocks below.
   Do NOT open a new media query: the stylesheet has exactly five width
   queries and adding a sixth is how a breakpoint scheme rots.
   ========================================================================= */

/* ---- catalog chrome ---------------------------------------------------- */
.sec-toc { display: grid; grid-template-columns: repeat(3, 1fr); gap: 26px 32px; }
.sec-toc__group h3 { margin: 0 0 10px; }
.sec-toc ul { list-style: none; padding: 0; margin: 0; display: grid; gap: 6px; }
/* Two columns rather than an inline number, so a name that wraps to a second
   line hangs under itself instead of resetting to the left margin. */
.sec-toc a, .sec-toc__soon { display: grid; grid-template-columns: 2.4em 1fr; }
.sec-toc a { text-decoration: none; }
.sec-toc a:hover span + span { text-decoration: underline; }
.sec-toc__n {
  font-family: var(--font-head);
  font-weight: 700;
  color: var(--coral);
}
.sec-toc__soon { color: var(--muted); }
.sec-toc__soon em { font-style: normal; opacity: 0.8; }
/* Marks a section you can actually operate on this page, rather than look at. */
.sec-toc__live, .sec-bar__live {
  font-style: normal;
  font-size: 1.125rem;
  color: var(--coral);
  font-weight: 600;
}
.sec-bar__live {
  padding: 2px 10px;
  border: 1px solid var(--coral);
  border-radius: 999px;
}

/* The strip that names a section. Sticky so the number stays visible while you
   scroll a tall section, which is the whole point of numbering them. */
.sec-entry { border-top: 3px solid var(--coral); }
.sec-bar {
  position: sticky;
  /* Clears the sticky site header: the 72px row (.site-header__inner min-height) plus its 8px
     top padding (2026-10-27), at every width. top: 0 would park this strip underneath it. */
  top: 80px;
  z-index: 40; /* under the site header (50): the header must stay clickable */
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 6px 16px;
  padding: 12px 24px;
  background: var(--dark);
  color: #fff;
}
.sec-bar__n {
  font-family: var(--font-head);
  font-size: 1.6rem;
  font-weight: 700;
  color: var(--coral);
  line-height: 1;
}
.sec-bar__name { font-family: var(--font-head); font-weight: 600; font-size: 1.2rem; }
.sec-bar__group { color: #cfc9c4; font-size: 1.125rem; }
.sec-bar__anchor { margin-left: auto; color: #cfc9c4; font-size: 1.125rem; }
/* overflow-wrap because these two lines carry class-name inventories, and a selector
   list is one unbreakable token to a line breaker: section 45's "needs" reads
   ".plan__strip/body/head/tag/price/facts/cta/pill", 44 characters with no space in
   them. At 390px that one token could not fit its line box and pushed the DOCUMENT to
   420px, so the whole catalog scrolled sideways on a phone off a single paragraph.
   Diagnosed 2026-08-23 and fixed here rather than by rewording the entry, because the
   next inventory to be written would bring it straight back. Nothing else on the site
   overflowed at any width; this was the only case. */
.sec-meta, .sec-note {
  max-width: 1140px;
  margin: 14px auto 0;
  padding: 0 24px;
  font-size: 1.125rem;
  color: var(--muted);
  overflow-wrap: break-word;
}
.sec-note { color: var(--body-text); }
.sec-stage { margin-top: 10px; }

/* A drawn placeholder, not a file: the template's 160 images went with the
   rebuild, and a real file here would imply a finished asset. */
.ph {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  border: 2px dashed var(--line);
  border-radius: var(--radius);
  background: repeating-linear-gradient(
    -45deg,
    var(--band) 0 10px,
    var(--surface) 10px 20px
  );
  color: var(--muted);
  font-size: 1.125rem;
  text-align: center;
  padding: 12px;
}

/* ---- reusable section components --------------------------------------- */
/* Centered eyebrow + heading above a grid. */
.sec-head { text-align: center; max-width: 720px; margin: 0 auto 34px; }
.sec-head h2 { margin-bottom: 0; }

/* The flow's head (2026-11-01): left-aligned over the four columns, in line with the hero copy
   above it, where the shared .sec-head centres over a grid. The full container width, so the
   h2 holds one line at 1140 (owner, later the same day: "This should be full width, not
   wrapping"; it was capped at 760 and broke after "four"); it wraps only where the container
   itself is too narrow for it. 12px above it and 40px below it to the first step. The home
   page dropped its head the same day (owner); only the /sections entry still renders one. */
.sec-head--flow { text-align: left; max-width: none; margin: 12px 0 40px; }

/* Card with an image slot beneath the copy (section 9). Pushes the image to
   the bottom so cards with unequal copy still line their images up. */
.card--tool { display: flex; flex-direction: column; }
.card--tool .ph { margin-top: auto; }

/* Dark featured card (section 15). In the template this is a STATIC class
   variant on the middle card (their #111827), not a hover state; ours paints
   with --dark. Composes with .card--tool. */
.card--dark { background: var(--dark); border-color: var(--dark); color: #cfc9c4; }
.card--dark h3 { color: #fff; }

/* The home page's download card (owner, 2026-09-19): one container-wide row 134px tall,
   between the benefit cards and the closing coral band, carrying a heading, the /mobile page's
   "native app" line and the largest of the three store-badge pairs (DOWNLOAD_CARD in
   site-pages-1.mjs; section 56 of the catalog). It composes with `.card--dark` above, which
   supplies the #232323 ground and paints the 1px border in that same colour, so the geometry
   every `.card` shares is kept and no edge shows; this modifier adds only the row.
   HEIGHT at desktop, top to bottom: 1px border + 34px padding + 64px badges + 34px padding +
   1px border = 134px, the badges being the tallest thing in the row. It was 120 with 27px
   paddings until the owner added 5px top and bottom (2026-09-19, "for both new app store
   button sections", the two placings of this one card) and then "2px more"; the 850px
   block's stacked padding went from 24 to 29 to 31 top and bottom by the same two
   instructions, its 24px sides unchanged. The
   copy column is
   26px x 1.15 = 29.9px of heading, a 4px gap and 18px x 1.4 = 25.2px of subline, 59.1px in
   all, so it never sets the height while the heading holds one line. The h2 takes its own
   26px here rather than the site's 34 (34 plus the subline would push the card past 150px)
   and stays well over the 18px floor; the subline is 18px, the floor itself, so no sub-floor
   exception is made. Colours: the heading is white on #232323 (15.6:1) and the subline wears
   the footer's #b9b2ac (7.50:1), both over AA.
   WIDTH at 1140px: the container's 1092px less the borders, the two 32px side paddings, the
   28px gap and the 442.67px pair leaves 555px for the heading, which Inter Tight 600 at 26px
   needs about 475px of, so it holds one line down to roughly 1060px of viewport and wraps to
   two below that (adding about 30px to the card) until the 850px block stacks the row.
   `min-width: 0` on the copy is what lets it wrap rather than push the row wider than the
   card, and the badge group is `flex: 0 0 auto` (on its own rule) so it never shrinks. */
.card--download { display: flex; align-items: center; justify-content: space-between; gap: 28px; padding: 34px 32px; }
.card--download .card__copy { flex: 1 1 auto; min-width: 0; }
.card--download h2 { color: #fff; font-size: 26px; margin: 0 0 4px; }
.card--download p { color: #b9b2ac; line-height: 1.4; margin: 0; }

/* Icon square at the top of a rich card (section 15). Same recipe as
   .icon-card__glyph, standalone so it can sit inside a plain .card. */
.card__glyph {
  width: 46px;
  height: 46px;
  border-radius: 12px;
  background: var(--band);
  display: grid;
  place-items: center;
  margin-bottom: 14px;
  font-family: var(--font-head);
  font-weight: 700;
  color: var(--coral);
}
.card--dark .card__glyph { background: rgba(255, 255, 255, 0.12); }

/* The home page's benefit cards put a bare line icon on the SAME ROW as the title (owner,
   2026-09-17). Its own pair of classes rather than a `.card__glyph` modifier: with the tile
   gone there is no background, no 46px box, no radius and no bottom margin left, so a modifier
   would have had to negate every declaration of the thing it modified. `.card__glyph` keeps
   its 15 uses on /sections, all of them numbered tiles, and is untouched.
   33px is the 22px it shipped at this morning plus half. The stroke scales with the box, as
   scaling an icon should: 2 units on the 24 grid renders 2.75px here where it rendered 1.83px
   at 22px. Holding the stroke at its old absolute width was tried and looked underweight
   beside a 24px heading.
   The row's 12px bottom margin is the gap the h3 used to carry, so the distance from the title
   to the paragraph is unchanged and only the icon moved. */
.card__head { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
/* #232323, the flat neutral the step titles, the hero H1 and the account menu rows also take
   (owner, 2026-09-17), rather than the warmer --ink the shared heading rule hands everything
   else. No `:not(.inverted)` guard, unlike the hero H1: that one needs it because /sections'
   invert switch lands `.inverted` on the hero root itself, and this row is on the home page
   only, inside a plain section, with no presence in the catalogue at all. If it is ever
   catalogued, `.inverted` would sit on an ANCESTOR section rather than on .card__head, so the
   fix would be a counterpart rule in the inverted block, not a :not() here. */
.card__head h3 { margin: 0; color: #232323; }
.card__icon { flex: none; color: var(--coral); }

/* The one benefit card that carries a picture (owner, 2026-09-18): the same lamp on the back
   of a DSLR and framed in the app's capture screen, under "Capture Photos".
   No border, radius or shadow, for the same reason `.hero-mock` takes none: the artwork is on
   a TRANSPARENT ground, so a box would frame the empty corners rather than the two devices.
   Deliberately NOT pinned to the bottom of the card. `.card` is a plain block, so `margin-top:
   auto` would do nothing here, and turning it into a flex column to earn that would change
   every other `.card` on the site to buy alignment in one row. The grid stretches a row to its
   taller card, so the neighbour keeps whitespace under its text; that is the cost and it is
   paid on one row of one page. */
.card__shot {
  display: block;
  width: 100%;
  height: auto;
  margin-top: 18px;
}

/* The barcode card's picture is a tighter crop than the capture card's, so at full card width
   it would stand 378px tall against that one's 305px and drag the whole row up to the taller
   of the two. Its height is capped to 305px to match instead, its width follows from its own
   aspect ratio, and it is centred in what that leaves. Both cards then finish level and
   neither carries whitespace under its picture.
   305 is a MEASUREMENT of the sibling image, not a design value: it is what
   `card-capture.webp` renders at across the 481px card. It is hard-coded because the
   proportion that would keep the two in step is the sibling's aspect ratio, and CSS here has
   no way to read that. Replace either picture and re-measure this.
   Named for what it does rather than for the shape it was first used on. It started as
   `--portrait`, which stopped being true the moment the barcode shot was recropped to
   landscape on 2026-09-18, and a class that lies about its own subject is worse than a
   vague one. */
.card__shot--capped {
  width: auto;
  max-width: 100%;
  height: 305px;
  margin-left: auto;
  margin-right: auto;
}

/* Mini stat rows: the third card's stacked figures (section 15). --hero is
   the filled highlight row. Placeholder digits only until a real, checkable
   figure replaces them. */
.mini-stat { display: flex; align-items: center; gap: 12px; padding: 12px 14px; border: 1px solid var(--line); border-radius: 12px; background: var(--card); }
.mini-stat + .mini-stat { margin-top: 10px; }
.mini-stat--hero { background: var(--green); border-color: var(--green); color: #fff; }
.mini-stat__icon {
  width: 38px;
  height: 38px;
  border-radius: 10px;
  background: var(--band);
  flex: none;
  display: grid;
  place-items: center;
  font-family: var(--font-head);
  font-weight: 700;
  color: var(--coral);
}
.mini-stat--hero .mini-stat__icon { background: rgba(255, 255, 255, 0.2); color: #fff; }
.mini-stat__val { font-family: var(--font-head); font-weight: 700; }
.mini-stat__label { color: var(--muted); font-size: 1.125rem; }
.mini-stat--hero .mini-stat__label { color: #fff; }
.mini-stat__delta { margin-left: auto; font-size: 1.125rem; font-weight: 600; }
.arrow-link {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin: 4px 0 18px;
  font-weight: 600;
  text-decoration: none;
}
.arrow-link::after {
  content: "";
  width: 9px;
  height: 9px;
  border-top: 2px solid currentColor;
  border-right: 2px solid currentColor;
  transform: rotate(45deg);
  transition: transform 0.18s ease;
}
.arrow-link:hover::after { transform: rotate(45deg) translate(2px, -2px); }
@media (prefers-reduced-motion: reduce) {
  .arrow-link::after { transition: none; }
}

/* Two-column split, text one side, art the other (sections 7 and 13). */
.split { display: grid; grid-template-columns: 1fr 1fr; gap: 48px; align-items: center; }

/* Overlapping image cluster (section 7). The floats are absolutely placed
   against the base image, so the whole thing scales with its container. */
.collage { position: relative; }
.collage__float { position: absolute; box-shadow: var(--shadow); border-radius: var(--radius); overflow: hidden; }
.collage__float--a { width: 34%; right: -6%; top: 14%; }
.collage__float--b { width: 52%; left: -6%; bottom: 8%; }

/* Exclusive accordion (section 7). Built on <details name>, so one-open-at-a-
   time and full keyboard support come from the browser with no JS at all.
   Deliberately NOT .faq-item: this one fills the open panel with coral. */
.accord { display: grid; gap: 12px; margin-top: 26px; }
.accord__item { background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; }
.accord__item summary {
  list-style: none;
  cursor: pointer;
  padding: 16px 20px;
  font-family: var(--font-head);
  font-weight: 600;
  font-size: 1.2rem;
  color: var(--ink);
}
.accord__item summary::-webkit-details-marker { display: none; }
.accord__item[open] { background: var(--coral); border-color: var(--coral); }
.accord__item[open] summary { color: #fff; }
.accord__body { padding: 0 20px 18px; }
.accord__body p { margin: 0; }
.accord__item[open] .accord__body { color: #fff; }

/* Count-up figures (section 13). The value is in the HTML; site.js only
   animates it, so this reads correctly with JS off. */
.counters { display: flex; flex-wrap: wrap; gap: 34px; margin-top: 30px; }
.counter__num {
  display: block;
  font-family: var(--font-head);
  font-size: clamp(2rem, 4vw, 2.8rem);
  font-weight: 700;
  color: var(--coral);
  line-height: 1.1;
}
.counter__label { font-size: 1.125rem; color: var(--muted); }

/* ---- tabs (JS: [data-tabs]) --------------------------------------------
   Panels are NOT hidden here. site.js hides all but the active one once it
   has wired the tablist; with JS off every panel stays visible and stacked,
   which is a worse layout but a complete one. Hiding by default in CSS would
   blank the section for anyone whose script did not run. */
.tabs__list { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 26px; }
.tabs__tab {
  padding: 10px 18px;
  border: 2px solid var(--line);
  border-radius: 999px;
  background: var(--card);
  font-family: var(--font-head);
  font-size: 1.125rem;
  font-weight: 600;
  color: var(--ink);
  cursor: pointer;
}
.tabs__tab:hover { border-color: var(--coral); color: var(--coral); }
.tabs__tab.is-active { background: var(--coral); border-color: var(--coral); color: #fff; }
/* Only once JS is driving it does a stacked panel get a divider; before that
   the panels run together and the rule would be noise. */
[data-tabs-ready] .tabs__panel + .tabs__panel { border-top: 0; }

/* ---- carousel (JS: [data-carousel]) -------------------------------------
   Scroll-snap does the work; the buttons and dots are the enhancement. The
   track is swipeable and complete before any script runs. */
.carousel { position: relative; }
.carousel__track {
  display: grid;
  grid-auto-flow: column;
  /* Three across, and the track overflows once there is a fourth slide. This
     MUST be a fraction of the container minus the gaps, not `1fr`: with 1fr
     the columns shrink to fit whatever is there, the track never overflows,
     and a carousel that cannot scroll is just a row. The 44px is the two
     visible gaps (2 x 22px); change one and change the other. */
  grid-auto-columns: calc((100% - 44px) / 3);
  gap: 22px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  padding-bottom: 4px;
}
.carousel__track::-webkit-scrollbar { display: none; }
.carousel__slide { scroll-snap-align: start; min-width: 0; }
.carousel__nav { display: flex; gap: 10px; align-items: center; margin-top: 20px; }
.carousel__btn {
  width: 46px;
  height: 46px;
  border-radius: 999px;
  border: 2px solid var(--line);
  background: var(--card);
  color: var(--ink);
  font-size: 1.125rem;
  cursor: pointer;
  line-height: 1;
}
.carousel__btn:hover:not(:disabled) { border-color: var(--coral); color: var(--coral); }
.carousel__btn:disabled { opacity: 0.4; cursor: default; }
.carousel__dots { display: flex; gap: 8px; margin-left: auto; }
.carousel__dot {
  width: 10px;
  height: 10px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: var(--line);
  cursor: pointer;
}
.carousel__dot.is-active { background: var(--coral); width: 26px; }

/* ---- marquee -----------------------------------------------------------
   Pure CSS, no JS. Same loop maths as the platform ticker: the track holds
   the set TWICE and translates exactly -50%, so the seam lands on an
   identical frame. Duplicate the items or the loop visibly jumps. */
.marquee { overflow: hidden; -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); }
.marquee__track { display: flex; width: max-content; gap: 56px; animation: marquee 26s linear infinite; }
.marquee__item { font-family: var(--font-head); font-size: 1.3rem; font-weight: 600; color: var(--muted); white-space: nowrap; }
@keyframes marquee { from { transform: translateX(0); } to { transform: translateX(-50%); } }
@media (prefers-reduced-motion: reduce) {
  .marquee__track { animation: none; }
}

/* ---- scroll entrances (JS: [data-reveal], [data-reveal-children]) -------
   `will-reveal` is added BY JS, not written into the HTML, so content is
   never left invisible when the script does not run. Each variant picks the
   HIDDEN pose; they all resolve to the same visible resting state.

   ORDER IS LOAD-BEARING: `.will-reveal.is-revealed` ties every variant
   selector on specificity (0,2,0), so it wins only by coming last. Move a
   variant below it and that variant never un-hides. */
.will-reveal { opacity: 0; transform: translateY(18px); transition: opacity 0.55s ease, transform 0.55s ease; }
[data-reveal="left"].will-reveal { transform: translateX(-26px); }
[data-reveal="right"].will-reveal { transform: translateX(26px); }
[data-reveal="zoom"].will-reveal { transform: scale(0.93); }
/* The template's signature card entrance: tipped back, standing up as it
   arrives. The original used rotateX(90deg); 28deg reads the same without
   the split second of invisibility a 90deg start has. */
[data-reveal="flip"].will-reveal { transform: perspective(900px) rotateX(28deg) translateY(24px); transform-origin: center top; }
/* The home page's benefit cards arrive FROM THE OUTSIDE (owner, 2026-09-18): the left column
   starts 64px further left, the right column 64px further right, so the pair closes toward the
   page's centre line, fading as it comes. With data-reveal-repeat they slide back out on the
   way past.
   64px, not the 26px the plain `left`/`right` variants above use: those are a nudge that reads
   as emphasis, and this should read as arriving from off to the side. Separate variants rather
   than a change to those two, which /sections uses in five places at the smaller distance.
   This replaced a 25deg pivot on the bottom corners, tried first the same day and withdrawn.
   It cost two things worth remembering: a rotated card's bounding box is 76% taller than a
   straight one, which fed a measurement loop and made the cards visibly shake, and the swing
   reached 101px past the viewport at 1000px. A translate has neither problem, though it still
   overhangs enough on a narrow screen to need the clip below. */
[data-reveal="outside-left"].will-reveal { transform: translateX(-64px); }
[data-reveal="outside-right"].will-reveal { transform: translateX(64px); }
.will-reveal.is-revealed { opacity: 1; transform: none; }
/* A section holding an entrance that travels sideways has to CLIP horizontally, or the
   entrance produces a horizontal scrollbar. With the 64px slide the outer cards overhang by
   40px at 1000px, where the page's own margin is only 24px; at 1440px the margins absorb it.
   `overflow-x: clip`, not `hidden`: `hidden` would make the section a scroll container, which
   traps `position: sticky` descendants, and it is the only value that lets the other axis stay
   `visible`. Where `clip` is unsupported the declaration is dropped and the scrollbar returns,
   a degraded entrance rather than a broken page. */
.reveal-clip { overflow-x: clip; }

@media (prefers-reduced-motion: reduce) {
  .will-reveal { opacity: 1; transform: none; transition: none; }
}

/* ---- meters / progress bars (JS: [data-progress]) ----------------------- */
.meter + .meter { margin-top: 18px; }
.meter__head { display: flex; justify-content: space-between; font-size: 1.125rem; margin-bottom: 8px; }
.meter__track { height: 10px; border-radius: 999px; background: var(--band); overflow: hidden; }
.meter__bar { height: 100%; border-radius: 999px; background: var(--coral); transition: width 0.9s ease; }
@media (prefers-reduced-motion: reduce) {
  .meter__bar { transition: none; }
}

/* ---- cycling headline word (JS: [data-cycle]) --------------------------- */
.cycle { position: relative; display: inline-grid; vertical-align: bottom; }
.cycle b { grid-area: 1 / 1; font-weight: inherit; color: var(--coral); opacity: 0; transition: opacity 0.4s ease; }
.cycle b.is-current { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .cycle b { transition: none; }
}

/* ---- Features page cycle diagram (/features hero; /how-it-works until 2026-09-19) ----
   Inline SVG, not an image, so the four arrows can be animated individually.
   They draw on in clockwise order (photograph -> AI -> review -> export),
   hold while all four are visible, then reset. The stagger is delay-only, so
   every arrow shares one keyframe pair.

   Two deliberate constraints:
   - No CSS transform on .cyc-arrow. Arrows 2-4 carry a rotate() presentation
     attribute, and a CSS transform on the same element would replace it and
     collapse all four onto the top arrow.
   - stroke-dasharray lives inside the no-preference query, so reduced motion
     gets a plain solid stroke rather than a dashed one. */
.cycle-diagram { width: 80%; height: auto; display: block; margin: 0 auto; }

@media (prefers-reduced-motion: no-preference) {
  /* 136, deliberately NOT the measured path length of 135.14. stroke-dasharray has to
     EXCEED the path length: at dasharray == length the round linecap paints a 25px dot at
     the arrow's tail for the whole undrawn phase, once per cycle. Measured on this path:
     530 painted pixels at dashoffset 135.14, zero at 136. Do not "correct" this to the
     exact arc length (r=352 swept through 22deg); the 0.86 of slack is the fix. */
  .cyc-shaft { stroke-dasharray: 136; animation: cyc-draw 5.2s ease-in-out infinite; }
  .cyc-head { animation: cyc-head-in 5.2s ease-in-out infinite; }
  .cyc-arrow--2 .cyc-shaft, .cyc-arrow--2 .cyc-head { animation-delay: 0.5s; }
  .cyc-arrow--3 .cyc-shaft, .cyc-arrow--3 .cyc-head { animation-delay: 1s; }
  .cyc-arrow--4 .cyc-shaft, .cyc-arrow--4 .cyc-head { animation-delay: 1.5s; }
  @keyframes cyc-draw {
    0% { stroke-dashoffset: 136; opacity: 1; }
    12%, 82% { stroke-dashoffset: 0; opacity: 1; }
    92%, 100% { stroke-dashoffset: 0; opacity: 0; }
  }
  @keyframes cyc-head-in {
    0%, 9% { opacity: 0; }
    17%, 82% { opacity: 1; }
    92%, 100% { opacity: 0; }
  }
}

/* ---- media frame (screenshots, video) ---------------------------------- */
.media-frame { border-radius: var(--radius-lg); overflow: hidden; box-shadow: var(--shadow); background: var(--card); }
.media-frame video { display: block; width: 100%; height: auto; }

/* ---- quote / testimonial card ------------------------------------------ */
.quote-card { background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); padding: 26px; display: flex; flex-direction: column; gap: 16px; height: 100%; }
.quote-card blockquote { margin: 0; font-size: 1.2rem; }
.quote-card__who { display: flex; align-items: center; gap: 12px; margin-top: auto; }
.quote-card__avatar { width: 46px; height: 46px; border-radius: 999px; background: var(--band); flex: none; }
.quote-card__name { font-family: var(--font-head); font-weight: 600; }
.quote-card__role { font-size: 1.125rem; color: var(--muted); }

/* ---- leadership card photo -----------------------------------------------
   A real photograph, so it does not reuse .ph: that class is documented above as
   "a drawn placeholder, not a file" and exists for the /sections catalog only.
   .site-avatar (the logged-in header avatar) is the site's other circular real-photo
   treatment; this mirrors it at card scale rather than inventing a second idiom. */
.card__avatar { width: 140px; height: 140px; border-radius: 50%; object-fit: cover; border: 1px solid var(--line); margin-bottom: 14px; }
/* Leadership cards on desktop (owner, 2026-09-30): the photo fills the card with 50px either
   side (the card's 1px border and 26px padding plus 23px, so 46px off the content width) and
   the photo, name and job title are centred; the bio paragraph keeps its alignment. The phone
   block further down widens the photo to 40px margins and centres the whole card. */
.card__avatar { display: block; width: calc(100% - 46px); height: auto; aspect-ratio: 1; margin: 0 auto 14px; }
.card:has(> .card__avatar) > h3,
.card:has(> .card__avatar) > h3 + p { text-align: center; }

/* ---- person card (team) ------------------------------------------------ */
.person { text-align: center; }
.person .ph { border-radius: var(--radius); margin-bottom: 14px; }
.person__name { font-family: var(--font-head); font-weight: 600; font-size: 1.2rem; }
.person__role { font-size: 1.125rem; color: var(--muted); }

/* ---- icon card / logo grid --------------------------------------------- */
.icon-card { background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); padding: 24px; }
.icon-card__glyph { width: 46px; height: 46px; border-radius: 12px; background: var(--band); display: grid; place-items: center; margin-bottom: 14px; font-family: var(--font-head); font-weight: 700; color: var(--coral); }
.logo-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 16px; }
.logo-grid .ph { aspect-ratio: 5 / 2; }

/* ---- orbit graphic ------------------------------------------------------
   A ring of tiles around a centre mark. Positioned with individual angles
   rather than a transform loop so each tile can be labelled in the HTML. */
.orbit { position: relative; aspect-ratio: 1 / 1; max-width: 420px; margin: 0 auto; border-radius: 999px; border: 1px dashed var(--line); }
.orbit::after { content: ""; position: absolute; inset: 18%; border-radius: 999px; border: 1px dashed var(--line); }
.orbit__hub { position: absolute; inset: 34%; border-radius: 999px; background: var(--coral); color: #fff; display: grid; place-items: center; font-family: var(--font-head); font-weight: 700; text-align: center; padding: 8px; }
.orbit__node { position: absolute; top: 50%; left: 50%; width: 92px; margin: -30px 0 0 -46px; padding: 8px 4px; border-radius: 12px; background: var(--card); border: 1px solid var(--line); text-align: center; font-size: 1.125rem; transform: rotate(var(--a)) translate(190px) rotate(calc(-1 * var(--a))); }

/* ---- pricing tier (template style, distinct from the app-matched .plans) */
.tier { background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); padding: 26px; display: flex; flex-direction: column; gap: 14px; height: 100%; }
.tier--featured { border-color: var(--coral); box-shadow: var(--shadow); }
.tier__price { font-family: var(--font-head); font-size: 2.4rem; font-weight: 700; line-height: 1; }
.tier__per { font-size: 1.125rem; color: var(--muted); }
.tier__list { list-style: none; padding: 0; margin: 0; display: grid; gap: 10px; }
.tier__list li { padding-left: 26px; position: relative; }
.tier__list li::before { content: "\2713"; position: absolute; left: 0; color: var(--green); font-weight: 700; }

/* ---- contact form (visual only, matching the live contact page) --------- */
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.field { display: grid; gap: 6px; }
.field--wide { grid-column: 1 / -1; }
.field label { font-size: 1.125rem; font-weight: 600; }
.field input, .field textarea, .field select {
  font: inherit;
  font-size: 1.125rem;
  padding: 12px 14px;
  border: 2px solid var(--line);
  border-radius: 12px;
  background: var(--field-bg);
  color: var(--ink);
  width: 100%;
}
.field textarea { min-height: 130px; resize: vertical; }

/* ---- info list (addresses, hours, offices) ------------------------------ */
.info-list { display: grid; gap: 18px; }
.info-list__item { display: grid; gap: 4px; }
.info-list__label { font-family: var(--font-head); font-weight: 600; }
.info-list__value { color: var(--muted); }

/* ---- stat row ----------------------------------------------------------- */
.stat-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 22px; }
.stat { text-align: center; }
.stat__num { display: block; font-family: var(--font-head); font-size: clamp(1.9rem, 3.4vw, 2.6rem); font-weight: 700; color: var(--coral); line-height: 1.1; }
.stat__label { font-size: 1.125rem; color: var(--muted); }

/* ---- hover states -------------------------------------------------------
   The template hung a hover interaction on nearly every card (127 of them
   in its interaction data): lift, scale, border swap. Rebuilt as plain CSS.

   ORDER IS LOAD-BEARING, same as the entrance rules: this block declares
   the ONE unified `transition` for hover-able components and must come
   AFTER `.will-reveal`, or the entrance's transition (which does not cover
   box-shadow and border-color) wins and hovers snap. Opacity keeps the
   entrance's 0.55s; everything else runs at hover speed. */
.card--tool, .icon-card, .quote-card, .tier, .person, .logo-grid .ph {
  transition: opacity 0.55s ease, transform 0.3s ease, box-shadow 0.3s ease, border-color 0.3s ease;
}
.card--tool:hover, .icon-card:hover, .quote-card:hover, .tier:hover {
  transform: translateY(-6px);
  box-shadow: var(--shadow);
  border-color: var(--coral);
}
.person:hover { transform: translateY(-6px); }
.logo-grid .ph:hover { transform: scale(1.05); border-color: var(--coral); }
/* Room for the lift: the track clips at its padding edge, and a -6px
   translate with no headroom shears the card top off. */
.carousel__track { padding-top: 8px; }
@media (prefers-reduced-motion: reduce) {
  .card--tool, .icon-card, .quote-card, .tier, .person, .logo-grid .ph { transition: none; }
  .card--tool:hover, .icon-card:hover, .quote-card:hover, .tier:hover, .person:hover, .logo-grid .ph:hover { transform: none; box-shadow: none; }
}

/* ============================================================================
   INVERTED SECTIONS (owner, 2026-08-11)

   `inverted` is a class you put on a section's ROOT element. It flips that one
   section to its tonal opposite and travels with the markup, so it is both the
   thing the /sections toggle switches on and the thing an author orders when
   they say "use #32 inverted": paste section 32 with `inverted` appended to its
   root class list and it works on any page, no extra CSS.

   IT FLIPS BOTH WAYS. A normally light section goes charcoal. A section that is
   ALREADY dark (`.band--dark`, four of them) or coral (`.band--coral`, one) goes
   LIGHT, because inverting a dark band by darkening it is not an inversion. Same
   for the two dark islands nested inside light sections: the featured
   `.card--dark` and the `.calc__out` result tiles.

   HOW IT WORKS. Nearly every component here paints from the tokens at the top of
   this file, so the backbone is redefining those tokens on `.inverted` itself and
   letting the cascade do the rest. Below that sit two smaller groups: rules that
   hardcode a color literal (the token cascade cannot reach them), and the fill
   rule described further down. Everything is scoped under `.inverted`; with the
   class absent not one declaration in this block applies, which is the whole
   contract. Do not "simplify" any selector here by dropping that scope.

   THE NEUTRAL RAMP RUNS card ABOVE the ground and band BELOW it. Light mode
   orders card (#fff) > surface > band, so band reads recessed on a card and
   almost invisible on the page. Preserving that RELATIONSHIP, rather than
   mirroring the raw luminances, is what keeps a glyph tile looking like a tile
   and the .ph hatch looking like a hatch. It also matches the one piece of dark
   UI this stylesheet already shipped: `.card--dark .card__glyph` is a LIGHTER
   tile on a dark card (line 789), not a darker one.

   THE FILL RULE. --coral and --green are brightened here so they clear 4.5:1 as
   TEXT on charcoal, which is what they mostly are. That inverts the label
   problem on the handful of places they are a FILL under `color: #fff`: white on
   the brightened coral is 3.1:1. So every coral or green fill carries a charcoal
   label in inverted mode instead. Bright accent, dark label, which is how a dark
   theme reads anyway. Miss one and it fails silently, so they are all listed
   together below rather than filed with their components.
   ========================================================================= */

.inverted {
  /* Not brightened any more: #d17b63 until 2026-11-04, chosen to clear 4.5:1 as text on charcoal;
     the owner's one-orange rule (see :root) puts #CB5F48 here too, 3.9:1 on #232323. */
  --coral: #cb5f48;
  --coral-hover: #dd9179;
  --coral-soft: #d97e63;
  --green: #4cb85c;
  --ink: #f1f1f1;
  --btn-ink: #f1f1f1;
  /* Inverts with --btn-ink: the light-mode #6a6361 is under 3:1 on the charcoal ground. */
  --btn-outline: #f1f1f1;
  --body-text: #cfc9c4;
  --muted: #9e9895;
  --surface: #232323;
  --card: #2b2b2b;
  --band: #1b1b1b;
  --band-deep: #161616;
  --line: #3d3b39;
  --shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
  /* background-COLOR, never the `background` shorthand: the shorthand would also
     reset background-image, and section 31's root is `.hero hero--snug`, which
     carries the brand whirl plus three more background longhands (line 177).
     The paint is required rather than inherited because 34 of the 54 section
     roots declare no background at all and simply show the body through, and
     because redefining --surface here cannot reach `body` (line 47), which
     resolved its own background against :root long before this class existed. */
  background-color: var(--surface);
  color: var(--body-text);
}

/* ---- the light island --------------------------------------------------
   One idiom, four uses. Restoring the token set rather than patching colors
   one by one is what makes a nested placeholder work: .ph draws from --band,
   --surface, --line and --muted (line 741), so a light card whose tokens were
   left dark renders a dark hatch with a 2.4:1 caption inside it. */
.inverted.band--dark,
.inverted.band--coral,
.inverted .card--dark,
.inverted .calc__out div {
  --coral: #cb5f48;
  --coral-hover: #a8533e;
  --coral-soft: #cb5f48;
  --green: #349744;
  --ink: #1d1a18;
  --btn-ink: #232323;
  --btn-outline: #6a6361;
  --body-text: #3d3835;
  --muted: #6a6360;
  --surface: #edebe8;
  --card: #ffffff;
  --band: #e9e1de;
  --band-deep: #e7e0d9;
  --line: #e2dad3;
  --shadow: 0 10px 30px rgba(29, 26, 24, 0.08);
  background-color: var(--surface);
  color: var(--body-text);
}
/* .band--dark paints from --dark and states its own heading and body colors,
   so restoring tokens is not enough on its own. (0,2,0) and (0,2,1) beat the
   (0,1,0) and (0,1,1) of lines 266-267. */
.inverted.band--dark h2,
.inverted.band--dark h3,
.inverted .card--dark h3,
.inverted .card--download h2 {
  color: var(--ink);
}
/* The download card states its own heading and subline colours for the dark ground (#fff and
   the footer's #b9b2ac, line ~1921), so like .band--dark it needs them re-stated once the
   island flips it light: the tokens are re-set above, and --ink and --muted inside the island
   are #1d1a18 (14.55:1) and #6a6360 (4.95:1) on #edebe8. The black badges are right as they are. */
.inverted .card--download p {
  color: var(--muted);
}
.inverted .card--dark {
  border-color: var(--line);
}
.inverted .card--dark .card__glyph {
  background: var(--band);
}
/* Built for a dark ground (rgba-white border, #fff label), so on a band that has
   just flipped to light it is white on near-white. */
.inverted.band--dark .btn--ghost-light {
  border-color: var(--ink);
  color: var(--ink);
}
.inverted.band--dark .btn--ghost-light:hover {
  background: rgba(29, 26, 24, 0.08);
  color: var(--ink);
}
/* Section 27's stripe link. The markup used to carry `style="color:#fff;
   text-decoration:underline"`, which no stylesheet can outrank, so it moved to
   this class in site-sections.mjs. Both declarations are reproduced exactly:
   `a` is text-decoration: none (line 51), so dropping the underline here would
   change the un-inverted rendering, which is not allowed. */
.band--dark .band-link {
  color: #fff;
  text-decoration: underline;
}
/* --ink rather than --coral: the stripe has flipped to a light ground, and coral on that
   ground is 3.5:1, the same shortfall the light site's own links carry. The underline is
   what marks it as a link here, so ink costs nothing and clears AA at 14.5:1. */
.inverted.band--dark .band-link {
  color: var(--ink);
}
/* The coral band keeps its identity by inverting to a coral TINT with coral
   type, rather than to the plain page surface. In a page of charcoal sections a
   light CTA band is also the one that draws the eye, which is its job. */
.inverted.band--coral {
  background-color: #f7e9e4;
  color: #5c4038;
}
.inverted.band--coral h2 {
  color: #8f4231;
}
/* Light mode swaps this to a white fill with coral text (line 270). Inverted, it
   swaps back to the ordinary coral button, which on this tint is the emphatic
   one. Restated in full because line 270 would otherwise still apply. */
/* The deeper coral, not the base one: white on --coral is 4.17:1, which is the ratio the
   light site's own primary button has always had. A new rule may as well clear AA, and at
   this size the two corals are indistinguishable anyway. */
.inverted.band--coral .btn--primary {
  background: var(--coral-hover);
  color: #fff;
}
.inverted.band--coral .btn--primary:hover {
  background: #8f4231;
  color: #fff;
}

/* ---- the fill rule ------------------------------------------------------
   Every coral or green fill that carries `color: #fff` in light mode. Grouped
   on purpose: these fail as a class, not as individual components. */
.inverted .btn--primary,
.inverted .btn--primary:hover,
.inverted .step::before,
.inverted .orbit__hub,
.inverted .tabs__tab.is-active,
.inverted .accord__item[open] summary,
.inverted .accord__item[open] .accord__body,
.inverted .mini-stat--hero,
.inverted .mini-stat--hero .mini-stat__label,
.inverted .mini-stat--hero .mini-stat__icon {
  color: #232323;
}
/* The one exception to the rule above: .btn--outline is not a coral fill, it is
   an --ink fill, and --ink is now near-white. Its light-mode `color: #fff` hover
   label (line 117) would be white on white. */
.inverted .btn--outline:hover {
  color: var(--surface);
}

/* The result tiles flip to a light island, but lines 576-577 paint their figures #f1f1f1 at
   (0,1,1), which the island's inherited color cannot outrank. Left alone this is near-white
   type on a cream tile at 1.05:1, which is to say invisible. */
.inverted .calc__out strong {
  color: var(--ink);
}
.inverted .calc__out span {
  color: var(--muted);
}

/* ---- literals the token cascade cannot reach ---------------------------- */
/* A near-white zebra stripe (line 430) blazing on a dark table. */
.inverted table.plain tbody tr:nth-child(even) {
  background: #232323;
}
/* Hardcoded light field chrome (lines 569-571). */
.inverted .calc input[type="number"] {
  border-color: var(--line);
  /* The light field on a dark band too (owner, 2026-10-10: every form field is --field-bg), so the
     ink is the dark one, not this band's inverted --ink. */
  background: var(--field-bg);
  color: #1d1a18;
}
/* No placeholder rule exists in light mode and none is added there: a browser's
   default placeholder grey is fine on white and unreadable on charcoal. */
.inverted .field input::placeholder,
.inverted .field textarea::placeholder,
.inverted .calc input::placeholder {
  color: var(--muted);
}
/* --line is a border value; as the only paint on a 10px control it is too close
   to the ground to read as a dot at all. */
.inverted .carousel__dot {
  background: #5c5754;
}
/* opacity 0.4 removes far more of a control on charcoal than it does on white. */
.inverted .carousel__btn:disabled {
  opacity: 0.55;
}
/* Hardcoded thumb (line 456) against a --band track that inverts on its own. */
.inverted .plans::-webkit-scrollbar-thumb {
  background: #57534e;
}

/* ---- the pricing plan cards --------------------------------------------
   These deliberately carry the APP's palette rather than this file's tokens
   (see docs/DESIGN-SYSTEM.md), so none of the ramp above reaches them and every
   value is restated. Inverted, they move to the app's dark neutrals. The belt
   accent colors are identity and do not move, with one unavoidable exception:
   the black belt's #111827 is invisible on a #262626 card, so it lifts to the
   slate that reads as the darkest of the set without disappearing into the card.
   Only the white, yellow and green belts are built in the catalog (section 45),
   so the blue and black rules here cannot be eyeballed on /sections. */
.inverted .plan {
  background: #262626;
  border-color: #3f3f3f;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}
.inverted .plan__head h2,
.inverted .plan__price {
  color: #fafafa;
}
.inverted .plan__tag {
  color: #a3a3a3;
}
.inverted .plan__facts {
  color: #d4d4d4;
}
.inverted .plan__facts .check::before {
  color: #22c55e;
}
.inverted .plan hr {
  border-top-color: #3f3f3f;
}
.inverted .plan--black .plan__strip {
  background: #4b5563;
}
.inverted .plan--white .plan__pill {
  background: #3f3f46;
  color: #fafafa;
  border-color: #52525b;
}
.inverted .plan--white .plan__pill:hover {
  background: #52525b;
  color: #fafafa;
}
.inverted .plan--black .plan__pill {
  background: #e5e7eb;
  color: #111827;
}
.inverted .plan--black .plan__pill:hover {
  background: #f3f4f6;
  color: #111827;
}

/* ============================================================================
   THE INVERTED-COLORS SWITCH  (/sections only)

   Two separate marks, and keeping them separate is the point. `inverted` goes on
   each section root and is the reusable thing an author pastes. `catalog-inverted`
   goes on <body> and restyles the numbered strips, which are catalog furniture
   and must NEVER travel with a pasted section. The owner scoped the flip to the
   sections plus those strips and nothing else, so the header, footer, contents
   list and the meta lines under each strip stay light on purpose.
   ========================================================================= */

/* The strip is a sibling of the stage, not a descendant of any section, which is
   why it cannot ride on `.inverted`. It flips to the light card surface so a dark
   strip never butts against a dark section with no seam between them. */
body.catalog-inverted .sec-bar {
  background: var(--card);
  color: var(--ink);
  border-bottom: 1px solid var(--line);
}
body.catalog-inverted .sec-bar__group,
body.catalog-inverted .sec-bar__anchor {
  color: var(--muted);
}

/* Hidden until site.js says otherwise, the same contract as [data-tabs-ready]
   (line 899): a control that does nothing without JS should not be offered.
   Nothing else on the page is hidden this way, because nothing else is inert. */
.sec-flip {
  display: none;
}
[data-invert-ready] .sec-flip {
  display: block;
  margin: 22px 0 0;
}
.sec-flip__switch {
  display: inline-flex;
  align-items: center;
  gap: 16px;
  padding: 10px 22px 10px 10px;
  background: var(--card);
  border: 2px solid var(--line);
  border-radius: 999px;
  cursor: pointer;
  font-family: var(--font-head);
  font-size: 1.2rem;
  font-weight: 600;
  color: var(--ink);
  text-align: left;
}
.sec-flip__switch:hover {
  border-color: var(--coral);
}
.sec-flip__switch:focus-visible {
  outline: 2px solid var(--coral);
  outline-offset: 3px;
}
.sec-flip__track {
  position: relative;
  flex: none;
  width: 78px;
  height: 44px;
  border-radius: 999px;
  background: var(--band);
  border: 2px solid var(--line);
  transition: background 0.2s ease, border-color 0.2s ease;
}
.sec-flip__knob {
  position: absolute;
  top: 4px;
  left: 4px;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: var(--muted);
  transition: transform 0.2s ease, background 0.2s ease;
}
.sec-flip__switch[aria-checked="true"] .sec-flip__track {
  background: var(--coral);
  border-color: var(--coral);
}
.sec-flip__switch[aria-checked="true"] .sec-flip__knob {
  background: #fff;
  transform: translateX(34px);
}
@media (prefers-reduced-motion: reduce) {
  .sec-flip__track,
  .sec-flip__knob {
    transition: none;
  }
}

/* ------------------------------------------------------------ responsive */
@media (max-width: 1000px) {
  .gallery { grid-template-columns: repeat(2, 1fr); }
  .cards--4 { grid-template-columns: repeat(2, 1fr); }
  .calc__grid { grid-template-columns: repeat(2, 1fr); }
  .site-footer__grid { grid-template-columns: 1fr 1fr; }
  .site-footer__col--pull { margin-left: 0; }
  .sec-toc { grid-template-columns: repeat(2, 1fr); }
  .split { gap: 32px; }
  .logo-grid { grid-template-columns: repeat(3, 1fr); }
  .stat-row { grid-template-columns: repeat(2, 1fr); }
  .orbit__node { transform: rotate(var(--a)) translate(150px) rotate(calc(-1 * var(--a))); }
  /* Two across; one gap, so 22px rather than 44px. */
  .carousel__track { grid-auto-columns: calc((100% - 22px) / 2); }
  /* The home flow drops from four across to two here (the handoff suggests two below about
     900px and one below about 560px; the site's 1000 and 850 stand in, since no new width
     query is allowed). Four items make two full rows, no orphan. */
  /* The hero trust row at tablet widths: three equal thirds are ~267px here, narrower than
     "No credit card required" at 21.2px, and the 70px insets pushed all three items onto two
     lines. Content-sized tracks with the spare width shared between them fit on one line, so
     the desktop arrangement (thirds, centred middle, inset ends) is above 1000px only. The
     850px block below stacks it. */
  /* Same shape as the base rule since 2026-09-19; kept as the explicit statement of what this
     width needs, so a later change to the base cannot silently take it away. */
  .trust-cols { grid-template-columns: auto auto auto; justify-content: space-between; }
  .trust-cols li:first-child { margin-left: 0; }
  .trust-cols li:nth-child(3) { margin-right: 0; }
  /* Two across. No per-column padding rules any more: each step's padding is its own and the
     step edges do the aligning. */
  .flow { grid-template-columns: repeat(2, minmax(0, 1fr)); row-gap: 10px; }
  /* Two across the columns are wide enough for a 280px phone (2026-11-01). */
  .flow { --flow-phone-max: 280px; }
  /* Two across makes steps 1 and 3 the LEFT column, so step 3 must lose the arrow it gets from
     following step 2; only the right-hand column (2 and 4) keeps one. */
  .flow__step:nth-child(odd)::after { display: none; }
  /* Two across, every phone has slack in its column, so all of them enlarge from the centre. */
  .flow__step:nth-child(n) .flow__phone img { transform-origin: 50% 50%; }
  /* Two across: the left column is the odd children, the right column the even ones. */
  .flow__step:nth-child(odd) { padding-left: 0; padding-right: 20px; }
  .flow__step:nth-child(even) { padding-left: 20px; padding-right: 0; }
  /* The arrow's midpoint inputs to match (see `.flow__step + .flow__step::after`): only the even
     steps draw one, and each follows an odd step. */
  .flow__step:nth-child(even)::after { --arrow-pl: 20px; --arrow-pr: 0px; --arrow-ppl: 0px; --arrow-ppr: 20px; }
}
@media (max-width: 850px) {
  /* The two-column Sign Up stacks, promo first, at the same breakpoint the drawer uses. The
     app's own comment naming 760 is stale; 850 is what it ships, and 850 is already one of this
     site's five approved width queries, so no sixth is introduced. */
  .login-dialog__cols {
    grid-template-columns: 1fr;
  }
  /* Stacked, the dark promo column is the modal's top, so its X shows (owner, 2026-10-06). */
  .login-dialog__promo-top .login-dialog__x { display: inline-flex; }

  /* The download card stacks (owner, 2026-09-19): copy first, badges beneath, both centred,
     on 24px padding all round instead of the desktop's 27 x 32. WIDTH at a 320px screen: 320
     less the container's two 24px gutters, the card's two 24px paddings and its two 1px
     borders leaves 222px inside the card. A badge at the clamp's 52px floor is
     52 x 840/252 = 173.33px wide, so each fits with 49px to spare, and the pair (2 x 173.33 +
     16 = 362.67px) wraps to two centred rows through `.store-badges`' own `flex-wrap: wrap`
     rather than overflowing: no page-level sideways scroll at any width. The pair sits on one
     row wherever the card's inside is at least 442.67px, which is a viewport of 541px and up;
     below that it is always two rows, because the badges hold 64px down to 400px of viewport
     and the clamp's 16vw slope below that shrinks them more slowly than the viewport shrinks
     the card. */
  .card--download { flex-direction: column; align-items: stretch; text-align: center; gap: 20px; padding: 31px 24px; }
  /* The heading 2px larger with 20px under it on the phone (owner, 2026-09-30, the space as 10
     then 20): the desktop's 26px and 4px margin become 28 and 20. One class, so every page's
     card follows. */
  /* Line-height 10px taller than the 32.2px the h2's 1.15 gives at 28 (owner, 2026-09-30, as
     4, 4, then 2). */
  .card--download h2 { font-size: 28px; margin-bottom: 20px; line-height: 42.2px; }
  /* Its subline 4px larger on the phone (owner, 2026-09-30): 18px to 22. line-height stays
     the desktop's 1.4 ratio. */
  .card--download p { font-size: 22px; }
  /* Less space above the download card on every page under /platforms (owner, 2026-09-30):
     40px less, then 20, 15, 5, 10 and 3 more on the four guides. page() tags that section
     `download-band--platforms` there; the pull is a negative top margin so the section before
     it is left alone. The guides follow `.guide` (72px bottom padding, so 102px of air became
     9); the /platforms index follows a plain 30px section, where more would have put the card
     flush against the text above, so it keeps the 40 (60px of air became 20). */
  .download-band--platforms { margin-top: -40px; }
  .guide + .download-band--platforms { margin-top: -93px; }
  /* BODY TEXT ON THE PHONE, ONE STYLE (owner, 2026-09-30): first "2px larger everywhere", then
     "use a consistent text style for body text, with the text under the homepage H1 as the
     baseline", then "make it 20px". That line is `.lead`: Rethink Sans 400 on a 1.65
     line-height in --body-text, and the size is the owner's 20px, the lead included (it was
     1.2rem + 2 = 21.2). Measured across every page at 390px, all other body text is sized by
     this short list, so each rule is restated with its own selector at 20px:
     body (inherited by nearly everything), .site-footer, .consent-bar, .field label,
     .flow__body p, .ph and the screenshot figcaptions. `body`'s 1.65 line-height already
     matches; the step descriptions' 1.6 is brought up to it here and the guide pages' callout
     and Q&A paragraphs in the guide block further down. Six /about and /contact paragraphs
     carried an inline 1.125rem restating the body size; it came out of site-pages-2.mjs so
     they inherit.
     NOT moved: the approved sub-floor lines (footer credits, legal line and tagline, the drawer
     copyright), and every heading, button and label set in Inter Tight or JetBrains Mono.
     DIALOGS ARE PINNED at the 18px they inherited: the auth modals must stay identical to the
     app's (D-060), and a few of their lines inherit `body`, so without this they would grow
     with the page. The sales, calculator and sign-out dialogs keep 18px with them. */
  body { font-size: 20px; }
  dialog { font-size: 18px; }
  .site-footer { font-size: 20px; }
  .consent-bar { font-size: 20px; }
  .field label { font-size: 20px; }
  .flow__body p { font-size: 20px; line-height: 1.4444; }
  .ph { font-size: 20px; }
  figure.shot-fig figcaption { font-size: 20px; }
  .lead { font-size: 20px; }
  .trust-cols { font-size: 23.2px; }
  .store-badges--download { justify-content: center; }
  /* The download card's badges fill the card on the phone, 20px in from each edge (owner,
     2026-09-30): one per row at full width instead of height-sized. The card's side padding is
     24px plus a 1px border, so the pair pulls out 5px a side to land 20px from the card's outer
     edge; the text above keeps its 25. Width drives the size here, height follows the badges'
     840:252, and the art itself is untouched (the programmes' rules, see `.store-badges`). The
     840px sources are 2.8x a 300px badge, so they stay sharp on a 3x phone at this size. Every
     page's card is the same component, so every page follows. */
  .store-badges--download { flex-direction: column; align-items: stretch; margin: 0 -5px; }
  .store-badges--download .store-badges__link img { width: 100%; height: auto; }
  /* Margin round each badge (owner, 2026-09-30): 10px, then 5 off the top, so 5px above and
     10px on the other three sides. 30px from the card's sides, 31px between the two (the 16px
     gap plus 10 + 5), 5px more above the pair and 10px more below it. */
  .store-badges--download .store-badges__link { margin: 5px 10px 10px; }
  /* 15% smaller on the phone (owner, 2026-09-30): each badge is 85% of the width it had (the row
     less its 20px of side margin), centred in the card, the height following through the badges'
     840:252. */
  .store-badges--download { align-items: center; }
  .store-badges--download .store-badges__link { width: calc((100% - 20px) * 0.85); }

  section { padding: 30px 0; }
  /* This block sets `padding` directly rather than the token, so it outranks :root and has to
     carry the same 30px or mobile springs back to a gap looser than the desktop it compacts.
     It read 52px with two page-scoped rules re-tightening it to 30; all three are gone now
     that 30 is the default. `.hero` keeps its own 44px just below. */
  .hero { padding: 44px 0; }
  /* The home hero's H1 sits 30px higher on the phone (owner, 2026-09-30): 44 less 30. Home
     only; every other page's hero keeps the 44 above. */
  .hero--home { padding-top: 14px; }
  /* ...and 12px less under its buttons (owner, 2026-09-30, as 8 then 4): the hero's 44px bottom
     padding to 32. */
  .hero--home { padding-bottom: 32px; }
  /* Phone order (owner, 2026-09-30): H1, then the animated devices, then the lead and the
     buttons. The three copy elements are siblings of the image only through `display: contents`
     on their wrapper, so each can take its own place in the grid; the source order is
     unchanged (H1 first, then lead and buttons, then the image), so a screen reader reads the
     copy straight through. The grid's own gap would now fall between every pair, so it is
     zeroed and the spacing is the elements' own margins: the H1's 20px under it (below), 20px
     under the image, and the lead's existing 28px above the buttons. */
  .hero--home .hero__copy { display: contents; }
  .hero--home h1 { order: 0; }
  .hero--home .hero__media { order: 1; margin-bottom: 20px; }
  .hero--home .hero__copy .lead { order: 2; }
  .hero--home .hero__copy .btn-row { order: 3; }
  .hero--home .hero__grid { row-gap: 0; }
  /* 4px less under the home H1 on the phone (owner, 2026-09-30): the h1's 24px margin to 20.
     Centred on the phone (owner, 2026-11-14); the lead and buttons below keep their alignment. */
  .hero--home h1 { margin-bottom: 20px; text-align: center; }
  /* 8px more between the lead and the buttons (owner, 2026-09-30): the lead's 20px margin to 28. */
  .hero--home .lead { margin-bottom: 28px; }
  /* /about on the phone (owner, 2026-09-30): 30px less under the H1, then 15px more. The gap was
     the head section's 30px bottom padding plus the next section's own 30px (the H1's margin is
     0 here), so the head's goes to 0, and a -15px bottom margin pulls the next section up by the
     further 15. The shared section padding is untouched. */
  section.hero.about-head { padding-bottom: 0; margin-bottom: -15px; }
  section.about-intro { padding-top: 30px; padding-bottom: 30px; }
  /* "Leadership" on the phone: 5px more under it, then 15 more (owner, 2026-09-30), its 13px
     margin to 33. It is the only `.page-subtitle` on the site. */
  .page-subtitle { margin-bottom: 33px; }
  /* The leadership cards on the phone (owner, 2026-09-30): photo, name and title centred, the
     photo wholly inside the card with 40px either side (the card's 1px border and 26px padding
     plus 13px, hence the 26px off the content width), and the title line 2px larger. Scoped to
     the cards that carry a photo, which are the only ones on the site. */
  .card:has(> .card__avatar) { text-align: center; }
  .card__avatar { display: block; width: calc(100% - 26px); height: auto; aspect-ratio: 1; margin: 0 auto 14px; }
  .card:has(> .card__avatar) > h3 + p { font-size: calc(1em + 2px); }
  /* "Works with your platform." 20px smaller on the phone (owner, 2026-09-30, as 8, 8, then 4):
     the band's 60px becomes 40. Line-height 4px taller than the 46px the h2's 1.15 gives at
     40 (owner, 2026-09-30, as 2 then 2). The home page is the band's only instance. */
  .hero--platforms h2 { font-size: 40px; line-height: 50px; }
  /* "Get your time back." 12px smaller on the phone (owner, 2026-09-30, as 8 then 4): 60px
     becomes 48. The closing band is on every page, so every page follows. The -10px sits it
     10px closer to the band's top edge (owner, same day, as 5 then 5): the band's 95px phone
     padding is `.band--coral`'s, which /sections' catalog specimen shares, so the heading moves
     rather than the band. 34px under it, the h2's 24 plus 10 (owner, same day). 40px of side
     padding narrows its measure inside the container's 24px gutters (owner, same day, as 10,
     20, 35, 30, then 40). */
  .cta-band__title { font-size: 48px; margin-top: -10px; margin-bottom: 34px; padding: 0 40px; }
  /* The brand whirl is back on the phone (owner, 2026-10-01), behind the devices. It was dropped
     on 2026-08-14 because the desktop rule parks it at the hero's centre plus a right-and-down
     nudge, which pushed it off a narrow canvas, and that background still belongs to the hero,
     so it stays off here. Instead the same artwork hangs off the devices picture itself and is
     sized and placed RELATIVE to it, in the proportions the desktop has: measured at 1280px the
     whirl is 1.77x the devices' width and 0.19 of their height below their centre, which here is
     a 177%-wide box shifted +13% of its own height. Horizontally it is centred on the devices and
     then moved 50px to the left and 8px up (owner, 2026-10-01, as 35, 5 more, then 10 more left), because the artwork's weight is not in
     the middle of its box and a physically centred swoosh does not LOOK centred; the desktop's
     proportional 0.32 offset is not carried over. It
     paints behind the picture (negative z-index inside the hero's own stacking context, so it
     can never drop behind the page) and the hero's `overflow: hidden` clips what spills past the
     screen. Decorative: an empty content box, no alt to announce. */
  .hero--snug { background-image: none; }
  .hero--home { isolation: isolate; }
  .hero--home .hero-mock-pic { position: relative; }
  .hero--home .hero-mock-pic::before {
    content: "";
    position: absolute;
    z-index: -1;
    left: 50%;
    top: calc(50% + 10px);
    width: 177%;
    aspect-ratio: 1000 / 520;
    background: url("/images/hero-whirl.webp") center / 100% 100% no-repeat;
    transform: translate(calc(-50% - 50px), calc(-50% + 13% - 8px));
    pointer-events: none;
  }
  .hero__grid { grid-template-columns: 1fr; gap: 34px; }
  /* One column here, so the image already spans the full container and there is
     no side room for the desktop overspill: 115% would just be clipped by
     `.hero`'s overflow. Back to its own width. */
  .hero-mock { width: 100%; max-width: 100%; margin-left: 0; }
  /* One card per row on the phone. The row gap matches the column gap above, so stacked cards
     sit the same distance apart as they do side by side. */
  .flow { grid-template-columns: 1fr; row-gap: 10px; }
  /* Icon, stopwatch-and-title row and description all centred on the phone (owner,
     2026-09-30); it had been the handoff's centred icon above left-aligned text. The title
     row's centring moved to the base rule on 2026-11-01, when the desktop took it too. */
  .flow__body { text-align: center; }
  /* ...except the description in all four steps, which reads left-aligned under the centred
     icon and title (owner, 2026-09-30), inset 15px from each side of the container. */
  .flow__body p { text-align: left; margin-left: 15px; margin-right: 15px; }
  /* THE HOME CARDS' PICTURES on the phone (owner, 2026-09-30): the five card illustrations and
     the calculator, centred on their own row and as wide as the card less 30px a side. The card
     is 1px border + 26px padding, so 3px more inside the content box lands 30px from its outer
     edge: width calc(100% - 6px), centred by auto margins. Height follows each picture's own
     ratio. The calculator's "Try Me!" cue keeps its desktop proportion to the calculator
     beneath it (96px over a 127px-wide calculator = 75.5%, 6px in = 4.7%), so the pair scales
     as one. The card SVGs stay sharp at any size; the two WebPs (calculator 391px, cue 300px
     wide) are soft only on a 3x screen. */
  .card__visual { width: calc(100% - 6px); height: auto; max-width: none; margin: 0 auto; }
  /* 8px more below the calculator (owner, 2026-09-30, as 10 then 2 back). */
  .calc-open { width: calc(100% - 6px); margin: 0 auto 8px; top: 0; }
  .calc-open__img { width: 100%; height: auto; }
  .calc-open__cue { width: 75.5%; margin-left: 4.7%; }
  /* The calculator card on the phone: "Calculate savings" moves BELOW the calculator picture,
     centred (owner, 2026-09-30). The link lives inside `.calc-card__text` with the title and
     paragraph, so that wrapper steps aside (`display: contents`) and the row becomes one
     column whose items can be ordered: title, paragraph, calculator, link. The row's 16px gap
     goes to 0 so the title-to-paragraph 12px and the link's own 14px top margin stay what they
     were, and the calculator keeps the 20px it had below the link as its top margin now.
     Visual order only: both controls open the same dialog, and the source keeps the link
     first. */
  .calc-card__row { flex-direction: column; align-items: stretch; gap: 0; }
  .calc-card__text { display: contents; }
  .card--calc .calc-card__link { order: 2; align-self: center; }
  .card--calc .calc-open { order: 1; margin-top: 20px; }
  /* The stopwatch only: 20px less above and below it (owner, 2026-09-30). Its drawing sits in
     the middle of the shared 114x164 frame, so at full card width it carried the most empty
     space of the five; the negative margins let the card close up round the art. */
  /* ...and 2px less again below it (owner, same day): -22. */
  .card__visual--save-time-stopwatch { margin-top: -20px; margin-bottom: -22px; }
  /* The calendar: 30px less above it and 20px less below (owner, 2026-09-30), for the same
     reason. */
  .card__visual--expand-business-calendar { margin-top: -30px; margin-bottom: -20px; }
  /* The sample lot card: 8px more below it (owner, 2026-09-30). */
  .card__visual--sample-lot-card { margin-bottom: 8px; }
  /* The money jar: 10px more below it (owner, 2026-09-30, as 4, 4, then 2). */
  .card__visual--save-money-jar { margin-bottom: 10px; }
  /* Card titles 8px larger on the phone (owner, 2026-09-30): every `.card h3` takes the global
     h3's 24px, so this one rule moves them all to 32: the home page's benefit and calculator
     cards, the /about team names and the /platforms cards. Section headings that happen to sit
     in a card (the h2s on /contact, /compatibility and the guides) are not card titles and keep
     their sizes. */
  .card h3 { font-size: 32px; }
  /* The small icon beside each card title sits at the TOP of the title on the phone (owner,
     2026-09-30), not centred on the whole block, which put it between the lines of any title
     that wraps. This is the calculator card's own desktop rule (see `.card--calc .card__head`)
     applied to every card here: the icon is pinned to the top, and the title is offset so the
     icon is centred on its FIRST line, half of what is left of the 33px icon after one line
     box (about -2px at 32px). */
  .card__head { align-items: flex-start; }
  .card__head h3 { margin-top: -2px; margin-top: calc((33px - 1lh) / 2); }
  /* The phone on the phone (2026-11-01): 300px wide, centred by the step's `align-items`,
     24px above the title row. The 2026-09-30 rules this replaces sized the ICON to 90% of the
     container and pulled each of the four up by its own negative margin to close the
     transparent padding the illustrations carried; the phone pictures carry none, so every
     per-step offset went with them. The step keeps 10px of top padding (26 on the desktop, 0
     in the 2026-09-30 rules) so a phone does not sit flush under the description above it. */
  .flow__phone { max-width: 300px; margin: 0 auto 24px; }
  /* Stacked, there is no gap between phones for an arrow. */
  .flow__step::after { display: none; }
  /* The icon up to the handoff's 56px beside the 34px titles below. */
  .flow__head img { width: 56px; height: 56px; }
  /* No hover lift on the phone (owner, 2026-09-30, for the icon that grew then): a tap sets
     :hover on touch screens, so it swelled when tapped and stayed swollen. */
  .flow__step:hover .flow__phone img,
  .flow__step.is-zoomed .flow__phone img { transform: none; }
  .flow__phone,
  .flow__step.is-zoomed .flow__phone { cursor: auto; }
  .flow__step.is-zoomed,
  .flow__step.is-unzooming { z-index: auto; }
  .flow__step { padding-top: 10px; }
  /* 20px less under the last step's description (owner, 2026-09-30, as 15 then 5): that step's
     26px bottom padding to 6. The flow's own 26px and the next section's 30px are unchanged. */
  .flow__step:last-child { padding-bottom: 6px; }
  /* The step titles 12px larger on the phone (owner, 2026-09-30, as 6, 2, then 4): the
     desktop's 22 plus 12. */
  .flow__head h3 { font-size: 34px; }
  /* One across: every step is both the first and the last in its row, so neither side keeps
     the between-columns padding. Restated at nth-child specificity so it beats the 1000px
     block's pair above rather than losing to them. */
  .flow__step:nth-child(odd),
  .flow__step:nth-child(even) { padding-left: 0; padding-right: 0; }
  .trust-cols { grid-template-columns: 1fr; gap: 18px; }
  /* Stacked, every item reads from the left; the centred and right-aligned thirds above
     are a three-across arrangement only. */
  .trust-cols li { justify-self: start; }
  .trust-cols li:first-child { margin-left: 0; }
  .trust-cols li:nth-child(3) { margin-right: 0; }
  /* Left slide-out drawer, mirroring the app's mobile menu (owner,
     2026-08-09): scrim + sliding panel with the logo up top, roomy links,
     a full-width Log In pill, and a centered copyright pinned at the foot.
     The header's backdrop-filter must go on mobile: a backdrop-filter makes
     the header a CONTAINING BLOCK for fixed descendants, which trapped the
     drawer and scrim inside the header bar instead of the viewport. */
  .site-header { backdrop-filter: none; -webkit-backdrop-filter: none; }
  .drawer-overlay {
    display: block;
    position: fixed;
    inset: 0;
    background: rgba(29, 26, 24, 0.45);
    /* Safe here, unlike on .site-header: the panel is a SIBLING of the scrim,
       not a descendant, so this containing block traps nothing. */
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.28s ease;
    z-index: 80;
  }
  .drawer-overlay.is-open { opacity: 1; pointer-events: auto; }
  .site-nav {
    display: flex;
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    width: min(82vw, 340px);
    background: var(--surface);
    flex-direction: column;
    align-items: flex-start;
    padding: 20px 24px calc(20px + env(safe-area-inset-bottom));
    gap: 26px;
    margin: 0;
    z-index: 90;
    transform: translateX(-100%);
    transition: transform 0.28s ease;
    box-shadow: 0 0 40px rgba(29, 26, 24, 0.3);
    overflow-y: auto;
  }
  .site-nav.is-open { transform: translateX(0); }
  /* 1.325rem = 21.2px: the 1.2rem (19.2px) this carried until 2026-09-21 plus the 2px the owner
     added to the bar above, so the drawer keeps its slight lead over the desktop row rather than
     ending up smaller than it. Kept in rem because that is the unit it was written in. */
  .site-nav a { font-size: 1.325rem; }
  .nav-close {
    display: block;
    position: absolute;
    top: 18px;
    right: 18px;
    padding: 6px;
    background: none;
    border: 0;
    line-height: 0;
    color: var(--ink);
    cursor: pointer;
  }
  .site-nav__brand { display: block; margin-bottom: 6px; }
  .site-nav__brand img { height: 38px; width: auto; max-width: none; }
  /* Spans the drawer, but sits at the 18px text floor with tighter padding:
     the drawer's own 1.2rem link size would otherwise apply here too.
     The colors must be restated, not just inherited from .btn--primary:
     `.site-nav a` (0,1,1) outranks `.btn--primary` (0,1,0), so the label came
     out dark ink on coral, and the nav link hover turned it coral on coral. */
  .site-nav .site-nav__login,
  .site-nav .site-nav__catalogs {
    display: inline-flex;
    width: 100%;
    font-size: 1.125rem;
    border-radius: 999px;
    padding: 9px 22px;
    margin-top: 12px;
    background: var(--coral);
    color: #fff;
  }
  .site-nav .site-nav__login:hover,
  .site-nav .site-nav__catalogs:hover {
    background: var(--coral-hover);
    color: #fff;
  }
  /* Signed in, the drawer opens on the visitor (owner, 2026-10-23, from a mockup): the avatar
     and Profile, then Manage Catalogs as the same full-width pill Log In is, in the logo's place
     at the top. The site's links follow, then a rule, then the account menu's rows as plain
     drawer links. The identity stops 32px short of the content edge, the close X's tap box,
     so a long name wraps instead of running under it. The rule is #B6B6B6, not --line (owner,
     2026-10-23; #6A6A6A was tried first), darker than the site's other dividers. */
  .site-header.is-signed-in .site-nav__brand { display: none; }
  .site-nav__account-head {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 20px;
    width: 100%;
  }
  .site-nav__account-head .site-nav__identity { max-width: calc(100% - 32px); }
  .site-nav__account-head .account__who { min-width: 0; }
  .site-nav__account {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 20px;
    width: 100%;
    padding-top: 24px;
    border-top: 1px solid #b6b6b6;
  }
  .site-nav .site-nav__catalogs { margin-top: 0; }
  .site-nav__account-link { display: inline-flex; align-items: center; gap: 12px; }
  .site-nav__identity { font-weight: 600; }
  /* The drawer stacks the name over the email too, and truncates the email to the drawer. */
  .site-nav__identity .account__email { max-width: 220px; }
  /* Sign Out in the drawer is a <button> dressed as a drawer link, in the same error red. */
  button.site-nav__account-link {
    padding: 0;
    border: 0;
    background: none;
    font-family: var(--font-head);
    font-weight: 500;
    font-size: 1.2rem;
    cursor: pointer;
  }
  .site-nav__account-link--danger,
  .site-nav__account-link--danger:hover { color: #b3261e; }
  /* 12px is a deliberate, owner-approved exception to the site's 18px text
     floor (2026-08-09). It applies to this legal line only. */
  .site-nav__foot {
    display: block;
    margin-top: auto;
    width: 100%;
    text-align: center;
    font-size: 12px;
    color: var(--muted);
  }
  @media (prefers-reduced-motion: reduce) {
    .site-nav, .drawer-overlay { transition: none; }
  }
  /* Sits 3px above the header's centre line (owner, 2026-11-04: "Move mobile menu hamburger
     icon button up 3px."). A transform, so the header row's own layout does not move. */
  .nav-toggle { display: block; transform: translateY(-3px); }
  /* With the nav collapsed, nothing pushes the actions right; do it here. */
  .site-header__actions { margin-left: auto; }
  .site-header__cta { display: none; }
  .site-header__login { display: none; }
  /* The drawer carries the signed-in links on a phone, as it carries Log In. */
  .account { display: none; }
  /* The fluid signed-in header row stops here: restate the drawer layout's own values,
     which the three-class is-signed-in selectors above would otherwise override. */
  .site-header.is-signed-in .site-header__inner { gap: 14px; }
  .site-header.is-signed-in .site-header__logo img { height: 38px; }
  .site-header.is-signed-in .site-nav { gap: 26px; }
  .site-header__inner { gap: 14px; }
  .site-header__logo img { height: 38px; }
  .site-header__cta { padding: 9px 14px; }
  .cards--2, .cards--3, .cards--4, .contact-split, .gallery, .calc__grid, .calc__out { grid-template-columns: 1fr; }
  .checklist { grid-template-columns: 1fr; }
  .btn-row .btn { width: 100%; }
  .site-footer__grid { grid-template-columns: 1fr; }
  /* The footer on the phone (owner, 2026-09-30): the logo 20% larger, then 5% larger again,
     128px to 153.6 to 161.28, and 15px less above it, the footer's 56px top padding to 41. */
  .site-footer { padding-top: 41px; }
  .site-footer__logo { width: 161.28px; }
  /* The brand block (logo, tagline, store badges) moves below the link columns on the phone,
     so it follows "Cookie Policy" (owner, 2026-09-30). Visual order only: the grid is one
     column here, and `order` sends the block last within it; the source keeps it first. */
  .site-footer__brand { order: 1; }
  /* The footer's store badges 15% larger on the phone (owner, 2026-09-30): the whole clamp
     scaled, floor, slope and ceiling, 32/10vw/41.6 to 36.8/11.5vw/47.84, so the step holds at
     every width. At 390px that is 39px to 44.85px tall, and the pair (2 x 149.5 + 13 = 312px)
     still sits on one row inside the 342px column; at 320px it is 258px inside 272. */
  .store-badges--footer { --badge-h: clamp(36.8px, 11.5vw, 47.84px); }
  .sec-toc { grid-template-columns: 1fr; }
  .split { grid-template-columns: 1fr; gap: 34px; }
  /* The floats overlap the base image at desktop width; on a phone there is no
     room for that, so they drop into the flow instead of covering it. */
  .collage__float { position: static; width: auto; margin-top: 14px; }
  .counters { gap: 22px; }
  .counter { flex: 1 1 40%; }
  .sec-bar { padding: 10px 20px; }
  .sec-bar__anchor { margin-left: 0; }
  .form-grid { grid-template-columns: 1fr; }
  .logo-grid { grid-template-columns: repeat(2, 1fr); }
  .stat-row { grid-template-columns: repeat(2, 1fr); }
  /* One slide at a time on a phone; the track is already swipeable. */
  .carousel__track { grid-auto-columns: 100%; }
  /* The ring does not survive a 390px canvas. Nodes become a plain list and
     the hub becomes its heading, which is the same information unstacked. */
  .orbit { aspect-ratio: auto; border: 0; max-width: none; }
  .orbit::after { display: none; }
  .orbit__hub { position: static; inset: auto; border-radius: var(--radius); padding: 16px; margin-bottom: 14px; }
  .orbit__node { position: static; transform: none; width: auto; margin: 0 0 8px; text-align: left; padding: 10px 14px; }
}

/* ==================================================================== blog ==
 * Everything under /blog/. Built on the tokens above, so a blog page is the
 * same surface, type and palette as the rest of the site rather than a second
 * design that happens to share a header.
 *
 * Two constraints shaped this block and are enforced by npm run check:
 *
 *   1. No font-size below 1.125rem. Blog chrome is exactly the kind of thing
 *      that reaches for 13px meta lines, so the hierarchy here is carried by
 *      COLOUR, WEIGHT and CASE instead of by size. Every size below is 18px+.
 *   2. Only the approved 850/1000 width queries. There is no blog specific
 *      breakpoint: the grids collapse 3 -> 2 at 1000 and 2 -> 1 at 850, which
 *      is the same rhythm the rest of the site already uses.
 */

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* A section heading that sits under the page h1 without competing with it. */
.section-label {
  font-size: 1.75rem;
  margin-bottom: 24px;
}

.blog-head { margin-bottom: 26px; }
.blog-head h1 { margin-bottom: 14px; }
.blog-head .lead { margin-bottom: 14px; }

/* The breadcrumb is the first thing in the page's opening `section.tight`, which
   gives it 44px of padding above. Owner asked for 10px less air there
   (2026-08-12) and for a further 20px on 2026-09-15, so it pulls itself up by
   30 rather than every page carrying a modifier class on its first section.
   This is the site-wide breadcrumb nudge. .breadcrumbs is on the blog's category, archive
   and search pages only: the help articles lost theirs on 2026-09-15 and /support on
   2026-09-19 (owner, both). */
.breadcrumbs {
  font-size: 1.125rem;
  color: var(--muted);
  margin-top: -30px;
  /* 20px until the owner asked for 6 more on 2026-09-15. */
  margin-bottom: 26px;
}
.breadcrumbs ol {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 9px;
  padding: 0;
  margin: 0;
}
.breadcrumbs a { color: var(--muted); text-decoration: underline; }
.breadcrumbs a:hover { color: var(--coral); }
/* Same colour as the crumb text, at owner request (2026-08-12). It previously
   overrode to --line, which on --surface is about 1.16:1 and read as a smudge. */
.breadcrumbs__sep { color: inherit; }

/* ---- search ---- */
.blog-search { margin-top: 8px; }
/* The visible "Search the blog" label went on 2026-09-19 (owner); the element stays as
   .visually-hidden, so .blog-search__label has no markup left and its rule is gone with it. */
/* The field and its icon, wrapped so the icon can be positioned against the INPUT rather than
   against the row. The row wraps below 850px everywhere except inside .blog-menu, and an icon
   centred on a wrapped two-line row would sit in the seam between the field and the button.
   The wrapper carries the flex sizing the input used to. */
.blog-search__row { display: flex; gap: 12px; flex-wrap: wrap; }
.blog-search__field { position: relative; display: flex; flex: 1 1 280px; min-width: 0; }
/* Centred in the field and out of the pointer's way, so a click anywhere in the box still lands
   on the input. `currentColor` takes the muted tone from this rule. Its `left` and the input's
   left padding move together, or the placeholder collides with the glyph. */
.blog-search__icon {
  position: absolute;
  left: 16px;
  top: 50%;
  transform: translateY(-50%);
  color: var(--muted);
  pointer-events: none;
}
.blog-search__input {
  flex: 1 1 auto;
  min-width: 0;
  font-family: var(--font-body);
  font-size: 1.125rem;
  color: var(--body-text);
  background: var(--field-bg);
  border: 2px solid var(--line);
  border-radius: 10px;
  /* Left padding clears the icon: its 16px box starts 16px in, then an 8px gap, less the 2px
     border the icon's `left` is measured across. */
  padding: 12px 16px 12px 38px;
}
/* Focus recolours the border and draws nothing else (owner, 2026-09-19: "The outline around the
   search input field is too thick, it should only change the color of the search field's default
   border"). It was a 3px coral outline offset 1px OUTSIDE a border that also turned coral, which
   read as two rings. The 2px border is the indicator now, which is how the app's own fields do
   it. `outline: none` is safe ONLY because the border colour carries the focus state: the
   contrast between --line and --coral is the visible change. */
.blog-search__input:focus-visible {
  outline: none;
  border-color: var(--coral);
}

/* ---- category nav ---- */
.cat-nav, .archive-nav {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  list-style: none;
  padding: 0;
  margin: 0;
}
/* The category pills sit against the right edge (owner, 2026-09-19), on every page that
   carries the strip, so they do not jump sides when a visitor picks a category. The archive's
   year links share the rule above and stay left. Below 850px the pills wrap onto several rows,
   and a ragged left edge reads as a mistake there, so they go back to the left. */
.cat-nav { justify-content: flex-end; }
.cat-nav__link {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  font-family: var(--font-head);
  font-weight: 600;
  font-size: 1.125rem;
  color: var(--body-text);
  background: var(--card);
  border: 2px solid var(--line);
  border-radius: 999px;
  padding: 9px 18px;
  /* Matches the tag pills at 0.25s ease (owner, 2026-08-12). background-color is in the list
     for the .is-active state, which is a page state rather than a hover, but transitions the
     same way if it is ever toggled client side. */
  transition: background-color 0.25s ease, border-color 0.25s ease, color 0.25s ease;
}
.cat-nav__link:hover { border-color: var(--btn-ink); color: var(--btn-ink); }
@media (prefers-reduced-motion: reduce) {
  .cat-nav__link { transition: none; }
}
.cat-nav__link.is-active {
  background: var(--btn-ink);
  border-color: var(--btn-ink);
  color: #fff;
}
.cat-nav__count { color: var(--muted); font-weight: 500; }
.cat-nav__link.is-active .cat-nav__count { color: rgba(255, 255, 255, 0.75); }

/* ---- post cards ---- */
.post-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 28px; }
/* Two across, for the /blog index's Recent posts (owner, 2026-09-19): that section gives up
   its left third to a blog navigation column. It sits above the width queries on purpose, so
   the single-column collapse below 850px still wins. */
.post-grid--2 { grid-template-columns: repeat(2, 1fr); }
/* The /blog index's lower section (owner, 2026-09-19): a blog navigation menu in the left
   third, Recent posts in the other two. Three equal tracks on the post grid's own 28px gap,
   with the posts spanning two, so a card here is exactly as wide as a card in the three-across
   grid this replaced. The menu holds the search form, moved down from under the page title.
   There the form is exactly as wide as the menu, and the button is joined to the field's
   right edge as one control: no gap, no wrap, the field's right corners and the button's left
   corners squared, the field's right border dropped so the seam is a single line. These rules
   outrank the 850px `.blog-search__row .btn { width: 100% }` on specificity, so the joined
   shape holds on a phone as well. Below 850px the layout is one column, menu first. */
.blog-layout { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 28px; align-items: start; }
.blog-layout__main { grid-column: span 2; min-width: 0; }
.blog-menu .blog-search { margin-top: 0; }
.blog-menu .blog-search__row { flex-wrap: nowrap; gap: 0; }
.blog-menu .blog-search__field { flex: 1 1 0; }
.blog-menu .blog-search__input { border-right: 0; border-radius: 10px 0 0 10px; }
.blog-menu .blog-search__row .btn { flex: none; width: auto; border-radius: 0 10px 10px 0; }

/* position: relative is what makes the whole card clickable: the title's link carries an
   ::after that stretches to these edges (owner, 2026-08-12). Done this way rather than by
   wrapping the <article> in an anchor, because that would put the category, excerpt and date
   inside the link and make its accessible name the entire card read aloud. Here the card has
   exactly one link, named by the title, and the overlay only extends where you can click. */
.post-card {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  overflow: hidden;
  /* Resting at 95 percent so the card settles back very slightly against the page and comes
     fully forward under the pointer (owner, 2026-08-12). Checked against the type floor rather
     than assumed: at 0.95 the card composites 5 percent of the page surface through itself, so
     the title lands at 15.2:1 instead of 15.4:1 and the muted date line at 5.28:1 instead of
     5.89:1. Both still clear AA, and the image dims with the card, which is the point. */
  opacity: 0.95;
  transition: opacity 0.3s ease, box-shadow 0.3s ease, border-color 0.3s ease;
}
.post-card__title a::after {
  content: "";
  position: absolute;
  inset: 0;
}
/* Deliberately NO translate (owner, 2026-08-12). The catalog's other card components lift 6px
   on hover; these do not, so the grid stays still and only the shadow and border answer the
   pointer. focus-within matches the hover for keyboard users, who never fire :hover. */
.post-card:hover,
.post-card:focus-within {
  opacity: 1;
  box-shadow: var(--shadow);
  border-color: var(--coral);
}
@media (prefers-reduced-motion: reduce) {
  .post-card { transition: none; }
}
.post-card__media { display: block; aspect-ratio: 16 / 9; overflow: hidden; }
.post-card__media img { width: 100%; height: 100%; object-fit: cover; }
.post-card__body {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 11px;
  padding: 24px;
  flex: 1;
}
/* 24px, owner-set 2026-08-12; was 1.25rem (20px), which had been tuned against the old 20px
   h3 base and left a card title smaller than a body h3 once that moved to 24px. */
/* No font-size: a post title IS an h3 and takes the 24px base, so it follows any future change
   to the scale instead of having to be found and re-pinned. Pinning it to a literal equal to
   the base was exactly the trap that left .archive-item__title stranded at 20px when the base
   moved on 2026-08-12. Same reasoning as .post-layout .prose h3 further down. */
.post-card__title { margin: 0; }
.post-card__title a { color: var(--ink); }
.post-card__title a:hover { color: var(--coral); }
.post-card__excerpt { margin: 0; }
.post-card__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 9px;
  font-size: 1.125rem;
  color: var(--muted);
  margin-top: auto;
  padding-top: 6px;
}

/* The category label. Uppercase and coral so it reads as a label at body size. */
.post-cat {
  font-family: var(--font-head);
  font-weight: 600;
  font-size: 1.125rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--coral);
  margin: 0;
}
/* No hover rule: the category eyebrow is a <span>, not a link (owner, 2026-08-12).
   It reads as a label above the post title, and on a post page the breadcrumb
   directly above it already links to the same category. Category pages stay
   reachable from the "Browse by category" nav on the blog index and archive. */


/* ---- featured ---- */
.featured {
  display: grid;
  grid-template-columns: 1.1fr 1fr;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  overflow: hidden;
}
.featured__media { display: block; height: 100%; }
.featured__media img { width: 100%; height: 100%; object-fit: cover; min-height: 260px; }
.featured__body {
  padding: 38px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 14px;
}
/* Flat 34px, matching h2 (2026-08-12). This was the last clamp() heading in the file after the
   scale went flat, so it was the only title still resizing with the viewport. It is an h3 in
   the markup but the lead card of the blog index, so it takes the h2 size rather than the 24px
   h3 base. At desktop this is a 0.4px change from the clamp's 33.6px ceiling; on a narrow
   screen it no longer drops to 27.2px. */
.featured__title { font-size: 34px; margin: 0; }
.featured__title a { color: var(--ink); }
.featured__title a:hover { color: var(--coral); }
.featured__excerpt { margin: 0; }

/* ---- post meta line ---- */
.post-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 11px;
  font-size: 1.125rem;
  color: var(--muted);
  margin: 0;
}
.post-meta__author { font-weight: 600; color: var(--body-text); }
.post-meta__dot { color: inherit; }
.post-avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 46px;
  height: 46px;
  border-radius: 50%;
  background: var(--teal);
  color: #fff;
  font-family: var(--font-head);
  font-weight: 700;
  font-size: 1.125rem;
  letter-spacing: 0.03em;
}

/* ---- pagination ---- */
.pagination-wrap { margin-top: 44px; }
.pagination {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 9px;
  list-style: none;
  padding: 0;
  margin: 0;
}
.pagination__link,
.pagination__current,
.pagination__disabled {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 50px;
  min-height: 50px;
  padding: 0 17px;
  font-family: var(--font-head);
  font-weight: 600;
  font-size: 1.125rem;
  border: 2px solid var(--line);
  border-radius: 10px;
  background: var(--card);
  color: var(--body-text);
}
.pagination__link:hover { border-color: var(--ink); color: var(--ink); }
.pagination__current { background: var(--ink); border-color: var(--ink); color: #fff; }
/* A page with no target is a span, not a dead link. Muted rather than hidden so
   the control set stays a stable shape as you move between pages. */
.pagination__disabled { color: var(--muted); opacity: 0.55; }

/* ---- post page ---- */
.post-header { margin-bottom: 30px; }
.post-header .post-cat { display: inline-block; margin-bottom: 12px; }
.post-header h1 { margin-bottom: 16px; }
.post-header .lead { margin-bottom: 20px; }

.post-figure { margin: 0; }
.post-figure img { width: 100%; border-radius: var(--radius-lg); }
.post-figure figcaption {
  font-size: 1.125rem;
  color: var(--muted);
  padding-top: 13px;
}

.post-layout {
  display: grid;
  grid-template-columns: 260px minmax(0, 1fr);
  gap: 48px;
  align-items: start;
}

/* The STICKY ELEMENT IS THE GRID ITEM, not the card inside it. Sticking .toc
 * instead does nothing: with align-items: start the aside is content-height, so
 * a sticky child's containing block is exactly its own box and it has zero
 * travel. The aside's own containing block is the grid AREA, which is full row
 * height, so it travels the length of the article.
 *
 * top clears the 82px sticky site header (8px top padding + 72px min-height + 2px border)
 * plus 24px of air. .sec-bar above uses top: 80px for the same header. */
.post-aside {
  position: sticky;
  top: 106px;
}

.toc {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 24px;
}
.toc__heading {
  font-size: 1.125rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 14px;
}
/* Square bullets, not numbers (owner, 2026-09-19). The list was an <ol> numbered by a CSS
   counter in `a::before`; it is a <ul> now, since nothing about it is ordered any more, and the
   marker is the browser's own. That also fixes the wrap: a counter inside the link let a second
   line run back under the number, where a real marker hangs it under the text. The 20px is the
   marker's room; a flex column keeps each <li> a list-item, so the markers survive it. */
.toc ul {
  list-style: square;
  padding: 0 0 0 20px;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 11px;
}
.toc li::marker { color: var(--muted); }
.toc a { color: var(--body-text); }
.toc a:hover { color: var(--coral); }

/* ---- article body ----
 * SCOPED TO .post-layout ON PURPOSE. `.prose` is NOT a blog class: /privacy and
 * /terms have used it since v0.5.0, and docs/DESIGN-SYSTEM.md documents it as
 * belonging to the legal pages. This block sits after the original `.prose`
 * rules near line 650, so bare `.prose h2` here would beat them on source order
 * at equal specificity and silently restyle both legal pages - measured, it cost
 * /terms 18.4px of separation on each of its 14 headings and underlined its
 * links. Every selector that could collide is prefixed. `.prose__rule` is not,
 * because that class exists only on a blog post. */
.post-layout .prose h2 { margin-top: 1.5em; }
.post-layout .prose h2:first-child { margin-top: 0; }
/* Font-size deliberately dropped here (2026-08-12). This carried 1.35rem, 21.6px, chosen only
   to sit a step ABOVE the old 20px base. With the base owner-set to 24px it would have made a
   post-body h3 the SMALLEST h3 on the site, which reverses what it was for. It now inherits
   the base and follows any future change to it. The margin stays: that is about prose rhythm,
   not size. */
.post-layout .prose h3 { margin-top: 1.4em; }
/* `:not(.tags)` / `:not(.tag)` (owner, 2026-08-12). These are rules for PROSE lists, and
   .tags is a component list that happens to be a <ul> of <li> sitting inside the article body.
   Unprefixed they beat it on specificity, (0,2,1) against .tags at (0,1,0), so the pill row was
   indented 26px past its own heading and past the share row directly below it, and picked up a
   bottom margin on the list and on every pill. Same collision the .post-layout prefix was added
   to stop; a component nested INSIDE the prose has to be excluded as well as scoped around. */
.post-layout .prose ul:not(.tags),
.post-layout .prose ol { margin: 0 0 1em; padding-left: 26px; }
.post-layout .prose li:not(.tag) { margin-bottom: 0.45em; }
.post-layout .prose blockquote {
  margin: 30px 0;
  padding: 4px 0 4px 26px;
  border-left: 4px solid var(--coral);
  font-family: var(--font-head);
  font-size: 1.35rem;
  line-height: 1.45;
  color: var(--ink);
}
.post-layout .prose blockquote p:last-child { margin-bottom: 0; }
.post-layout .prose figure { margin: 30px 0; }
.post-layout .prose figure img { border-radius: var(--radius); }
.post-layout .prose figcaption {
  font-size: 1.125rem;
  color: var(--muted);
  padding-top: 11px;
}
.post-layout .prose code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 1.125rem;
  background: var(--band);
  border-radius: 6px;
  padding: 2px 7px;
}
/* `:not(.tag)` for the third time in this block, and for the same reason as the ul and li
   exclusions above: this underlines PROSE links, and the tag pills are a component that lives
   inside the article body. It only started catching them when the pills became anchors on
   2026-08-12; at (0,2,1) it beats `a { text-decoration: none }` at (0,0,1) and drew a line
   under every pill's label. A pill is not a run of prose and reads as a button, not a link. */
.post-layout .prose a:not(.tag) { text-decoration: underline; }
.prose__rule { border: 0; border-top: 1px solid var(--line); margin: 40px 0 32px; }

/* ---- tags, share ---- */
.tags { display: flex; flex-wrap: wrap; gap: 10px; list-style: none; padding: 0; margin: 0; }
/* The pill is now an <a> inside the <li>, not the <li> itself (owner, 2026-08-12). It needs a
   display mode: an inline element takes horizontal padding but its vertical padding does not
   grow the line box, so the pill would have collapsed to a flat stripe. The color declaration
   also does real work here now, holding the label at body text against the global
   `a { color: var(--coral) }`; it outranks it at (0,1,0) against (0,0,1). */
.tag {
  display: inline-flex;
  align-items: center;
  font-size: 1.125rem;
  background: var(--band);
  color: var(--body-text);
  border-radius: 999px;
  padding: 7px 16px;
  /* 0.25s ease, up from 0.15s (owner, 2026-08-12). The pill was already transitioning; at
     0.15s a cross-fade this large, near-white fill to near-black, is over before the eye
     registers it as a fade rather than a snap. background-color rather than the `background`
     shorthand: the shorthand animates every background longhand, and colour is the only one
     that moves here. */
  transition: background-color 0.25s ease, color 0.25s ease;
}
/* #232323 literal, owner-set 2026-08-12. Deliberately not var(--dark), which holds the same
   value: that token means "the dark band, footer and consent bar", and pointing this at it
   would tie a hover state to a change made for those. Was var(--ink). */
a.tag:hover {
  background: #232323;
  color: var(--surface);
}
@media (prefers-reduced-motion: reduce) {
  .tag { transition: none; }
}
.share { display: flex; flex-wrap: wrap; gap: 11px; }
/* Icon-only since 2026-08-12. Square rather than the old label-width pill, and sized in px
   because there is no text in the box to size it: the visible content is a 29px glyph in a
   62px box, both 30 percent up on the original 22 and 48 (owner, 2026-08-12), which keeps the
   tap target well past the 44px minimum on a phone. No font-size here on purpose. The only
   text left is .visually-hidden, which is 1px and clipped, so declaring a size would put a
   sub-18px rule in this file for text nobody can read. The glyph's own width and height live
   on the <svg> in site-pages-blog.mjs; change the two together or the icon stops being
   centred by its own margin of white space. */
.share__link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 62px;
  height: 62px;
  color: var(--btn-ink);
  background: var(--card);
  border: 2px solid var(--line);
  border-radius: 10px;
  /* Same 0.25s ease as the tag and category pills. The glyph rides along without being named
     here: the <svg> is fill: currentColor, so it interpolates with the color property. */
  transition: background-color 0.25s ease, border-color 0.25s ease, color 0.25s ease;
}
/* The two swap places (owner, 2026-08-12): the glyph is --btn-ink on a --card button at rest,
   and on hover the button takes the glyph's colour and the glyph takes the button's. Same
   inversion the tag pills do, so the three families now share one hover idiom. */
.share__link:hover {
  background: var(--btn-ink);
  border-color: var(--btn-ink);
  color: var(--card);
}
@media (prefers-reduced-motion: reduce) {
  .share__link { transition: none; }
}
/* Fills from the link's own color, so the hover above moves the glyph with the border. */
.share__icon { display: block; }

/* The previous/next post pair that sat under a "Continue reading" heading was removed on
   2026-08-12 (owner). Its .post-nav rules went with it rather than being left behind: nothing
   emits that markup any more, and an unused block in a hand-written stylesheet reads as a
   component that still exists. Recoverable from git if the pair comes back. */

/* ---- archive ---- */
.archive-group { margin-bottom: 44px; }
.archive-group:last-of-type { margin-bottom: 0; }
.archive-list { list-style: none; padding: 0; margin: 0; border-top: 1px solid var(--line); }
.archive-item {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 26px;
  padding: 22px 0;
  border-bottom: 1px solid var(--line);
}
.archive-item__main { display: flex; flex-direction: column; align-items: flex-start; gap: 7px; }
/* Was 1.25rem (20px), which silently became the odd one out when the h3 base moved to 24px on
   2026-08-12. Inherits now, like .post-card__title. */
.archive-item__title { margin: 0; }
.archive-item__title a { color: var(--ink); }
.archive-item__title a:hover { color: var(--coral); }
.archive-item__excerpt { margin: 0; }
.archive-item__date { font-size: 1.125rem; color: var(--muted); white-space: nowrap; }

/* ---- search results ---- */
.search-summary { font-size: 1.125rem; color: var(--muted); margin: 22px 0 0; }
mark { background: #f7e3bb; color: var(--ink); border-radius: 4px; padding: 0 3px; }

/* ---- empty states ---- */
.blank-slate {
  max-width: 640px;
  margin: 0 auto;
  padding: 58px 30px;
  text-align: center;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
}
.blank-slate__glyph { font-size: 44px; line-height: 1; margin: 0 0 16px; color: var(--coral); }
.blank-slate h2 { margin-bottom: 12px; }
.blank-slate p { color: var(--muted); }
.blank-slate__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  justify-content: center;
  margin-top: 24px;
}

.count-pill {
  display: inline-block;
  font-size: 1.125rem;
  background: var(--band);
  color: var(--body-text);
  border-radius: 999px;
  padding: 7px 16px;
  margin: 0;
}

/* ============================================================================
   HELP CENTER (/help)
   Rendered from the app's help feed by help-renderer/ through scripts/site-pages-help.mjs; the
   committed placeholder in public/help/ uses the same classes. Built on the blog's parts where
   they fit (.blog-search__input, .post-cat, .visually-hidden) and held to the same
   two rules: nothing under 1.125rem, and no width query of its own. Its 850 rules sit in the
   blog's responsive block just below, so the stylesheet still has exactly the approved five.
   ============================================================================ */

.help-head h1 { margin-bottom: 22px; }
/* The app's "can't be loaded" line: the placeholder's only content, and the index's when the
   feed held no published article. */
.help-unavailable { font-size: 1.125rem; color: var(--muted); margin: 0; }

/* site.js toggles `hidden` on these, and each sets its own display, which outranks the UA's
   [hidden] rule. Scoped rather than a global [hidden] reset, as the account menu's are. */
.help-results[hidden],
.help-categories[hidden],
.help-results__status[hidden] { display: none; }

.help-search { margin: 0; }
.help-search__input { width: 100%; }
/* The /support index's bar wears the web app's header search bar (owner, 2026-11-07: "copy all of
   these styles"; the app's universal-search-field.tsx is the reference), at this bar's own size.
   The dark theme's half is with the other theme rules at the end of this file. The left padding
   is the icon's 16px inset, less the 2px border, plus its 17.2px and the 8px gap to the text.
   `[data-help-search]` is on the index's form only, so the blog's search and the article aside
   are untouched. */
[data-help-search] .blog-search__icon {
  width: 17.2px;
  height: 17.2px;
  color: #a5a5a5;
  transition: color 150ms;
}
/* Pill-shaped like the app's universal search field, `rounded-full` (owner, 2026-11-07). */
[data-help-search] .help-search__input {
  padding-left: 39.2px;
  border-radius: 9999px;
  transition: background-color 150ms, border-color 150ms, color 150ms;
}
[data-help-search] .help-search__input::placeholder { color: #a5a5a5; opacity: 1; }
/* The light theme's bar must read the same size as the dark theme's (owner, 2026-11-07). The box
   is identical in both, but --line nearly vanishes on the light page, so the white looked 4px
   smaller than the dark theme's bar, whose border reads as part of it. The border takes the
   fill's colour here. `:not(:focus-visible)` keeps the coral focus border. */
html:not(.theme-dark) [data-help-search] .help-search__input:not(:focus-visible) {
  border-color: var(--field-bg);
}
/* The search field ends `section.tight`, so the gap under it is that section's 44px bottom plus
   the results/categories section's own --section-pad top. Dropping the second closes it to 44px,
   which is the boundary the owner asked for (2026-09-15). */
.help-body { padding-top: 0; }
.help-results__status { font-size: 1.125rem; color: var(--muted); margin: 0 0 18px; }

/* One card per category, two across: the app's own Help page layout (a category card listing
   its articles), in this site's card chrome. */
.help-categories {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 24px;
}
.help-category {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: 28px;
}
/* An h2 by document structure, sized as a card title: eight 34px section headings in a grid
   would out-shout the page's own h1. 24px is the h3 base. */
.help-category__title { font-size: 24px; margin-bottom: 14px; }

.help-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.help-list a { font-weight: 500; }
.help-list a:hover { text-decoration: underline; }
/* The category under a search result: a label, not a link, like .post-cat on the blog. */
.help-list__meta { display: block; font-size: 1.125rem; color: var(--muted); }

/* ---- article ----
   Scoped to .help-article, never bare .prose: .prose belongs to the legal pages, and an unscoped
   rule here would restyle /terms and /privacy on source order, the collision measured on
   .post-layout .prose above. */
/* 11px above the h1 in the centre column and 12px above RELATED in the right one (owner,
   2026-10-21: 40px, then 20, 5 and 2 more taken off both, 1 more off the h1 alone, 2 off both,
   then 1 back on both). Three-column layout only: the 1000px stack takes both off, below. */
.help-article__head { margin: 11px 0 28px; }
.help-layout > .help-rail { padding-top: 12px; }
.help-article__head h1 { margin: 0; }
/* Heading sizes for this page type, cut over several owner passes on 2026-09-15: h1 48 -> 30
   (8, 4, 2 then 4), h2 34 -> 20 (8, 4 then 2). The base sizes near the top of this file are unchanged, so /help's
   index, the blog and every static page keep the site's scale. The line-height rules below are
   ratios in `em`, so they follow these sizes down without a second edit.
   NOTE: h3 stays at its 24px base, so an h2 here is now SMALLER than an h3 under it. No live
   article uses h3 (every body is h2-only after relevelHeadings) so nothing shows it today, but
   an article that introduces one will render its subheadings larger than their heading until h3
   is dropped to match. */
.help-article-page h1 { font-size: 30px; }
.help-article-page h2 { font-size: 20px; }
.help-article .prose h2 { margin-top: 1.5em; }
.help-article .prose h2:first-child { margin-top: 0; }
.help-article .prose h3 { margin-top: 1.4em; }
.help-article .prose ul,
.help-article .prose ol { margin: 0 0 1em; padding-left: 26px; }
.help-article .prose li { margin-bottom: 0.45em; }
.help-article .prose a { text-decoration: underline; }
/* No outline around an article's images (owner, 2026-11-04: "there is no reason to show an
   outline around images like this screenshot in the body of the page"). It was a 1px --line
   border, which drew a box the full width of a screenshot's transparent margins. The radius
   stays, for images that fill their box. Both themes. */
.help-article .prose img { border-radius: var(--radius); }
/* No horizontal rules in a support article (owner, 2026-10-01: "remove the use of horizontal
   rule/lines" on all the support article pages). A rule inside the HelpScout body is dropped
   rather than drawn; the lines that framed the previous link and the contact link are gone too
   (below), with their spacing kept. */
.help-article .prose hr { display: none; }
.help-article .prose blockquote {
  margin: 30px 0;
  padding: 4px 0 4px 26px;
  border-left: 4px solid var(--coral);
}
.help-article .prose code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 1.125rem;
  background: var(--band);
  border-radius: 6px;
  padding: 2px 7px;
}
.help-article .prose pre {
  overflow-x: auto;
  background: var(--band);
  border-radius: 10px;
  padding: 14px 18px;
}
.help-article .prose pre code { background: none; padding: 0; }
/* A wide table scrolls inside itself rather than pushing the page sideways on a phone. */
.help-article .prose table {
  display: block;
  overflow-x: auto;
  border-collapse: collapse;
  margin: 0 0 1em;
}
.help-article .prose th,
.help-article .prose td {
  border: 1px solid var(--line);
  padding: 8px 12px;
  text-align: left;
  vertical-align: top;
}
/* ---- 6px off every line-height on the article page (owner, 2026-09-15) ----
   CSS cannot subtract from an inherited line-height, and a px value set on a wrapper is no use
   either: line-height inherits as a COMPUTED length, so one value on the section would give the
   30px h1 and the 18px body the same leading. So each rule restates the ratio it is trimming and
   subtracts the 6px in `em`, which calc() resolves against the element's OWN font-size. One rule
   then holds at every size on the page, and a new size needs no new rule.

   The page inherits exactly two ratios, so there are exactly two rules: 1.15 from the `h1, h2,
   h3, h4` rule near the top of this file, and body's 1.65 for everything else. h5 and h6 are in
   the second group on purpose, because that heading rule does not name them and they inherit
   1.65 like body text.

   Scoped to `.help-article-page`, which only the help ARTICLE template emits, so /help's index
   and the blog keep the site's leading. The site header, footer and consent bar are outside this
   section and are untouched: they are chrome, not this page. The search field is left out
   deliberately, being the one control here whose line-height is `normal` and whose height comes
   from its padding. */
.help-article-page :is(h1, h2, h3, h4) { line-height: calc(1.15em - 6px); }
.help-article-page
  :is(
    p, li, a, span, strong, em, b, i, code, pre, blockquote, figcaption,
    td, th, dt, dd, label, h5, h6
  ) {
  line-height: calc(1.65em - 6px);
}

/* ---- the three-column article layout ----
   Topics left, the article centre, "On this page" right, as the owner's mockup has it
   (2026-09-15). Columns are PLACED, not ordered: each child names its grid-column, so the source
   order stays the reading order and the mobile stack is a row change rather than a DOM change.
   Only the approved 1000 and 850 queries are used; the rules for both sit in the blog/help
   responsive block below, so the stylesheet still has exactly five width queries. */
/* The article page has no breadcrumb since 2026-09-15 (owner), so the site-wide `.breadcrumbs`
   pull of -30px no longer reaches this page. The pull stays, moved to the LAYOUT, because it is
   what holds the page as high under the header as the owner set it: all three columns rise
   together and their tops stay level. */
.help-article-page .help-layout { margin-top: -30px; }

.help-layout {
  display: grid;
  grid-template-columns: 232px minmax(0, 1fr) 216px;
  /* Both column gaps, 48px until the owner took 5 off each on 2026-09-15 and then 5 more. The
     two side columns are fixed, so the 20px comes back to the article. Only the 1000px stack's
     row gap is set separately, below. */
  gap: 38px;
  align-items: start;
}
.help-layout > .help-aside { grid-column: 1; grid-row: 1; }
.help-layout > .help-main { grid-column: 2; grid-row: 1; }
.help-layout > .help-rail { grid-column: 3; grid-row: 1; }

/* Both rails stick once the article is taller than the viewport, which is the whole point of a
   persistent topic list and a table of contents. 100px clears the fixed header (92px until the
   header gained 8px on 2026-10-27). */
.help-aside,
.help-rail {
  position: sticky;
  top: 100px;
}
/* The field's TEXT lines up with the HELP TOPICS label and the topic names below it (owner,
   2026-09-15). Its box was already flush with the column; what sat 18px in was the text, behind
   the control's own 2px border and 16px padding. So the field is pulled out by that 18px rather
   than having its padding stripped, which would leave typed text touching the border. It ends up
   14px into the container's 24px gutter, level with the topic pills, and its right edge stays on
   the column. The pull MUST equal the search icon's `left` below, since the icon is now the
   field's leftmost content and therefore what sits on that 94px line. It was -18 back when the
   TEXT sat there behind a 16px padding, so change the two together or the alignment drifts. */
/* 3px above it (owner, 2026-10-21: 6px, then 2 off twice and 1 back on, with the other two
   columns), three-column layout only like the h1 and RELATED spacing. */
.help-aside__search { position: relative; margin: 3px 0 22px -14px; }
/* Centred in the field and out of the pointer's way, so a click anywhere in the box still lands
   on the input. `currentColor` takes the muted tone from the rule below. */
.help-aside__search-icon {
  position: absolute;
  left: 14px;
  top: 50%;
  transform: translateY(-50%);
  color: var(--muted);
  pointer-events: none;
}
/* The aside's field is the index's control in a narrower column, so it may shrink below the
   280px flex-basis .blog-search__input carries for the blog's search row. Its padding is 4px
   tighter than that control's 12px/16px (owner, 2026-09-15); the index's own search box is
   untouched, this is scoped to the article page's aside. */
.help-aside__search .help-search__input {
  flex: 1 1 auto;
  /* Left padding clears the icon: 12px of its own, then the icon's 16px box and an 8px gap,
     less the 2px border the icon's `left` is measured across. */
  padding: 8px 12px 8px 36px;
  /* 14px, approved by the owner 2026-09-15: a sixth exception to the 18px floor, for this one
     control. The index's search field stays at 1.125rem. */
  font-size: 14px;
}

/* The two column labels. Uppercase and tracked like .post-cat, muted rather than coral because
   they label furniture, not the article. 1.125rem: the 18px floor is the owner's rule, so these
   are larger than the mockup draws them rather than carrying a new exception.
   Named for their CONTENT, not their column: the owner swapped which column each block sits in
   on 2026-09-15, and a container-named label class would have had to swap with it. */
.help-topics__label,
.help-toc__label {
  font-family: var(--font-head);
  font-weight: 600;
  font-size: 1.125rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 12px;
}

.help-topics,
.help-toc {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
/* The topic TEXT lines up with the HELP TOPICS label above it (owner, 2026-09-15). The pill's
   own 14px inset was indenting every item past its heading, so the list is pulled out by that
   inset on both sides rather than the padding being dropped: the current item's highlight keeps
   its breathing room and simply extends past the column, which is how the mockup draws it. The
   pull is 14px into the container's 24px gutter on the left and into the 48px column gap on the
   right, so it overlaps neither the viewport edge nor the article. */
/* Both link lists are pulled out by the pill's own text inset, so their TEXT lines up with the
   label above and their hover pills extend past the column. See the note on .help-topics__link's
   padding for why the contents list splits that 14px across a border and a padding. */
.help-topics,
.help-toc { margin-left: -14px; margin-right: -14px; }
.help-topics__link {
  /* 16px, approved by the owner 2026-09-15: a fourth exception to the 18px floor, for the
     help article page's two side columns only. These are navigation labels, not reading text
     — the article's own body copy stays at the floor. */
  font-size: 16px;
  display: block;
  padding: 9px 14px;
  border-radius: 10px;
  color: var(--body-text);
  text-decoration: none;
}
.help-topics__link:hover { background: var(--band); }
/* The current topic. aria-current carries it to a screen reader; this is the visual half. */
.help-topics__link.is-current {
  background: var(--band-deep);
  font-weight: 600;
}

/* The table of contents. Given the topic list's padding and hover pill (owner, 2026-09-15), so
   the two link lists read as one component in two columns.
   The current section's coral bar is painted by site.js adding .is-current as the reader passes
   each heading; with JS off every entry simply sits unmarked and still jumps to its section. */
.help-toc a {
  /* 16px, approved by the owner 2026-09-15: a fourth exception to the 18px floor, for the
     help article page's two side columns only. These are navigation labels, not reading text
     — the article's own body copy stays at the floor. */
  font-size: 16px;
  position: relative;
  display: block;
  /* The topic list's inset exactly, so the two lists' text lands on the same line. */
  padding: 9px 14px;
  border-radius: 10px;
  color: var(--muted);
  text-decoration: none;
}
.help-toc a:hover { background: var(--band); color: var(--body-text); }
.help-toc a.is-current { color: var(--body-text); }
/* The current section's marker, drawn as its own box rather than as a border-left: the 10px
   radius the hover pill needs bows a left BORDER into a crescent (owner-reported, 2026-09-15).
   A positioned bar keeps its edges straight and leaves the pill fully rounded, so this list and
   the topic list still match. Inset 6px top and bottom so it never crosses the pill's corners.
   #48ac9c (owner, 2026-09-15; tried #74b9e4 first), the one cool accent on a warm page and a
   near match for the teal in the logo; a 3px decorative bar carries
   no text contrast requirement, and aria-current does the non-visual half. */
.help-toc a.is-current::before {
  content: "";
  position: absolute;
  left: 0;
  top: 6px;
  bottom: 6px;
  width: 3px;
  background: #48ac9c;
}

/* ---- the Support card (supportCard() in site-template.mjs, owner 2026-10-01) ----
   The web desktop's Support card (support-card.tsx in the alpha repo) in the article page's right
   column, where the "Contact support" link was until today; that link's rules, its 16px
   exception and its icon rule went with it. TWO tiles here, "Ask Support" and "Suggestions"
   (owner: "2 buttons should fit inside the marketing site column of 216px").
   THE GEOMETRY IS THE APP'S, number for number: 93px high, 12px padding, 14px radius, a 1px
   border; equal columns 8px apart; each tile a centred column with a 6px gap, an 11px radius, a
   1px border and NO horizontal padding; a 21px icon; a 150ms colour transition.
   THE COLOURS ARE THIS SITE'S (owner: all light on marketing). The app's card is a fixed dark
   surface, #2F2E2E with cream washes; this one is --card on the page with --line edges, and the
   tiles are the same two washes turned over: --ink at 4% at rest and 8% on hover, where the app
   has cream at 4% and 8%. Washes rather than mixed literals, so they cannot drift from the card
   colour under them. Computed on the resting tile (#f6f6f6): the --ink label is 16.0:1 and the
   --coral icon 3.85:1; on the hover tile (#ededed), 14.75:1 and 3.55:1. The label clears AA's
   4.5:1 many times over, and the icon the 3:1 a graphic needs; the icon carries no text.
   No width of its own above 1000px, where the 216px column sizes it: 190px inside the padding,
   so 91px a tile, against labels measured at 75.4px and 76.2px. From 1000px down the column is
   a full-width row, so the card is CAPPED at 323px, the width of the app panel it lives in
   there, and sits at the left. A max-width, not a width query: check:breakpoints allows the five
   that exist and this needs none. */
.support-card {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
  height: 93px;
  max-width: 323px;
  padding: 12px;
  /* No fill and no edge of its own (owner, 2026-10-12; briefly 40% the same day): the two tiles
     carry the look. */
  border: 0;
  border-radius: 14px;
  background: none;
}
/* Only when the Related list is above it, which is no article today. The card is otherwise the
   first thing in the column and starts level with the article's h1. */
.help-topics + .support-card { margin-top: 20px; }
/* A link in the markup and a button once site.js has wired it (role="button"), so it is drawn
   as the control it becomes: no underline, the label in --ink rather than link coral. */
.support-card__tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  border: 1px solid var(--line);
  border-radius: 11px;
  /* Lighter at rest (owner, 2026-10-12): white at 60%, where it was a 4% dark tint. Hover still
     darkens to the 8% tint below, so the two states stay apart. */
  background: rgba(255, 255, 255, 0.6);
  color: var(--ink);
  text-decoration: none;
  cursor: pointer;
  transition: background-color 150ms, border-color 150ms, color 150ms;
}
/* The colour is restated ON PURPOSE: the tile is an <a>, and the global `a:hover` (0,1,1) beats
   the rule above (0,1,0) and would turn the label link coral on hover, 4.50:1 on this wash
   where --ink is 14.75:1. The app's label does not change colour on hover; nor does this one. */
.support-card__tile:hover {
  background: rgba(29, 26, 24, 0.08);
  border-color: var(--line-hover);
  color: var(--ink);
}
/* The modal fields' ring, 2px of --coral standing off the edge, so the keyboard focus reads the
   same on the tile as inside the dialog it opens. :focus-visible, so a mouse click draws none. */
.support-card__tile:focus-visible { outline: 2px solid var(--coral); outline-offset: 2px; }
/* The app's `text-link` terracotta is this site's --coral in all but one unit. `flex: none` so
   a tile squeezed on a very narrow phone gives up label room before it squashes the glyph. */
.support-card__icon { flex: none; color: var(--coral); }
/* 13.5px, approved by the owner 2026-10-01: "13.5px, like the web desktop". The app's tile
   label, size for size (support-card.tsx: 13.5px / 600 / line-height 1 / nowrap), and the ONE
   sub-18px rule this feature adds; at this site's 18px floor "Suggestions" is 101.6px wide and
   cannot fit a 91px tile. A label on a control, not reading text.
   TWO CLASSES, AND THE FIRST IS LOAD-BEARING: `.help-article-page :is(p, li, a, span, ...)`
   gives every span in the article page a line-height of calc(1.65em - 6px) at (0,1,1), which
   beats a single class and would make this 16.3px tall instead of the app's 13.5px. */
.support-card .support-card__label {
  font-family: var(--font-head);
  font-size: 13.5px;
  font-weight: 600;
  line-height: 1;
  white-space: nowrap;
}
/* /contact's THREE tiles (owner, 2026-10-19: the web desktop's three support buttons, "styled
   like the marketing website"): Help Docs, Ask Support, Suggest in equal columns. On /contact the
   card sits flush with the column's text rather than inset by its 12px padding, since it has no
   fill to pad.
   A THIRD LARGER THAN THE APP'S, ON /contact ONLY (owner, the same day: "Increase the size of
   the 3 buttons, their icons, and their text on the 'contact' page (only)"). Every measure goes
   up by the same 4/3: the label 13.5 -> 18px, which is this site's floor, so no sub-floor
   approval applies here; the icon 21 -> 28px; each tile 94 x 69 -> 125 x 92; the gaps 6 -> 8 and
   8 -> 11; the corners 11 -> 15; the dot 9 -> 12. The cap is three such tiles and two gaps.
   ON A PHONE THE COLUMN IS NARROWER THAN THAT (273px at 375), so the labels may wrap onto two
   lines and the tile grows to hold them, rather than an 18px nowrap label running out of its
   tile. Where three fit, nothing wraps and every tile is 92px tall. */
.support-card--three { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.contact-split__col .support-card--three {
  gap: 11px;
  height: auto;
  max-width: 397px;
  /* Last in the column, below the phone number (owner, 2026-10-27), so 36px above it, the margin
     it carried the last time it followed Call us; .contact-ways' 30px under the number collapses
     into it. Nothing under it. From 2026-10-25 until then it was first in the column, above Email
     us, with 70px above it (owner: "Add 40px of space above the 3 buttons", then "30px more") and
     30px below. */
  margin: 36px 0 0;
  padding: 0;
}
.contact-split__col .support-card--three .support-card__tile {
  min-height: 92px;
  gap: 8px;
  padding: 8px 2px;
  border-radius: 15px;
  /* Solid white at rest on /contact (owner, 2026-10-25: "The default background color of the 3
     buttons should be #FFFFFF"); the help articles' tiles keep the 60% white above. */
  background: #ffffff;
}
/* #F9F9F9 on hover (owner, 2026-10-25: "The hover color for each of the 3 buttons should be
   #F1F1F1", then "Try #F9F9F9"). Its own rule because the one above (0,3,0) outranks
   `.support-card__tile:hover` (0,2,0); the help articles' tiles keep that rule's 8% tint. */
.contact-split__col .support-card--three .support-card__tile:hover {
  background: #f9f9f9;
}
.contact-split__col .support-card--three .support-card__face { gap: 8px; }
.contact-split__col .support-card--three .support-card__icon,
.contact-split__col .support-card--three .support-card__spin {
  width: 28px;
  height: 28px;
}
.contact-split__col .support-card--three .support-card__label {
  font-size: 18px;
  line-height: 1.1;
  text-align: center;
  white-space: normal;
}
.contact-split__col .support-card--three .support-card__dot {
  top: 11px;
  right: 11px;
  width: 12px;
  height: 12px;
}
/* The Support Chat state (data-support-chat; site.js). Each face is the tile's own column layout,
   so the tile looks the same with either showing. This site has no global [hidden] rule. */
.support-card__face {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
}
.support-card__face[hidden],
.support-card__dot[hidden],
.support-card__spin[hidden] { display: none; }
.support-card__tile[data-support-chat] { position: relative; }
/* The app's availability dot: 9px, 8px in from the top right. Its green is this site's own
   --green, the colour every tick and "ON" here wears, rather than the app's #4CC38A. */
.support-card__dot {
  position: absolute;
  top: 8px;
  right: 8px;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--green);
}
/* While Beacon loads and the identity is fetched, a spinner takes the icon's 21px slot, as the
   app's Spinner does (aria-busy on the tile says the same to assistive tech). */
.support-card__spin {
  width: 21px;
  height: 21px;
  border: 2px solid rgba(29, 26, 24, 0.15);
  border-top-color: var(--coral);
  border-radius: 50%;
  animation: sales-spin 0.8s linear infinite;
}
.support-card__face:has(.support-card__spin:not([hidden])) .support-card__icon { display: none; }



/* ------------------------------------------------- platform pages: the guide
   The four platform pages are one document layout from the owner's design package
   (2026-09-24, "lotjitsu-platform-guide": README.md is the spec, reference/hibid.html the look).
   Markup: platformGuidePage() in scripts/site-pages-1.mjs; content: scripts/site-platform-guides.mjs.
   Every size is the README's, none under 18px; every colour is a site token except the seven the
   README names as new, which are local variables here rather than a second token set. The
   responsive rules are in the trailing 1000 and 850 blocks below, never a new width query (the
   README's 1220px gutter rule is not taken: .container already carries 24px gutters). */
.guide-head,
.guide,
.compat-head,
.compat {
  --guide-rule: #d9d1c9;
  --guide-dl-rule: #ece5df;
  --guide-quote-rule: #cfc5bc;
  --guide-callout-border: #bfb3a9;
  --guide-callout-fill: #f5f3f0;
  --guide-code-ink: #f3efeb;
  --guide-code-note: #a79d96;
  --guide-link-hover: #8a412f;
  --guide-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
}
/* One grid, 26px under the header like every page title: 260px sidebar, 35px gap, and the right
   column holding the head (h1 and summary) above the document, so the "At a glance" card's top is
   level with the h1 (owner, 2026-10-11). The switcher, the button, the 1px rule under the head and
   the summary's 760px cap were removed the same day. */
/* 30px at the bottom, not 120, so the last Q&A card sits 60px above the download card (owner,
   2026-10-11: "Make the gap only be 60px"). The phone's own bottom padding is in the 850 block. */
.guide { padding: 26px 0 30px; }
.guide-grid {
  display: grid;
  grid-template-columns: 260px minmax(0, 1fr);
  grid-template-areas:
    "aside head"
    "aside doc";
  grid-template-rows: auto 1fr;
  /* 35px, not the package's 72 (owner, 2026-10-11, after trying 36 and 40: "35px is better"). */
  column-gap: 35px;
  row-gap: 40px;
  align-items: start;
}
.guide-head { grid-area: head; display: flex; flex-direction: column; gap: 28px; }
.guide-head h1 { margin: 0; }
.guide-summary { margin: 0; }
.guide-aside { grid-area: aside; }
.guide-doc { grid-area: doc; }
/* Not sticky (owner, 2026-10-11: "the pages content should all scroll as one page"). It held
   "On this page" under the card too until the same day; the card is all that is left. */
.guide-aside__title {
  margin: 0 0 8px;
  font-size: 21px;
  line-height: 1.25;
}
.guide-glance {
  padding: 24px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.guide-glance__list { margin: 0; display: flex; flex-direction: column; }
.guide-glance__list > div {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 14px 0;
  border-bottom: 1px solid var(--guide-dl-rule);
}
.guide-glance__list > div:last-child { padding-bottom: 0; border-bottom: 0; }
.guide-glance__list dt { color: var(--muted); }
.guide-glance__list dd {
  margin: 0;
  line-height: 1.45;
  font-weight: 500;
  color: var(--ink);
}
/* Every row in one style. The photo names were set in JetBrains Mono until the owner asked for
   them to match the other rows (2026-10-11); the mono stack is still Figure 1's. */
/* The document: seven sections 40px apart (88 until the owner's 2026-10-11 "remove the extra
   spacing"; 40 is twice the 20px inside a section, so an h2 still reads as a new section); each
   section is a column of its own blocks. */
.guide-doc {
  display: flex;
  flex-direction: column;
  gap: 40px;
  min-width: 0;
}
.guide-section {
  display: flex;
  flex-direction: column;
  gap: 20px;
  /* No padding: the site-wide 30px section padding put 60px more between sections on top of the
     .guide-doc gap (owner, 2026-10-11: "Remove the 30px of padding above and below"). */
  padding: 0;
  scroll-margin-top: 98px;
}
.guide-section--upload { gap: 28px; }
.guide-section--cards { gap: 24px; }
.guide-section > h2,
.compat-section > h2,
.compat-card h2 {
  margin: 0;
  font-size: 26px;
  line-height: 1.2;
  letter-spacing: -0.01em;
}
.guide-section > p { margin: 0; }
/* Body-size text links use --coral-hover, not --coral: --coral on --surface is under 4.5:1. */
.guide-doc a { color: var(--coral-hover); text-decoration: underline; text-underline-offset: 3px; }
.guide-doc a:hover { color: var(--guide-link-hover); }
/* Figure 1: the dark filename panel, the filenames in the light ink and the notes in the muted
   one, monospace at the floor with 1.8 leading; it scrolls sideways rather than wrapping. */
.guide-figure {
  margin: 12px 0;
  display: flex;
  flex-direction: column;
  gap: 14px;
}
.guide-figure__panel {
  background: var(--dark);
  border-radius: var(--radius);
  padding: 28px 32px;
  overflow-x: auto;
}
.guide-figure pre {
  margin: 0;
  font-family: var(--guide-mono);
  font-size: 1.125rem;
  line-height: 1.8;
  color: var(--guide-code-ink);
  white-space: pre;
}
.guide-figure__note { color: var(--guide-code-note); }
.guide-figure figcaption { line-height: 1.5; color: var(--muted); }
.guide-figure figcaption strong { font-weight: 600; color: var(--body-text); }
/* The five upload steps in a row, each under a 1px rule; step 5 is the one that is yours, so
   its circle and rule are coral. */
.guide-steps {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 20px;
  list-style: none;
  margin: 0;
  padding: 0;
}
.guide-step {
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding-top: 20px;
  border-top: 1px solid var(--guide-rule);
}
.guide-step--last { border-top: 2px solid var(--coral); }
.guide-step__n {
  flex-shrink: 0;
  width: 44px;
  height: 44px;
  border-radius: 22px;
  background: var(--dark);
  color: #ffffff;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-head);
  font-weight: 600;
  font-size: 19px;
}
.guide-step--last .guide-step__n { background: var(--coral); }
.guide-step__body { display: flex; flex-direction: column; gap: 6px; line-height: 1.5; }
.guide-step__title {
  font-family: var(--font-head);
  font-weight: 600;
  font-size: 20px;
  line-height: 1.3;
  color: var(--ink);
}
/* The dashed callout with the lock. */
.guide-callout {
  display: flex;
  gap: 20px;
  align-items: flex-start;
  padding: 28px;
  border: 2px dashed var(--guide-callout-border);
  border-radius: var(--radius);
  background: var(--guide-callout-fill);
}
.guide-callout__icon { flex-shrink: 0; margin-top: 2px; color: var(--ink); }
.guide-callout p { margin: 0; line-height: 1.4444; }
.guide-callout strong { color: var(--ink); font-weight: 600; }
/* The health check as five white cards, two up, each with a green check. */
.guide-checks {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 16px 20px;
  list-style: none;
  margin: 0;
  padding: 0;
}
.guide-checks li {
  display: flex;
  gap: 16px;
  align-items: flex-start;
  padding: 22px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.guide-check__icon { flex-shrink: 0; }
.guide-checks li > div { display: flex; flex-direction: column; gap: 4px; line-height: 1.5; }
.guide-checks strong { color: var(--ink); font-weight: 600; }
/* The pull quote: ruled above and below, no left border, the heading face at 34px. */
.guide-quote {
  margin: 20px 0;
  padding: 32px 0;
  border-top: 1px solid var(--guide-quote-rule);
  border-bottom: 1px solid var(--guide-quote-rule);
}
.guide-quote p {
  margin: 0;
  font-family: var(--font-head);
  font-weight: 600;
  font-size: 34px;
  line-height: 1.2;
  letter-spacing: -0.015em;
  color: var(--ink);
}
/* The Q&A as eight white cards, two up, h3 questions at 20px. */
.guide-qa {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 20px;
}
.guide-qa__item {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 26px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.guide-qa__item h3 {
  margin: 0;
  font-size: 20px;
  line-height: 1.35;
}
.guide-qa__item p { margin: 0; line-height: 1.4444; }


/* ------------------------------------------------- /compatibility: the directory
   The owner's design package of 2026-09-24 ("lotjitsu-compatibility-page"; the README is the
   spec). It shares the guide pages' local variables, mono stack and h2 rule above. Markup:
   `compatibility` in scripts/site-pages-1.mjs; copy: scripts/site-compatibility.mjs and the
   guides. Responsive rules are in the trailing 1000 and 850 blocks below. */
.compat-head { padding: 26px 0 0; }
.compat-head__inner {
  display: flex;
  flex-direction: column;
  gap: 20px;
  /* 16px, not the package's 56px (owner, 2026-09-24: "remove 40px of spacing below the
     'Start Cataloging - It's Free!' button"). */
  padding-bottom: 16px;
  /* The rule that closed this head is gone (owner, 2026-10-01), along with the one over each of
     the three "same on every platform" items below. Spacing is unchanged. */
}
.compat-head h1 { margin: 0; }
/* Full container width (owner, 2026-10-06: "Compatibility section should span the full width of
   the page when viewed on a desktop"). It was capped at 760px, which wrapped it at about the
   middle of a desktop. */
.compat-summary { margin: 0; }
/* The head's button is gone (owner, 2026-10-11: "Remove the 'Manage Catalogs' button"); the
   16px under the summary and the 26px below are unchanged. */
/* 30px less under the head's button on desktop (owner, 2026-10-01): the section's top padding
   was 56px, now 26. The phone block takes 15px off its own 40px. */
/* 75px less under the last row of cards on desktop (owner, 2026-10-01, as 50 then 25 more):
   120px bottom padding, now 45. The phone block keeps its own 72px. */
.compat { padding: 26px 0 45px; }
.compat__inner { display: flex; flex-direction: column; gap: 96px; }
.compat-section {
  display: flex;
  flex-direction: column;
  gap: 20px;
  padding: 0;
}
.compat-section--same { gap: 24px; }
/* "Streamline Your Work" 2px larger (owner, 2026-10-01): 28px, from the shared 26px h2 size. The
   phone block sets its own, also 2 up. The card titles beneath it are not touched. */
.compat-section--same > h2 { font-size: 28px; }
.compat-section > p { margin: 0; max-width: 760px; }
/* The four cards: a title, one paragraph that grows so the links line up across a row, and a
   "Read the X guide" link with an arrow, at least 44px tall, underlined on hover only. */
.compat-cards {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 24px;
  list-style: none;
  margin: 0;
  padding: 0;
}
.compat-card {
  display: flex;
  flex-direction: column;
  gap: 18px;
  padding: 32px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.compat-card p { margin: 0; flex-grow: 1; }
.compat-card__more {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  font-weight: 500;
  color: var(--coral-hover);
  text-decoration: none;
}
.compat-card__more:hover {
  color: var(--guide-link-hover);
  text-decoration: underline;
  text-underline-offset: 3px;
}
/* Three items with a stroke icon (their top rules were removed 2026-10-01). */
.compat-same {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 24px;
  list-style: none;
  margin: 0;
  padding: 0;
}
.compat-same li {
  /* Icon LEFT of the sub-header, description beneath across both (owner, 2026-10-01, desktop).
     The phone block below puts it back to a column with the icon above. */
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: 12px 14px;
  /* Each item is a card now (owner, 2026-10-01), the same surface, border, radius and 32px
     padding as the four platform cards above. Desktop and phone. */
  padding: 32px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.compat-same li > svg { grid-column: 1; grid-row: 1; }
.compat-same li > strong { grid-column: 2; grid-row: 1; }
.compat-same li > span { grid-column: 1 / -1; grid-row: 2; }
.compat-same svg { color: var(--ink); }
.compat-same strong {
  font-family: var(--font-head);
  font-weight: 600;
  /* 24px: 6 up from 20, then 2 down (owner, 2026-10-01). The phone block sets its own size. */
  font-size: 24px;
  line-height: 1.3;
  color: var(--ink);
}

/* ---- blog and help responsive: the approved 1000 and 850 only ---- */
@media (max-width: 1000px) {
  .post-grid { grid-template-columns: repeat(2, 1fr); }
  /* The sidebar has nowhere to go at this width, so the contents list moves
     inline above the article and stops being sticky. Unstick the ASIDE, which
     is where position: sticky lives (see the note on .post-aside above). */
  .post-layout { grid-template-columns: 1fr; gap: 30px; }
  .post-aside { position: static; }
  /* The help article's three columns become one. The article comes first, then the left column
     (its search field; its table of contents goes, because a contents list sitting BELOW the
     text it indexes is furniture), then the right column's topic list and Support card as a
     footer nav. Nothing sticks once each is a full-width band. */
  .help-layout { grid-template-columns: minmax(0, 1fr); gap: 36px; }
  .help-layout > .help-main { grid-column: 1; grid-row: 1; }
  .help-layout > .help-aside { grid-column: 1; grid-row: 2; }
  .help-layout > .help-rail { grid-column: 1; grid-row: 3; padding-top: 0; }
  .help-article__head,
  .help-aside__search { margin-top: 0; }
  .help-aside,
  .help-rail { position: static; }
  .help-toc,
  .help-toc__label { display: none; }
  /* One column: the head, then the "At a glance" card, then the document. */
  .guide-grid {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "head" "aside" "doc";
    grid-template-rows: auto;
  }
  /* /compatibility: the three "same on every platform" items stack. */
  .compat-same { grid-template-columns: minmax(0, 1fr); }
}

@media (max-width: 850px) {
  /* The guide on a phone: one column everywhere, steps as rows with the circle on the left,
     the h2 and the quote a step down, the head and body rhythm tightened. */
  .guide { padding: 26px 0 72px; }
  /* The callout and Q&A paragraphs take the site's one body line-height (1.4444 since 2026-10-06), like every other
     paragraph on the phone (owner, 2026-09-30; see the body-text note in the main 850 block). */
  .guide-callout p,
  .guide-qa__item p { line-height: 1.4444; }
  /* /compatibility's "same on every platform" items on the phone (owner, 2026-09-30): each line
     icon centred above its sub-header at the column's width less 30px a side (it was 28px),
     square by its own viewBox, and the sub-header 8px larger (20px to 28) and centred. The
     description beneath keeps its left alignment. */
  .compat-same li { display: flex; flex-direction: column; align-items: stretch; gap: 12px; }
  .compat-same svg { width: calc(100% - 60px); height: auto; margin: 0 auto; }
  /* 32px: 6 up from 28, then 2 down (owner, 2026-10-01, the same changes the desktop took). */
  .compat-same strong { font-size: 32px; text-align: center; }
  .guide-checks,
  .guide-qa,
  .guide-steps { grid-template-columns: minmax(0, 1fr); }
  .guide-step { flex-direction: row; align-items: flex-start; gap: 18px; }
  .guide-section > h2,
  .compat-section > h2,
  .compat-card h2 { font-size: 24px; }
  .compat-section--same > h2 { font-size: 26px; }
  .guide-quote p { font-size: 26px; }
  /* /compatibility on a phone: the cards stack; the table keeps its 760px minimum and scrolls
     inside its card. */
  /* 15px less under the head's button on the phone (owner, 2026-10-01): 40px to 25. */
  .compat { padding: 25px 0 72px; }
  .compat-head__inner { padding-bottom: 16px; }
  .compat__inner { gap: 64px; }
  .compat-cards { grid-template-columns: minmax(0, 1fr); }
  .post-grid { grid-template-columns: 1fr; }
  .blog-layout { grid-template-columns: 1fr; }
  .cat-nav { justify-content: flex-start; }
  .blog-layout__main { grid-column: auto; }
  .featured { grid-template-columns: 1fr; }
  .featured__body { padding: 28px; }
  .featured__media img { min-height: 0; aspect-ratio: 16 / 9; }
  /* Title over date rather than beside it: at 390px there is not room for both
     on one line without the date wrapping mid-string. */
  .archive-item { flex-direction: column; align-items: flex-start; gap: 9px; }
  .blank-slate { padding: 44px 22px; }
  .blank-slate__actions .btn { width: 100%; }
  .blog-search__row .btn { width: 100%; }
  /* Help Center: the category cards stack, and Back to Help spans the column like the site's
     other mobile buttons. */
  .help-categories { grid-template-columns: 1fr; gap: 18px; }
  .help-category { padding: 22px; }
}

/* ================================================================== pricing (D-071) */
/* /pricing, moved here from the app (owner, 2026-10-08 and 2026-10-09; alpha's
   MOVE_PRICING_TO_MARKETING_PLAN.md, Phase 3a). A port of the app's
   apps/web/src/app/pricing/plans.css: its `.lj-plans ` prefix becomes `.pricing-page ` (the page's
   body class, so nothing here can restyle another page) and its `--lj-x` tokens become this
   file's `--x`, which are the same values. The rules are the app's, value for value; the comments
   keep the WHY and point at the app file for the full history. Markup: scripts/site-pricing.mjs.
   The sub-18px declarations each carry a dated owner approval: the page moves verbatim, and the
   app's own floor is 16px. The savings-calculator corner card is NOT ported (plan Appendix C,
   C11): its dialog lives on the home page only, so the corner cell is empty here. */

/* Tokens the app's plans.css has and this file did not, at the app's values (plans.css:117-163). */
:root {
  --section-dark: #404040;
  --belt-tint-white: #f2f2f2;
  --belt-tint-yellow: #fcf2d8;
  --belt-tint-green: #def2e6;
  --belt-tint-blue: #e3ebfc;
  --belt-tint-black: #dedfe1;
  --belt-outline: rgba(29, 26, 24, 0.4);
  --danger: #b3261e;
  --danger-wash: #fbeceb;
  /* The destructive button wears the Sign Out button's colours (owner, 2026-10-15: "The style
     used for the 'Confirm Change' button (and all other buttons with this style) needs to be
     replaced by the style we use for the 'Sign Out' button"): #b3261e with white ink, #971b1c on
     hover (.signout-dialog__confirm). It was the app's `destructive` variant, a #422121 ground
     with #c08080 ink; the two already met on hover. Size and font stay the submit's, so it still
     pairs with the Cancel beside it. */
  --destructive: #b3261e;
  --destructive-ink: #fff;
  --destructive-hover: #971b1c;
  --destructive-hover-ink: #f1f1f1;
  /* Confirm Change's own colours (owner, 2026-10-19): see .login-dialog__submit--caution. */
  --caution: #bb5e0c;
  --caution-hover: #b65601;
}

/* The page owns its hide state: this site deliberately has no global [hidden] rule. */
.pricing-cmp [hidden] {
  display: none;
}

/* The table's section: 19px under the header (owner, 2026-09-19, app page.tsx), the app's 72px
   below (52px on a phone, in the 850 block). */
.pricing-page section.pricing-cmp {
  padding: 19px 0 72px;
}

/* ---- belt strip and the Get Started pill (plans.css:841-1050) ----
   Scoped copies of the /sections rules above, so a change to the catalog's cards cannot move the
   pricing table. The pill is 10px tall padding and 16px type here, against the card's 14px/18px. */
.pricing-page .plan__strip {
  height: 6px;
  width: 100%;
}
.pricing-page .plan--white .plan__strip {
  background: #d9d9d9;
}
.pricing-page .plan--yellow .plan__strip {
  background: #f0b429;
}
.pricing-page .plan--green .plan__strip {
  background: #16a34a;
}
.pricing-page .plan--blue .plan__strip {
  background: #2563eb;
}
.pricing-page .plan--black .plan__strip {
  background: #111827;
}
/* No top padding (owner, 2026-10-12: the buttons "should be vertically aligned middle in the
   cells"). It was 4px, carried over from the app's pricing page, and sat each button 18px under
   the cell's top line against 14px over its bottom. */
.pricing-page .plan__cta {
  margin-top: 0;
  padding-top: 0;
}
/* The queued-change line: "Starts <date>", bold, one line, with the cancel link under it. */
.pricing-page .plan__state[data-cta-kind="pending"] {
  text-align: center;
  min-height: 0;
  margin-bottom: 2px;
  font-weight: 700;
  /* 15px: approved by the owner 2026-10-09 (D-071: the pricing page moves verbatim from the
     app's /pricing, plans.css:907; the app approved it 2026-10-06). */
  font-size: 15px;
}
/* The centred red cancel under a queued change: a <button> dressed as a link. --danger is the
   red INK, not the destructive button fill, which would be far too loud in a table cell. */
.pricing-page .plan__state-action {
  display: block;
  margin: 0 auto;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  /* 14px: approved by the owner 2026-10-11 ("Reduce the font-size of 'Cancel Downgrade' link by
     2px"), from the 16px approved 2026-10-09 (D-071, the app's plans.css:923). */
  font-size: 14px;
  color: var(--danger);
  text-decoration: underline;
  cursor: pointer;
}
/* The underline stays on hover: it is what marks this as a control. The wash is the feedback. */
.pricing-page .plan__state-action:hover:not(:disabled) {
  background: var(--danger-wash);
  text-decoration: underline;
}
.pricing-page .plan__state-action:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}
/* "Your Plan", bold and 22px (owner: 20px on 2026-10-06, 2px larger on 2026-10-15), and
   "Current" on the plan held while a downgrade is queued, the same text in place of a disabled
   pill (owner, 2026-10-12), so the cell's label keeps one size. */
.pricing-page .plan__state[data-cta-kind="current"],
.pricing-page .plan__state[data-cta-kind="cancel_downgrade"] {
  font-weight: 700;
  font-size: 22px;
}
/* The read-only states are prose, centred both ways in the cell; min-height matches the pill so a
   row mixing the two keeps one baseline. `white-space: normal` undoes the cells' nowrap; no margin,
   now that .plan__cta has no top padding to cancel. #1a1a1a is the owner's colour, not --ink. The
   buttons' face and size, so prose and pill read as one kind of text across a row. */
.pricing-page .plan__state {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  white-space: normal;
  margin: 0;
  text-align: center;
  color: #1a1a1a;
  font-family: var(--font-head);
  /* 16px: approved by the owner 2026-10-09 (D-071: the pricing page moves verbatim from the
     app's /pricing, plans.css:979). */
  font-size: 16px;
}
.pricing-page .plan__pill {
  display: block;
  width: 100%;
  text-align: center;
  border-radius: 999px;
  padding: 10px 16px;
  font-family: var(--font-head);
  /* 16px: approved by the owner 2026-10-09 (D-071: the pricing page moves verbatim from the
     app's /pricing, plans.css:997; the app reduced button text to 16px on 2026-09-19). */
  font-size: 16px;
  font-weight: 600;
  border: 1px solid transparent;
  transition: background 0.15s;
  white-space: nowrap;
  cursor: pointer;
}
/* A control that will not act must not claim it will (the app's note on the pill, plans.css:999). */
.pricing-page .plan__pill:disabled {
  cursor: default;
}
/* "Current" while a downgrade is queued keeps the pill's look but is never clickable (owner,
   2026-10-11): disabled in site.js, and no hover change, so it does not look pressable either. */
.pricing-page .plan__pill[data-cta-kind="cancel_downgrade"] {
  pointer-events: none;
}
.pricing-page .plan--white .plan__pill {
  background: #f3f4f6;
  color: #111827;
  border-color: #d4d4d4;
}
.pricing-page .plan--white .plan__pill:hover {
  background: #fff;
  color: #111827;
}
.pricing-page .plan--yellow .plan__pill {
  background: #f0b429;
  color: #111827;
}
.pricing-page .plan--yellow .plan__pill:hover {
  background: #f6c14b;
  color: #111827;
}
.pricing-page .plan--green .plan__pill {
  background: #16a34a;
  color: #fff;
}
.pricing-page .plan--green .plan__pill:hover {
  background: #1cb455;
  color: #fff;
}
.pricing-page .plan--blue .plan__pill {
  background: #2563eb;
  color: #fff;
}
.pricing-page .plan--blue .plan__pill:hover {
  background: #3572f2;
  color: #fff;
}
.pricing-page .plan--black .plan__pill {
  background: #111827;
  color: #fff;
}
.pricing-page .plan--black .plan__pill:hover {
  background: #1f2937;
  color: #fff;
}

/* ---- the Monthly / Yearly toggle (plans.css:1106-1158) ----
   Two real buttons in a pill track. The pressed one is coral with white text, which passes only as
   LARGE text, hence 19px at 700 in both states (so pressing never changes the width). Only the
   pressed button carries a border, coral on coral, so forced colours can still show which period
   is chosen; its padding gives back the 2px. */
.pricing-page .billing {
  display: flex;
  width: fit-content;
  max-width: 100%;
  margin: 0 auto 24px;
  padding: 4px;
  gap: 4px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--band);
  /* The whole pill is the control (owner, 2026-11-04: "This toggle button also needs to toggle
     anywhere you click it"); site.js flips the interval on any click inside it. */
  cursor: pointer;
}
.pricing-page .billing__option {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  margin: 0;
  padding: 8px 20px;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--ink);
  font-family: var(--font-head);
  font-size: 19px;
  font-weight: 700;
  line-height: 1.4;
  white-space: nowrap;
  cursor: pointer;
  transition:
    background 0.15s,
    color 0.15s;
}
.pricing-page .billing__option:hover {
  background: var(--surface);
}
.pricing-page .billing__option[aria-pressed="true"] {
  padding: 6px 18px;
  border: 2px solid var(--coral);
  background: var(--coral);
  color: var(--card);
}
.pricing-page .billing__option:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}
.pricing-page .billing__badge {
  padding: 1px 10px;
  border-radius: 999px;
  background: var(--card);
  color: var(--coral-hover);
  font-size: 18px;
  font-weight: 700;
}

/* ---- the comparison table (plans.css:1160-1860) ----
   The frame (white, 1px line, 14px radius) is drawn by the CELLS, not the table: the header's
   corner, the price label and the first button row's label are blank and sit outside it, so the
   frame is an L. That needs `border-collapse: separate`, and it is why the site's own
   `table.plain` border, radius, row zebra and last-row rule are taken back off below. */
/* The title is the page's one h1, wearing the h2 look it had (owner, 2026-09-19). */
.pricing-page .cmp__title {
  font-family: var(--font-head);
  color: var(--ink);
  line-height: 1.15;
  margin: 0 0 0.5em;
  font-weight: 600;
  letter-spacing: -0.015em;
  font-size: 34px;
}
/* Four columns, the first matching the 220px label column, so the toggle in the third is centred
   on the five belts rather than the table (owner, 2026-09-22). 220px is the coupling with
   .cmp__label-col. The fourth column holds [data-pricing-title-actions], filled by site.js. */
.pricing-page .cmp__titlebar {
  display: grid;
  grid-template-columns: 220px minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  margin: 0 0 24px;
}
.pricing-page .cmp__titlebar .cmp__title {
  grid-column: 1 / 3;
  justify-self: start;
  margin: 0;
  text-align: left;
}
.pricing-page .cmp__titlebar .billing {
  margin: 0;
}
.pricing-page .cmp__titlebar [data-pricing-title-actions] {
  justify-self: end;
}
/* Empty until site.js fills it; an empty grid item would add a row gap once the row stacks. */
.pricing-page .cmp__titlebar [data-pricing-title-actions]:empty {
  display: none;
}
/* The signed-in Payment Methods button (site.js builds it) is a small button, not the full-size
   outline: 16px text and 4px less padding each way than .btn's 13px 26px (owner, 2026-10-11).
   Its 16px label is an owner-approved exception to the 18px text floor (2026-10-11), for this
   button only. */
.pricing-page .pricing-methods-button {
  font-size: 1rem;
  padding: 9px 22px;
}
/* Hovered, it takes the primary action's colours, .btn--primary's #c16850 fill with white text,
   instead of the outline's dark fill (owner, 2026-10-11). The border goes the same colour so the
   button keeps its size. */
.pricing-page .pricing-methods-button:hover {
  background: #c16850;
  border-color: #c16850;
  color: #fff;
}
/* `position: relative` is load-bearing: the absolute .cmp__sr spans would otherwise resolve
   against the page and make it scroll sideways on a phone (app note 5). */
.pricing-page .table-wrap {
  position: relative;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
.pricing-page table.plain {
  width: 100%;
  min-width: 1020px; /* 220 label + 5 x 160 */
  table-layout: fixed;
  border-collapse: separate;
  border-spacing: 0;
  border: 0;
  border-radius: 0;
  overflow: visible;
  background: transparent;
  font-size: 18px;
  line-height: 1.4;
}
.pricing-page table.plain tbody tr {
  background: transparent;
}
.pricing-page table.plain .cmp__label-col {
  width: 220px;
}
.pricing-page table.plain th,
.pricing-page table.plain td {
  padding: 14px 12px;
  border-bottom: 1px solid var(--line);
  text-align: center;
  vertical-align: middle;
  background: var(--card);
  font-weight: 400;
  color: var(--body-text);
}
/* Every row draws its bottom line, the last of each body included. */
.pricing-page table.plain tbody tr:last-child td {
  border-bottom: 1px solid var(--line);
}
.pricing-page table.plain tbody th {
  text-align: left;
  padding-left: 16px;
  font-weight: 600;
  color: var(--ink);
}.pricing-page table.plain thead th,
.pricing-page table.plain thead td {
  background: var(--band);
  font-family: var(--font-head);
  font-size: 18px;
  font-weight: 700;
  color: var(--ink);
  padding: 20px 8px 14px;
}
/* Each tier's header cell wears a light tint of its own belt (owner, 2026-09-19). */
.pricing-page table.plain thead th.plan--white {
  background: var(--belt-tint-white);
}
.pricing-page table.plain thead th.plan--yellow {
  background: var(--belt-tint-yellow);
}
.pricing-page table.plain thead th.plan--green {
  background: var(--belt-tint-green);
}
.pricing-page table.plain thead th.plan--blue {
  background: var(--belt-tint-blue);
}
.pricing-page table.plain thead th.plan--black {
  background: var(--belt-tint-black);
}
/* The strip is absolute at the top of its header cell, which clips it for the rounded corners. */
.pricing-page table.plain .cmp__col {
  position: relative;
  overflow: hidden;
}
.pricing-page table.plain .plan__strip {
  display: block;
  position: absolute;
  top: 0;
  left: 0;
}
.pricing-page table.plain .cmp__tier {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 4px 8px;
}
/* A hairline outline round the belt's silhouette, so White reads on its pale cell (owner,
   2026-09-19). On the wrapper, so it composes with the knot's own filter. */
.pricing-page table.plain .cmp__belt {
  display: flex;
  flex: 0 0 auto;
  filter: drop-shadow(0 0 1px var(--belt-outline))
    drop-shadow(0 0 0.5px var(--belt-outline));
}
/* The knot at the app's scale 0.5: 31 x 19, and the pinstripe scaled with it (1/2/3px), so the
   weave keeps its proportion. The site's .belt-knot recipe is the app's BeltKnot. */
.pricing-page .cmp__belt .belt-knot {
  width: 31px;
  height: 19px;
}
.pricing-page .cmp__belt .belt-knot .k2 {
  background:
    repeating-linear-gradient(
      90deg,
      color-mix(in srgb, var(--belt) 84%, white) 0 1px,
      color-mix(in srgb, var(--belt) 96%, black) 1px 2px,
      color-mix(in srgb, var(--belt) 86%, white) 2px 3px
    ),
    linear-gradient(
      180deg,
      color-mix(in srgb, var(--belt) 88%, white) 0%,
      var(--belt) 45%,
      color-mix(in srgb, var(--belt) 82%, black) 100%
    );
}
/* The zebra, per body, on the CELLS (the sticky cells must be opaque). The first body counts
   only the rows that are always there, so the Yearly row never flips the stripes; the platform
   group tints its even rows; the foot row is always tinted. One rule on purpose: an engine that
   cannot parse "of S" then drops all of it rather than striping half a table (app note 9). */
.pricing-page
  table.plain
  tbody:not(.cmp__group)
  tr:nth-child(even of :not(.cmp__yearly))
  th,
.pricing-page
  table.plain
  tbody:not(.cmp__group)
  tr:nth-child(even of :not(.cmp__yearly))
  td,
.pricing-page table.plain .cmp__group tr:nth-child(even) th,
.pricing-page table.plain .cmp__group tr:nth-child(even) td,
.pricing-page table.plain tfoot td {
  background: #fdfbfa;
}
/* The signed-in account's plan column (owner, 2026-10-06): site.js sets data-current="<tier>" on
   that column's cells below the belt name. Built to match the zebra's specificity and follow it,
   so the tint wins on striped rows and the foot. Black and White share #f2f2f2. */
.pricing-page
  table.plain
  tbody
  tr
  > td:is([data-current="white"][data-current][data-current]),
.pricing-page
  table.plain
  tfoot
  tr
  > td:is([data-current="white"][data-current][data-current]) {
  background: #f2f2f2;
}
.pricing-page
  table.plain
  tbody
  tr
  > td:is([data-current="yellow"][data-current][data-current]),
.pricing-page
  table.plain
  tfoot
  tr
  > td:is([data-current="yellow"][data-current][data-current]) {
  background: #fcf2d8;
}
.pricing-page
  table.plain
  tbody
  tr
  > td:is([data-current="green"][data-current][data-current]),
.pricing-page
  table.plain
  tfoot
  tr
  > td:is([data-current="green"][data-current][data-current]) {
  background: #def2e6;
}
.pricing-page
  table.plain
  tbody
  tr
  > td:is([data-current="blue"][data-current][data-current]),
.pricing-page
  table.plain
  tfoot
  tr
  > td:is([data-current="blue"][data-current][data-current]) {
  background: #e2ebfc;
}
.pricing-page
  table.plain
  tbody
  tr
  > td:is([data-current="black"][data-current][data-current]),
.pricing-page
  table.plain
  tfoot
  tr
  > td:is([data-current="black"][data-current][data-current]) {
  background: #f2f2f2;
}
/* ...and outlined, from its belt name to the foot (owner, 2026-10-15). site.js lays one box over
   the column (placeCurrentOutline): above the plain cells, under a phone's sticky labels, which
   come after it. 3px top and bottom, 1px down the sides (owner: "Make the line on the sides
   1px"). The colour is the column's own strip (owner: "should match the color bar at the top of
   the column"), and it has no rounded corners (owner: "There should never be a corner radius in
   the top-left corner of a column"; 2026-11-04, the same for the top-right when Black Belt is the
   highlighted column). The header cell under it squares its own corner to match, below. */
.pricing-page .cmp__current-outline {
  position: absolute;
  z-index: 1;
  box-sizing: border-box;
  border: 1px solid #111827;
  border-width: 3px 1px;
  border-radius: 0;
  pointer-events: none;
}
.pricing-page .cmp__current-outline[data-current="white"] {
  border-color: #d9d9d9;
}
.pricing-page .cmp__current-outline[data-current="yellow"] {
  border-color: #f0b429;
}
.pricing-page .cmp__current-outline[data-current="green"] {
  border-color: #16a34a;
}
.pricing-page .cmp__current-outline[data-current="blue"] {
  border-color: #2563eb;
}
.pricing-page .cmp__current-outline[data-current="black"] {
  border-color: #111827;
}
.pricing-page table.plain tbody td {
  white-space: nowrap;
}
.pricing-page table.plain tbody td strong {
  font-weight: 700;
  color: var(--ink);
}
/* The allowance rows' values (Monthly Lots, Catalogs, Photos per lot, Collaborators): bold, in
   #626262 (owner, 2026-11-02; they were --ink like the price). */
.pricing-page table.plain .cmp__num {
  font-weight: 700;
  color: #626262;
}
/* The price row: the money 22px, "Free" 24px, "/month" 16px. The line under the price is the
   button row's top line, so the price cells draw none. */
.pricing-page table.plain .cmp__price {
  padding-left: 8px;
  padding-right: 8px;
  border-bottom: 0;
}
/* In Yearly (site.js sets `is-yearly` on the section) the amount sits right on "billed yearly" in
   the row beneath, so the price cells drop their bottom padding (owner, 2026-10-14). */
.pricing-page .is-yearly table.plain .cmp__price {
  padding-bottom: 0;
}
.pricing-page table.plain .cmp__price strong {
  font-size: 22px;
}
.pricing-page table.plain .cmp__price strong:only-child {
  font-size: 24px;
}
.pricing-page table.plain .cmp__price strong + span {
  /* 16px: approved by the owner 2026-10-09 (D-071: the pricing page moves verbatim from the
     app's /pricing, plans.css:1577; the app's 2026-09-19 type for "/month"). */
  font-size: 16px;
}
/* In Yearly the price cell holds the year's bill and no per-month figure (owner, 2026-10-14: "do
   not display the monthly equivalence"). The amount: 22px at 800 in #1e1a17, both asked for
   together (owner, 2026-09-21), when it sat in the yearly row. Placed after `strong:only-child`,
   whose 24px it must beat at equal specificity. */
.pricing-page table.plain .cmp__price strong.cmp__price--yearly {
  color: #1e1a17;
  font-size: 22px;
  font-weight: 800;
}
/* "Free" stays centred in Yearly (owner, 2026-10-11: it "sticks to the top"). White Belt has no
   yearly total, so its price cell and the empty cell beneath read as one box; the word moves down
   half of the 20px Yearly adds (the 34px row, less the 14px the price cells give up) so it sits in
   the middle of both. `position: relative` moves it
   without changing either row's height. The yearly copy of the price shows only in Yearly; the
   paid belts' yearly amount is the only other lone <strong> there, and stays put. */
.pricing-page
  table.plain
  .cmp__price
  [data-interval-show="yearly"]
  > strong:only-child:not(.cmp__price--yearly) {
  position: relative;
  top: 10px;
}
/* The yearly bill row: one block with the price (no top padding, no line), "billed yearly" under
   the amount in the price cell, nowrap (app note 9). 34px: the 20px line and 14px below it. */
.pricing-page table.plain .cmp__yearly > th {
  padding-top: 0;
  padding-bottom: 0;
}
.pricing-page table.plain .cmp__yearly-total {
  padding: 0 8px 14px;
  border-bottom: 0;
  /* 16px: approved by the owner 2026-10-09 (D-071: the pricing page moves verbatim from the
     app's /pricing, plans.css:1642). */
  font-size: 16px;
  line-height: 20px;
  color: var(--ink);
}
.pricing-page table.plain .cmp__yearly-part {
  display: block;
  white-space: nowrap;
}
.pricing-page table.plain .cmp__cta {
  padding: 14px 8px;
}
/* The frame, cell by cell. Every cell draws its own bottom line; these add the rest. */
.pricing-page table.plain tbody th,
.pricing-page table.plain .cmp__corner {
  border-left: 1px solid var(--line);
}
.pricing-page table.plain tr > :last-child {
  border-right: 1px solid var(--line);
}
.pricing-page table.plain thead th:last-child {
  border-top-right-radius: var(--radius);
}
/* ...except when the last column is the account's current plan: its outline is square, so the
   corner goes square with it (owner, 2026-11-04). */
.pricing-page table.plain thead th:last-child[data-current] {
  border-top-right-radius: 0;
}
/* The bottom corners are the foot row's: there are two bodies, so never `tbody tr:last-child`. */
.pricing-page table.plain tfoot tr:last-child td:first-child {
  border-bottom-left-radius: var(--radius);
}
.pricing-page table.plain tfoot tr:last-child td:last-child {
  border-bottom-right-radius: var(--radius);
}
/* The blank cells: no background, no border, the page showing through. */
.pricing-page table.plain thead tr > .cmp__blank,
.pricing-page table.plain tbody tr > .cmp__blank {
  background: transparent;
  border: 0;
}
.pricing-page table.plain .cmp__blank + th,
.pricing-page table.plain .cmp__blank + td {
  border-left: 1px solid var(--line);
}
.pricing-page table.plain thead .cmp__blank + th {
  border-top-left-radius: var(--radius);
}
/* ...except when that first column is the account's current plan: its outline is square, so a
   rounded cell under it left the page showing in the corner (owner, 2026-11-04: "there should be
   no corner radius used the top-left of the highlighted belt column"). */
.pricing-page table.plain thead .cmp__blank + th[data-current] {
  border-top-left-radius: 0;
}
/* Column lines between the tier cells of the price and yearly rows (owner, 2026-09-19) and of
   the button rows (owner, 2026-10-12: "There should be vertical lines between these cells"). */
.pricing-page table.plain .cmp__price + .cmp__price,
.pricing-page table.plain .cmp__yearly-total + .cmp__yearly-total,
.pricing-page table.plain .cmp__cta + .cmp__cta {
  border-left: 1px solid var(--line);
}
/* The line under the price belongs to the first button row (`tbody` only: the foot's repeat has
   the last platform row's line above it). */
.pricing-page table.plain tbody .cmp__cta-row > * {
  border-top: 1px solid var(--line);
}
/* The first button row's label cell is blank too (owner, 2026-10-01), matched to the zebra's
   specificity to take its tint back off. */
.pricing-page
  table.plain
  tbody:not(.cmp__group)
  tr.cmp__cta-row
  > td.cmp__corner {
  background: transparent;
  border: 0;
}
.pricing-page table.plain tbody .cmp__cta-row > .cmp__corner + td {
  border-left: 1px solid var(--line);
}
/* The inner corner, one row lower: the button row hands its bottom line to the row after it. */
.pricing-page table.plain tbody .cmp__cta-row > td {
  border-bottom: 0;
}
.pricing-page table.plain tbody .cmp__cta-row + tr > * {
  border-top: 1px solid var(--line);
}
.pricing-page table.plain tbody .cmp__cta-row + tr > th {
  border-top-left-radius: var(--radius);
}
/* THE SAVINGS-CALCULATOR CORNER CARD (the app repo's MOVE_PRICING_TO_MARKETING_PLAN.md, Appendix
   C C11, 2026-10-09), ported from the app's plans.css `.cmp__calc-card` rules value for value. It is
   laid over the blank cells below the header's corner rather than given a rowSpan, so the price
   row keeps its own <th>. `top` is a literal tied to the header and price row heights (77px), and
   site.js adds `cmp__calc-card--yearly` while Yearly shows, 20px taller. */
.pricing-page table.plain thead .cmp__corner {
  position: relative;
}
.pricing-page .cmp__calc-card {
  position: absolute;
  /* 77px in the app's plans.css (the header and price row heights); 8px higher by the owner,
     2026-10-13, with the 8px-taller card. The yearly offset below moves with it. */
  top: 69px;
  left: 0;
  width: 207px;
  /* 114px in the app's plans.css; +8px by the owner, 2026-10-13, after the title and subtitle
     grew 2px each. */
  height: 122px;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 0 15px;
  margin: 0;
  appearance: none;
  text-align: left;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  cursor: pointer;
  transition:
    box-shadow 0.3s ease,
    border-color 0.3s ease;
}
/* 69px plus the 20px Yearly adds: the 34px "billed yearly" row less the price cells' 14px bottom
   padding (54px, so 123px, until the amount moved into the price cell, 2026-10-14). */
.pricing-page .cmp__calc-card--yearly {
  top: 89px;
}
.pricing-page .cmp__calc-card:hover {
  border-color: var(--line-hover);
  box-shadow: var(--shadow);
}
.pricing-page .cmp__calc-card:focus-visible {
  outline: 3px solid var(--coral);
  outline-offset: 3px;
}
.pricing-page .cmp__calc-card__img {
  flex: none;
  display: block;
  width: 50px;
  height: auto;
}
.pricing-page .cmp__calc-card__text {
  flex: 1 1 auto;
  min-width: 0;
}
.pricing-page .cmp__calc-card__title {
  display: block;
  font-family: var(--font-head);
  /* Below the 18px floor, approved by the owner 2026-09-21 on the app's identical card (plans.css),
     and carried here by the identical-surfaces rule (D-060). Raised 13.5px -> 16.5px by the owner,
     2026-10-13 ("increase ... by 1px", then "1px more", twice). */
  font-size: 16.5px;
  font-weight: 700;
  line-height: 1.2;
  color: var(--ink);
}
.pricing-page .cmp__calc-card__sub {
  display: block;
  margin-top: 5px;
  font-family: var(--font-body);
  /* Below the 18px floor, approved by the owner 2026-09-21 on the app's identical card (plans.css),
     and carried here by the identical-surfaces rule (D-060). Raised 12px -> 14px by the owner,
     2026-10-13 ("increase ... by 1px", then "1px more"). */
  font-size: 14px;
  line-height: 1.25;
  color: var(--muted);
}
/* `nowrap`: the line fits its column by a fraction of a pixel in the app's measurement; a wrap
   would push the card's content past its own box. */
.pricing-page .cmp__calc-card__cta {
  display: block;
  margin-top: 3px;
  font-family: var(--font-body);
  /* Below the 18px floor, approved by the owner 2026-09-21 on the app's identical card (plans.css),
     and carried here by the identical-surfaces rule (D-060). Raised 12.5px -> 14.5px by the owner,
     2026-10-13 ("increase ... by 2px"), with the label changed to "See Savings". */
  font-size: 14.5px;
  font-weight: 600;
  line-height: 1.35;
  color: var(--coral);
  white-space: nowrap;
}
/* The platform group's subheading row: exactly 50px of #404040 with white 20px type, no light
   line at either end, and a transparent outline for forced colours (owner, 2026-09-19). Laid
   above the "Your Plan" outline (z-index 1), so the column's side lines pass under the band
   rather than across it (owner, 2026-10-31). */
.pricing-page table.plain tbody th.cmp__section {
  position: relative;
  z-index: 2;
  height: 50px;
  padding: 0 16px;
  border: 0;
  outline: 1px solid transparent;
  outline-offset: -1px;
  background: var(--section-dark);
  color: var(--card);
  font-family: var(--font-head);
  /* 20px: 4px up from the app's 16px (owner, 2026-10-31), so back over the 18px floor. */
  font-size: 20px;
  font-weight: 700;
  line-height: 1.4;
  text-align: center;
  white-space: nowrap;
}
.pricing-page table.plain .cmp__section-label {
  display: inline-block;
}
/* The tick, 15% larger than the cell text, line-height 1 so no row grows (owner, 2026-09-19). */
.pricing-page table.plain .cmp__tick {
  color: var(--green);
  font-weight: 700;
  font-size: 1.15em;
  line-height: 1;
}
/* Visually hidden, still read aloud: "Included", and the hidden row labels. */
.pricing-page .cmp__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---- the trust strip under the table (plans.css:1955-2028) ----
   The table's footnote: card marks first, badges second, in the DOM as on screen. 18px text,
   --muted (4.95:1 on the surface), icons 22px in the ticks' green. Everything wraps, no query. */
.pricing-page .trust {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px 32px;
  margin: 20px 0 0;
  font-size: 18px;
  line-height: 1.4;
  color: var(--muted);
}
.pricing-page .trust__badges,
.pricing-page .trust__marks {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
}
.pricing-page .trust__badges {
  justify-content: flex-end;
  margin-left: auto;
  gap: 8px 24px;
}
.pricing-page .trust__marks {
  justify-content: flex-start;
  gap: 8px;
}
.pricing-page .trust__badge {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
/* The app's Font Awesome glyph at 22px, here an inline SVG one em tall; the lock overflows its em
   box as the glyph does in the font. */
.pricing-page .trust__icon {
  flex: 0 0 auto;
  height: 22px;
  width: auto;
  overflow: visible;
  color: var(--green);
}
.pricing-page .trust__cards {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-start;
  gap: 8px 12px;
}
.pricing-page .trust__marks li {
  display: flex;
}
/* The card art is drawn for a white ground: a 40 x 30 tile (the 60 x 45 drawing at two thirds),
   ringed in --line so a mostly-white mark does not melt into the page. */
.pricing-page .trust__card {
  display: block;
  flex: 0 0 auto;
  width: 40px;
  height: 30px;
  overflow: hidden;
  border-radius: 4px;
  background: var(--card);
  box-shadow: 0 0 0 1px var(--line);
}
.pricing-page .trust__card svg {
  display: block;
  width: 100%;
  height: 100%;
}

/* THE BAND'S SLOT ON EVERY PAGE (owner, 2026-10-17), emitted by the page skeleton. ABOVE THE
   HEADER since 2026-10-18 (owner: "All alert sections ... needs to appear ABOVE the header, not
   below it"): first in <body>, so it scrolls away and the sticky header then takes the top. The
   notice inside keeps its own 1140px frame and auto margins; this only gives it the page gutter
   at phone width. */
.verify-band {
  padding: 0 16px;
}
.verify-band[hidden] {
  display: none;
}
/* ---- the verify notice (plans.css:4882-4935) ----
   After a sign-up that bought a belt: a notice, not an error, so #fffaf0 with a coral left rule.
   site.js renders it into [data-verify-band], the slot the page skeleton puts above the
   header on every page (2026-10-18; under it from 2026-10-17, and the pricing page's own slot
   before). */
.verify-notice {
  display: flex;
  align-items: flex-start;
  gap: 16px;
  max-width: 1140px;
  /* 14px above, 9px below (owner, 2026-10-18, once the band moved above the header: "Add 5px of
     space above it and remove 5px of space below it", then "Remove an additional 6px below the
     alert"). It was 9px and 20px from 2026-10-12, and 19px above with nothing below before that. */
  margin: 14px auto 9px;
  padding: 16px 20px;
  border: 1px solid var(--coral);
  border-left-width: 4px;
  border-radius: 8px;
  background: #fffaf0;
}
.verify-notice__text {
  margin: 0;
  flex: 1;
  /* 16px: approved by the owner 2026-10-09 (D-071: the pricing page moves verbatim from the
     app's /pricing, plans.css:4897). */
  font-size: 16px;
  line-height: 1.5;
  color: var(--ink);
}
/* The resend control inside the sentence. `font: inherit` keeps the UA from shrinking it. */
.verify-notice__resend {
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  cursor: pointer;
  color: var(--coral);
  text-decoration: underline;
  font-weight: 700;
}
.verify-notice__resend:disabled {
  cursor: not-allowed;
  opacity: 0.6;
}
/* The dismiss: a 24px glyph in a 44px touch target. */
.verify-notice__close {
  flex: none;
  width: 44px;
  height: 44px;
  margin: -10px -10px -10px 0;
  padding: 0;
  border: 0;
  background: none;
  font-size: 24px;
  line-height: 1;
  color: var(--ink);
  cursor: pointer;
}

/* ---- the pay and plan-change views inside the dialog (plans.css:2994-3048, 3528, 3867-3882) ----
   The app's `.lj-signup__X` as this site's `.login-dialog__X`, scoped to the dialog, which is on
   every page. */
/* The one number that matters in a pay or confirm view: what is owed today. */
.login-dialog .login-dialog__amount {
  margin: 0;
  font-size: 24px;
  font-weight: 700;
  color: var(--ink);
}
/* The plan's belt in the Upgrade / Downgrade title, immediately before the belt's name, scaled to the
   24px title (the knot's own size is 62x38). */
.login-dialog__title .login-dialog__belt {
  display: inline-flex;
  vertical-align: middle;
  margin: 0 8px 0 4px;
}
.login-dialog__title .belt-knot {
  width: 40px;
  height: 25px;
}
/* The title bar and the Sign Up column are near-black, so the Black Belt's knot would vanish
   into them: a light outline, on that belt and on those two dark surfaces only. */
.login-dialog__title .plan--black .belt-knot,
.login-dialog__promo-list .plan--black .belt-knot {
  filter: drop-shadow(0 0 1px rgba(255, 255, 255, 0.85)) drop-shadow(0 0 2px rgba(255, 255, 255, 0.45));
}
/* A body built of paragraphs: the flex gap is the only spacing. */
.login-dialog .login-dialog__prose > p {
  margin: 0;
}
/* A receipt reads left; the Check Your Email panel stays centred. */
.login-dialog .login-dialog__done.login-dialog__prose {
  text-align: left;
}
/* The opening sentence says what Confirm does: bold, 22px, #1a1a1a (owner, 2026-09-22). */
.login-dialog .login-dialog__prose > .login-dialog__lede {
  font-weight: 700;
  font-size: 22px;
  color: #1a1a1a;
}
/* The rest of a prose body is 18px, lifted off the shared muted 16px (owner, 2026-09-22). */
.login-dialog .login-dialog__prose > .login-dialog__muted {
  font-size: 18px;
}
/* The Stripe Payment Element's wrapper while it MOUNTS: clipped, never display:none, because
   Stripe measures its iframe on mount and a hidden parent leaves it zero-sized for good. */
.login-dialog .login-dialog__pay-element.is-mounting {
  max-height: 0;
  overflow: hidden;
}
/* The destructive primary: the submit's geometry in the app's destructive treatment. Pressed is a
   step off the HOVER ground, so a click never flashes back to the resting maroon. */
.login-dialog .login-dialog__submit--destructive {
  border: 1px solid var(--destructive);
  background: var(--destructive);
  color: var(--destructive-ink);
}
.login-dialog .login-dialog__submit--destructive:hover:not(:disabled) {
  background: var(--destructive-hover);
  color: var(--destructive-hover-ink);
}
.login-dialog .login-dialog__submit--destructive:active:not(:disabled) {
  background: rgba(151, 27, 28, 0.85);
  color: var(--destructive-hover-ink);
}
/* CONFIRM CHANGE IS AMBER, NOT RED (owner, 2026-10-19: "Since this isn't a critical warning,
   let's make the 'Confirm Change' button default color #BB5E0C, and its hover color #B65601").
   A downgrade waits for the end of the billing cycle and can be cancelled until then, so it is a
   caution, not a danger. End My Plan and Remove card keep the destructive red above. site.js
   picks the class per mode. Pressed stays on the hover ground, so a click never flashes back.
   White on #BB5E0C is 4.45:1, a hair under AA's 4.5 for this 18px/600 label (README gap 0, the
   owner's colour); the hover's #B65601 is 4.86:1. */
.login-dialog .login-dialog__submit--caution {
  border: 1px solid var(--caution);
  background: var(--caution);
  color: #ffffff;
}
.login-dialog .login-dialog__submit--caution:hover:not(:disabled),
.login-dialog .login-dialog__submit--caution:active:not(:disabled) {
  border-color: var(--caution-hover);
  background: var(--caution-hover);
}

/* ---- Payment Methods: the card list and the two-up fields ----
   Modelled on the app's payment-methods-modal.tsx and edit-card-form.tsx (the app's dead
   PaymentMethodsModal is the behaviour source, plan section 2.5). A row is a bordered card; the
   whole row is a full-bleed edit button under the content, with the actions raised above it. */
/* "No payment methods saved yet." at 18px (owner, 2026-10-10), in the /pricing dialog and on
   /payment-methods. It wears .login-dialog__muted for its colour, but that class is a deliberate
   16px for the Check Your Email spam line, so the size is raised here for this one line only. */
.login-dialog__muted.methods-empty {
  font-size: 18px;
}
.methods-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.methods-list__row {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px;
  border: 1px solid #e5e5e5;
  border-radius: 8px;
  transition: background 0.15s;
}
.methods-list__row:hover {
  background: #f5f5f5;
}
/* THE PAYMENT METHODS DIALOG'S LIST RUNS EDGE TO EDGE (owner, 2026-10-11: no margin between the
   rows, then no padding around them). The list cancels its container's 24px on the top and both
   sides, so the rows run from the dark title bar to the dialog's edges; the status line and the
   buttons below keep the 24px. The rows meet the dialog's edges and each other, so they lose
   their corners and keep only a bottom border as the divider; the 24px moves inside each row so
   the card text still lines up with the buttons. The /payment-methods page keeps its separate
   bordered rows 12px apart. */
[data-methods-dialog] [data-methods-view="list"] > .methods-list {
  gap: 0;
  margin: -24px -24px 0;
}
[data-methods-dialog] .methods-list__row {
  border-width: 0 0 1px;
  border-radius: 0;
  padding: 12px 24px;
}
[data-methods-dialog] .methods-list__edit {
  border-radius: 0;
}
.methods-list__edit {
  position: absolute;
  inset: 0;
  padding: 0;
  border: 0;
  border-radius: 8px;
  background: none;
  cursor: pointer;
}
.methods-list__edit:focus-visible {
  outline: 2px solid #dfc8c3;
  outline-offset: 0;
}
/* THE UPGRADE'S CARD STEP (owner rule, 2026-10-10: nothing is charged until a card is chosen and
   then confirmed). One radio row per saved card, then "Add a new card"; the rows wear the
   Payment Methods list's own frame and brand tile. */
.pc-methods {
  display: flex;
  flex-direction: column;
  gap: 12px;
  margin-bottom: 16px;
}
/* EVERY CARD FORM IS ONE COLUMN (owner, 2026-10-12: "change the 2 column modal layouts to 1
   column layouts by moving 'Card nickname' above 'Name on card'"), at the dialog's own width:
   "Name on card" and Stripe's card fields (.pay-cols__card; the nickname left every add form on
   2026-10-17 and is set on Edit once the card exists), then the review line,
   the terms and the challenge (.pay-cols__side) where a form has them. Used by the Pay view, the
   upgrade's new-card form and the Payment Methods add view. Each part keeps the dialog form's own
   16px rhythm, which its children do not reach through these wrappers. */
.pay-cols:not([hidden]) {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 16px;
}
/* The terms line and the challenge, under the card fields. */
.pay-cols__foot {
  margin-top: auto;
  display: flex;
  flex-direction: column;
  gap: 16px;
}
/* NO DEAD SPACE UNDER THE TERMS LINE (owner, 2026-10-12: "Remove extra space ... and the bottom
   buttons"). It was 56px: the line's own 16px bottom margin, the column's 16px gap, the bot check's
   slot (empty unless Cloudflare asks for a challenge), then the form's 16px and the row's 8px. The
   margin goes, and the slot hands its gap back whatever it holds (Turnstile fills it with a
   zero-height wrapper, so :empty never matched once the check loaded); it stays displayed,
   because Turnstile cannot render into a display:none box. Then 20px less again (owner,
   2026-10-12, "Remove 20px"): the button row's top padding is 4px here, not 8, leaving the line
   21px above the buttons. A challenge Cloudflare does show sits flush under the line. */
.pay-cols__foot > p {
  margin: 0;
}
/* THE TERMS LINE IS BODY TEXT, NOT MUTED (owner, 2026-10-17: "2 shades darker", then
   #3d3835 picked over heading ink). Pay's "By subscribing..." and the upgrade's new-card
   "By providing your card information..." alike. */
.pay-cols__foot > .login-dialog__muted {
  color: var(--body-text);
}
.pay-cols__foot > .turnstile-slot {
  margin-top: -16px;
}
.login-dialog .pay-row .login-dialog__actions {
  padding-top: 4px;
}
/* LOADING (owner, 2026-10-10: "We need loading spinner while we are waiting for forms to load").
   The status line becomes a centred spinner with its words beneath; the button row stays hidden
   until the form arrives (site.js). The spinner is a 128px ninja (owner, 2026-10-23), which
   replaced a dancing robot the same day, which replaced the Ask Sales send button's ring
   (sales-spin). A background image, not an <img>, so the 280KB GIF is fetched only when a
   billing dialog first shows it, never with the page, and so the reduce query below can swap it
   for a still: an animated image in markup plays on sight, out of every gate's reach (see
   "Motion: what is gated"). The still is frame 50, not frame 0: the loop opens on a spark and a
   puff of smoke, and the ninja is not on screen until 0.64s in. */
.login-dialog .pay-loading:not([hidden]) {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  padding: 32px 0;
  text-align: center;
  color: var(--muted);
  font-weight: 400;
}
.login-dialog .pay-loading::before {
  content: "";
  width: 128px;
  height: 128px;
  background: url("/images/loading-ninja.gif") center / 128px 128px no-repeat;
}
@media (prefers-reduced-motion: reduce) {
  .login-dialog .pay-loading::before {
    background-image: url("/images/loading-ninja-still.png");
  }
}
.login-dialog .pay-row[hidden] {
  display: none;
}

/* THE BUTTON ROW: the buttons LEFT (owner, 2026-10-11: "In all billing modals, the buttons should
   be left-justified, with the cancel button always on the right-side of the primary action
   button"), and always the modal's bottom line (owner, 2026-10-12). The trust badges that shared
   this row were removed from every billing modal (owner, 2026-10-12). The primary action comes
   first and Cancel, Back or Close second; that order is in the markup (scripts/site-billing.mjs). */
.pay-row {
  display: flex;
  align-items: center;
}
.login-dialog .pay-row .login-dialog__actions {
  justify-content: flex-start;
  margin-left: 0;
}
/* THE PAYMENT FLOW'S BUTTONS ARE RIGHT-JUSTIFIED (owner, 2026-10-17: "The button on this workflow
   of modals need to be right-justified"), as Sign Up step two's are: "<- Back" or Cancel first,
   the primary action last (scripts/site-billing.mjs). The card form, its review and the receipt,
   and since 2026-10-18 every step of the plan change, which follows the checkout's conventions
   (owner: "the primary action button should read 'Next: Payment' and be justified-right, the
   cancel button should be to the left of it"). Payment Methods and the two "Are you sure?"
   confirms keep the left-justified rule above. */
.login-dialog [data-pay-row] .login-dialog__actions,
.login-dialog [data-pay-receipt] .login-dialog__actions,
.login-dialog[data-plan-change-dialog] .login-dialog__actions {
  justify-content: flex-end;
  margin-left: auto;
}
/* ...EXCEPT CONFIRM CHANGE, which is left-justified with Cancel on its right (owner, 2026-10-19:
   "When displaying 'Confirm Change' and 'Cancel' buttons at the bottom of a modal, these two
   buttons should be left-justified and 'Cancel' should be located to the right of 'Confirm
   Change'"). site.js sets this class, and puts the button ahead of Cancel in the markup, only
   while the plan change's confirm step reads Confirm Change; Next: Payment and End My Plan keep
   the rule above. */
.login-dialog[data-plan-change-dialog] .login-dialog__actions.login-dialog__actions--start {
  justify-content: flex-start;
  margin-left: 0;
}
/* THE CARD NAME FIELD FLOATS ITS LABEL, as Stripe's card fields beside it do (owner, 2026-10-10:
   "Card Name field in the right column should use floating labels, like the actual credit card
   fields on the left"). Every value here is STRIPE_APPEARANCE's (site.js): 8px corners, the same
   font stack, 16px text, a #525252 label at 16px that floats to 12px, a coral border on focus.
   The label sits inside the box until the field holds focus or text (the input's placeholder is
   a single space, so :placeholder-shown means empty). */
.login-dialog .float-field {
  position: relative;
  display: block;
}
.login-dialog .float-field[hidden] {
  display: none;
}
.login-dialog .float-field input {
  display: block;
  width: 100%;
  /* Stripe's field height, measured 2026-10-10: its iframe is 216px for three fields, two 12px
     gaps and 4px of padding top and bottom, so 61px each. */
  height: 61px;
  padding: 24px 12px 7px;
  border: 1px solid #d1cbc8;
  border-radius: 8px;
  background: var(--field-bg);
  color: var(--ink);
  font-weight: 400;
  font-family: "SF Pro", Roboto, Inter, system-ui, -apple-system, sans-serif;
  /* 16px: approved by the owner 2026-10-10 ("use floating labels, like the actual credit card
     fields on the left"): Stripe's card fields beside it are 16px, and the two must read alike. */
  font-size: 16px;
}
.login-dialog .float-field input:focus-visible {
  outline: none;
  border-color: #be654f;
}
.login-dialog .float-field label {
  position: absolute;
  left: 13px;
  top: 50%;
  transform: translateY(-50%);
  font-family: "SF Pro", Roboto, Inter, system-ui, -apple-system, sans-serif;
  /* 16px: approved by the owner 2026-10-10 (floating labels like Stripe's card fields, whose
     resting label is 16px). */
  font-size: 16px;
  font-weight: 400;
  color: #525252;
  pointer-events: none;
  transition:
    top 0.15s ease,
    font-size 0.15s ease,
    transform 0.15s ease;
}
.login-dialog .float-field input:focus + label,
.login-dialog .float-field input:not(:placeholder-shown) + label {
  top: 9px;
  transform: none;
  /* 12px: approved by the owner 2026-10-10 (floating labels like Stripe's card fields, whose
     floated label is 12px). */
  font-size: 12px;
}
@media (prefers-reduced-motion: reduce) {
  .login-dialog .float-field label {
    transition: none;
  }
}
/* On the purchase review the card form is clipped (never display:none, Stripe must keep its
   measured height), so its row takes no gap. */
.pay-cols.is-review:not([hidden]) {
  row-gap: 0;
}
/* THE ORDER SUMMARY ON BOTH PURCHASE REVIEWS (owner, 2026-10-17: "Redesign the content on this
   modal, so it looks more like an order form. It needs to be a more visual and easier to read
   design", then the plan-change review "the same order-form layout"). A receipt card on the
   modal's surface: the plan with its belt knot, the price and term, the card with its brand mark,
   and the amount due today as the bold last line. It took the place of the one-sentence review
   ("{card} will be charged {amount} today.") on the Payment view and on the plan change. The
   frame is the card form's own (1px #d1cbc8, 8px corners, on --field-bg), so the summary reads
   as part of the form it follows. Every size here is 18px or above, so it takes no type-floor
   exception. */
/* `flex: none`: the card is a flex item of the dialog's scrolling column, and its overflow:hidden
   lets it shrink below its rows when that column runs out of height, which clipped the due line
   at phone height (2026-10-18). It keeps its height and the column scrolls instead. */
.pay-order {
  display: flex;
  flex: none;
  flex-direction: column;
  margin: 0;
  border: 1px solid #d1cbc8;
  border-radius: 8px;
  background: var(--field-bg);
  overflow: hidden;
}
.pay-order__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 8px 16px;
  padding: 14px 16px;
}
.pay-order__row + .pay-order__row {
  border-top: 1px solid var(--line);
}
.pay-order dt {
  flex: none;
  margin: 0;
  color: var(--muted);
  font-size: 18px;
  font-weight: 400;
  line-height: 1.3;
}
.pay-order dd {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 10px;
  margin: 0 0 0 auto;
  min-width: 0;
  text-align: right;
  color: var(--ink);
  font-size: 18px;
  font-weight: 600;
  line-height: 1.3;
}
/* The plan: its belt knot before its name, the term beneath in the muted weight. */
.pay-order__plan {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
}
.pay-order__name {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
.pay-order .login-dialog__belt {
  display: inline-flex;
}
.pay-order .belt-knot {
  width: 40px;
  height: 25px;
}
.pay-order__term {
  color: var(--muted);
  font-weight: 400;
}
/* The card's brand mark: the Payment Methods tile at two thirds. Its slot is a flex box so the
   cloned tile is laid out as a block and keeps its size; left inline, the tile's width and
   height would not apply and its SVG would fill the row. */
.pay-order__mark {
  display: inline-flex;
  flex: none;
}
/* THE CARD ROW STAYS ONE ROW (owner, 2026-10-17: "Mastercard ending 4444" dropped below "Payment
   method", where "Visa ending 4242" fits beside it). The label keeps its width and the value takes
   what is left, wrapping on its own side, balanced so "ending 4444" stays together, as the Plan row
   above it already shows two right-aligned lines. A longer brand (American Express) wraps the same
   way rather than pushing the row apart. */
.pay-order__row--card {
  flex-wrap: nowrap;
}
.pay-order__row--card dd {
  flex: 1 1 auto;
}
.pay-order__row--card dd > span:last-child {
  min-width: 0;
  text-wrap: balance;
}
.pay-order__mark:empty {
  display: none;
}
.pay-order .methods-list__mark {
  display: block;
  width: 40px;
  height: 30px;
}
/* "TO BE CHARGED TODAY", the line Confirm acts on. The row is #2e2e2e (owner, 2026-10-17, "from
   orange to #2E2E2E"; earlier that day it took the Confirm button's --coral, and before that it
   was a --band tint with ink text), with the label and the figure in #f1f1f1. The label is bold
   and the figure is the pay view's 24px amount. #f1f1f1 on #2e2e2e measures about 12:1, clear of
   AA at any size; the coral pairing measured about 3.7:1 (audit J8-10), so that record is
   closed. The Confirm button beside it keeps --coral. */
.pay-order__row--total {
  background: #2e2e2e;
}
.pay-order__row--total dt {
  color: #f1f1f1;
  font-weight: 700;
}
.pay-order__row--total dd {
  color: #f1f1f1;
  font-size: 24px;
  font-weight: 700;
}
/* The plan change's due line while its proration loads: a status ("Working out what you owe
   today…"), so body weight, and the same #f1f1f1 softened on the #2e2e2e. */
.pay-order__row--total dd.is-pending {
  font-size: 18px;
  font-weight: 400;
  color: rgba(241, 241, 241, 0.85);
}
/* THE PLAN CHANGE'S OPENING STEP (owner, 2026-10-18: "The body content of this modal looks bad.
   Reformat to make it look more professional", by "the same conventions we use for initial
   checkout"). It was a bold 22px sentence over a bold 24px amount line, so a quote that had not
   come back ("A prorated amount for the rest of this billing period") read as a second headline.
   Now an order form: the sentence as body text, then the review's order card without its card
   row (scripts/site-billing.mjs, changeSummary), then the note beneath it, as on the review. */
.login-dialog .pc-confirm > .login-dialog__lede {
  font-size: 18px;
  font-weight: 400;
  line-height: 1.5;
  color: var(--body-text);
}
/* A due line that is still a status wraps in balanced lines on its own side of the row. */
.pay-order__row--total dd.is-pending {
  text-wrap: balance;
}
/* THE PROMO CODE ON BOTH REVIEWS (owner, 2026-10-18: "Link on review step"). Under the order card,
   "Have a promo code?" opens the field and Apply; an applied code is a row in the card with Remove
   (scripts/site-billing.mjs, promoBox and promoRow). The field is the Log In dialog's own
   (.login-dialog__field), so its label, input and hint wear that dialog's approved rules and no
   rule here sets a size under the floor. The link and Remove take the dialog's dark coral, as the
   failed-login panel's link does, so they read as links at AA on the light panel. */
.promo-box {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 10px;
}
.promo-box__link,
.pay-order__remove {
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: 18px;
  font-weight: 600;
  color: #8f3520;
  text-decoration: underline;
  cursor: pointer;
}
.promo-box__link:hover,
.pay-order__remove:hover {
  color: #7e2f1c;
}
.pay-order__remove:disabled {
  cursor: not-allowed;
  opacity: 0.6;
}
.promo-box__field {
  align-self: stretch;
}
/* The input and Apply side by side, Apply as tall as the field. */
.promo-box__entry {
  display: flex;
  gap: 10px;
}
.promo-box__entry input {
  flex: 1 1 auto;
  min-width: 0;
  text-transform: uppercase;
}
.promo-box__apply {
  flex: none;
  padding: 10px 20px;
}
.promo-box__apply:disabled {
  cursor: not-allowed;
  opacity: 0.6;
}
/* The hint under the field only ever holds a refusal, so it is always the error red. The dialog's
   own red rule cannot reach it: that rule is `input[aria-invalid] ~ .login-dialog__hint`, and here
   the input sits inside the entry row, not beside the hint. No size is set (the hint's approved
   16px stands). */
.promo-box .login-dialog__hint {
  color: #b3261e;
}
/* The applied code's row: "Promo SAVE20" against what it gives, then Remove. On an upgrade the
   label is the whole sentence (the bills it discounts; site.js adds is-sentence), so it takes
   the row's width and wraps. */
.pay-order__row--promo.is-sentence {
  flex-wrap: nowrap;
  align-items: baseline;
}
.pay-order__row--promo.is-sentence dt {
  flex: 1 1 0;
  min-width: 0;
  text-wrap: pretty;
}
.pay-order__row--promo.is-sentence dd {
  flex: none;
}
.pay-order__row--promo dd {
  flex-wrap: wrap;
  font-weight: 400;
}
.pay-order__row--promo [data-pay-order-promo-terms] {
  font-weight: 600;
}
/* A switch that ends the running promo code says so before payment (D-079): the muted line's
   size (its approved 16px), in the ink and weight that make it read as a warning. */
.login-dialog .pc-promo-ends {
  color: var(--ink);
  font-weight: 600;
}
/* THE RECEIPT (owner, 2026-10-17: "more visually clear and useful"; the title "Purchase
   Complete", the sentence and "Close" are the owner's words). A green check beside the one
   sentence, then the plan and its renewal date in the order card, then Close across the modal.
   The green "You are on {belt}." banner (.login-dialog__sent) left this view; Reset Password
   keeps that class. */
.pay-receipt__note {
  display: flex;
  align-items: flex-start;
  gap: 14px;
}
.pay-receipt__check {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: var(--green);
  color: #ffffff;
}
.pay-receipt__check svg {
  width: 22px;
  height: 22px;
}
/* A failed plan-change payment (site.js sets .pay-receipt--failed on the step): the red mark of
   the error banner's #b3261e in the check's place, and no order card. */
.pay-receipt__alert {
  display: none;
  flex: none;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: #b3261e;
  color: #ffffff;
}
.pay-receipt__alert svg {
  width: 22px;
  height: 22px;
}
.pay-receipt--failed .pay-receipt__check {
  display: none;
}
.pay-receipt--failed .pay-receipt__alert {
  display: inline-flex;
}
.pay-receipt__text {
  margin: 0;
  color: var(--ink);
  font-size: 18px;
  line-height: 1.4;
}
/* Close spans the modal (owner, 2026-10-17: "make it span 100% of the modal width"). The pay
   flow's right-justify rule above gives this row `margin-left: auto`, which in the form's flex
   column shrinks the row to its content and would leave a 100% button its own width; the row
   takes the full width back here, and the button fills it. */
.login-dialog .pay-receipt .login-dialog__actions {
  margin-left: 0;
  width: 100%;
}
.login-dialog__submit--full {
  width: 100%;
}
.pay-cols__card,
.pay-cols__side {
  display: flex;
  flex-direction: column;
  gap: 16px;
  min-width: 0;
}
.pc-methods__row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px;
  border: 1px solid #e5e5e5;
  border-radius: 8px;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}
.pc-methods__row:hover {
  background: #f5f5f5;
}
.pc-methods__row:has(.pc-methods__radio:checked) {
  border-color: #262626;
}
.pc-methods__radio {
  flex: none;
  width: 18px;
  height: 18px;
  margin: 0;
  accent-color: #262626;
}
.pc-methods__row .methods-list__mark {
  width: 48px;
  height: 36px;
}
.pc-methods__text {
  flex: 1;
  min-width: 0;
  color: #262626;
  font-size: 18px;
  line-height: 26px;
}
.pc-methods__label,
.pc-methods__name {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.pc-methods__label {
  font-weight: 600;
}
.methods-list__info {
  position: relative;
  pointer-events: none;
  display: flex;
  align-items: center;
  gap: 12px;
  min-width: 0;
}
/* The brand mark at the row's 60 x 45 tile, on white. */
.methods-list__mark {
  flex: none;
  width: 60px;
  height: 45px;
  border-radius: 6px;
  background: var(--card);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1);
  overflow: hidden;
}
.methods-list__mark svg {
  display: block;
  width: 100%;
  height: 100%;
}
.methods-list__text {
  min-width: 0;
}
.methods-list__label,
.methods-list__brand,
.methods-list__expires {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  /* 16px: approved by the owner 2026-10-09 (D-071: the Payment Methods screens move verbatim
     from the app, payment-methods-modal.tsx:186-197, text-body-md). */
  font-size: 16px;
  line-height: 24px;
}
.methods-list__label {
  font-weight: 600;
  color: #262626;
}
.methods-list__brand {
  font-weight: 500;
  color: #262626;
  text-transform: capitalize;
}
.methods-list__expires {
  color: #737373;
}
.methods-list__actions {
  position: relative;
  z-index: 1;
  display: flex;
  flex: none;
  align-items: center;
  gap: 4px;
}
/* The badge or the button, under the card's three lines (owner, 2026-10-11; it was a fixed 150px
   slot beside the trash can). .methods-list__info passes clicks through to the row's edit button
   underneath, so the slot takes them back and stacks above that button. */
.methods-list__slot {
  position: relative;
  z-index: 1;
  display: flex;
  margin-top: 6px;
  pointer-events: auto;
}
.methods-list__badge {
  display: inline-flex;
  align-items: center;
  padding: 2px 10px;
  border-radius: 999px;
  background: #e5e5e5;
  color: #404040;
  /* 14px: approved by the owner 2026-10-09 (D-071: the Payment Methods screens move verbatim
     from the app, badge.tsx:29, text-label-lg). */
  font-size: 14px;
  line-height: 20px;
  font-weight: 500;
}
/* An extra-small button (owner, 2026-10-11: "should look like an extra small button"): the
   outline button's grey border and label, 28px tall, the primary action's coral fill on hover
   like the Payment Methods button on /pricing. Its 14px label is an owner-approved exception to
   the 18px text floor (2026-10-11), for this button only, matching the badge it alternates with. */
.methods-list__primary {
  display: inline-flex;
  align-items: center;
  height: 28px;
  padding: 0 12px;
  border: 2px solid var(--btn-outline);
  border-radius: 8px;
  background: transparent;
  color: var(--btn-outline);
  font-family: var(--font-head);
  font-weight: 600;
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
  transition:
    background 0.15s,
    color 0.15s,
    border-color 0.15s;
}
.methods-list__primary:hover:not(:disabled) {
  background: #c16850;
  border-color: #c16850;
  color: #fff;
}
.methods-list__primary:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}
/* Icon-only remove: neutral at rest, red only on hover, so it does not shout from every row. */
/* The app's list-row size (owner, 2026-10-11): 36px square, a 17px glyph (the app sets the font
   icon at text-[17px]; the ink box is 17.5px tall because the lid rises above the em). It was the
   app's smaller dialog version, 32px with a 15px stroke icon. */
.methods-list__remove {
  display: flex;
  flex: none;
  width: 36px;
  height: 36px;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: #24201e;
  cursor: pointer;
  transition:
    background 0.15s,
    color 0.15s;
}
.methods-list__remove svg {
  width: 15px;
  height: 17.5px;
}
.methods-list__remove:hover {
  background: #fef2f2;
  color: #dc2626;
}
/* A saved card's number and expiry as plain text under the Nickname field (owner, 2026-10-11),
   in the dialog and on /payment-methods: the label muted above, the value in ink. They replaced
   three disabled inputs and the "Card details can't be changed" line. No font size is set, so
   both lines take the form's own. */
.methods-facts {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.methods-facts dt {
  color: #737373;
}
.methods-facts dd {
  margin: 0;
  font-weight: 500;
  color: #262626;
}

@media (max-width: 1000px) {
  /* The title row stacks: the title first, the toggle centred beneath it (plans.css:4433). */
  .pricing-page .cmp__titlebar {
    grid-template-columns: minmax(0, 1fr);
    row-gap: 16px;
  }
  .pricing-page .cmp__titlebar .billing {
    justify-self: center;
  }
  .pricing-page .cmp__titlebar [data-pricing-title-actions] {
    justify-self: center;
  }
}

@media (max-width: 850px) {
  .pricing-page section.pricing-cmp {
    padding-bottom: 52px;
  }
  /* The payment points stack from the left edge on a phone (owner, 2026-09-30). */
  .pricing-page .trust__badges {
    flex-direction: column;
    align-items: flex-start;
    justify-content: flex-start;
    margin-left: 0;
  }
  /* "Compare Plans" centred on a phone (owner, 2026-09-30). */
  .pricing-page .cmp__titlebar .cmp__title {
    grid-column: 1 / -1;
    justify-self: center;
    text-align: center;
  }
  /* Tighter toggle buttons so both share one line at 320px; the sizes stay. */
  .pricing-page .billing__option {
    padding: 8px 12px;
    gap: 6px;
  }
  .pricing-page .billing__option[aria-pressed="true"] {
    padding: 6px 10px;
  }
  .pricing-page .billing__badge {
    padding: 1px 8px;
  }
  /* A 170px label column that stays put while the tier columns scroll under it. */
  .pricing-page table.plain {
    min-width: 970px; /* 170 label + 5 x 160 */
  }
  .pricing-page table.plain .cmp__label-col {
    width: 170px;
  }
  .pricing-page table.plain tbody th,
  .pricing-page table.plain .cmp__corner {
    position: sticky;
    left: 0;
    z-index: 1;
    box-shadow: 1px 0 0 var(--line);
  }
  /* The blank cells sit above the scrolling columns (split by row so the header's corner stays a
   step above the label). The app's calculator card lies over them; here they are see-through. */
  .pricing-page table.plain thead tr > .cmp__blank,
  .pricing-page table.plain tbody tr > .cmp__blank {
    background: transparent;
  }
  .pricing-page table.plain tbody tr > .cmp__blank {
    z-index: 3;
  }
  .pricing-page table.plain thead tr > .cmp__blank {
    z-index: 4;
  }
  /* What the desktop's blank button-row label would otherwise carry onto a phone, undone. */
  .pricing-page
    table.plain
    tbody:not(.cmp__group)
    tr.cmp__cta-row
    > td.cmp__corner {
    border-left: 1px solid var(--line);
  }
  .pricing-page table.plain tbody .cmp__cta-row > .cmp__corner + td {
    border-left: 0;
  }
  .pricing-page table.plain tbody .cmp__cta-row + tr > th {
    border-top-left-radius: 0;
  }
  .pricing-page table.plain .cmp__blank + th,
  .pricing-page table.plain .cmp__blank + td {
    border-left: 0;
  }
  /* The platform subheading: the cell does not stick (relative, to stay above the outline), the
     WORDS stick 16px in from the left. */
  .pricing-page table.plain tbody th.cmp__section {
    position: relative;
    box-shadow: none;
    text-align: left;
  }
  .pricing-page table.plain .cmp__section-label {
    position: sticky;
    left: 16px;
  }
  .pricing-page table.plain thead .cmp__corner {
    position: sticky;
    z-index: 4;
  }
  /* The phone's card (the app's plans.css, owner 2026-09-30): a flat panel the width of the label
     column running down through the header, price and button rows; the picture stacks over
     "Calculate savings" and the title and subtitle stay in the DOM, visually hidden. */
  .pricing-page .cmp__calc-card,
  .pricing-page .cmp__calc-card--yearly {
    top: 0;
    width: 170px;
    height: 199px;
    flex-direction: column;
    justify-content: center;
    gap: 10px;
    padding: 0 8px;
    text-align: center;
    background: linear-gradient(
      to right,
      rgb(237 235 232 / 1),
      rgb(237 235 232 / 0)
    );
    border: 0;
    border-radius: 0;
  }
  /* Yearly is 20px taller (the "billed yearly" row less the price cells' bottom padding), so the
     panel grows with it and still reaches the first data row's line (2026-10-14). */
  .pricing-page .cmp__calc-card--yearly {
    height: 219px;
  }
  .pricing-page .cmp__calc-card:hover {
    box-shadow: none;
  }
  .pricing-page .cmp__calc-card__img {
    width: 98px;
  }
  .pricing-page .cmp__calc-card__text {
    flex: none;
  }
  .pricing-page .cmp__calc-card__title,
  .pricing-page .cmp__calc-card__sub {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }
  .pricing-page .cmp__calc-card__cta {
    margin-top: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .pricing-page .billing__option {
    transition: none;
  }
}

/* ---------------------------------------------------------------------------------------------
   lotjitsu.com/invoices (D-071, 2026-10-09; the app's MOVE_PRICING_TO_MARKETING_PLAN.md 0.2).
   The account's receipts in the site's existing `table.plain`, each row linking out to Stripe's
   hosted invoice and PDF. No font sizes here: every size is the site's own, so nothing below
   needs a type-floor approval. Scoped to `.invoices` so it cannot restyle another page.
   --------------------------------------------------------------------------------------------- */
/* 26px on top, not 40: with the shared `page-title page-title--lg` h1 (2026-10-10), this puts the
   title exactly where /contact's sits (measured, 100px from the top of the window at 1280px). */
.invoices {
  padding: 26px 0 64px;
}
.invoices__title {
  margin: 0 0 24px;
}
.invoices [hidden] {
  display: none;
}
.invoices__shell {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 16px;
}
.invoices__shell p,
.invoices__empty,
.invoices__error {
  margin: 0;
}
/* "No payments yet." at 18px (owner, 2026-10-10), matching "No payment methods saved yet." */
.invoices__empty {
  font-size: 18px;
}
.invoices__error {
  color: #b3261e;
}
/* The Invoice link, right-justified in the row (owner, 2026-10-15). Still a link: it opens
   Stripe's invoice PDF, so middle-click and open-in-new-tab keep working. */
/* `td.` in the selector: `table.plain td` (text-align: left) would outrank a bare class. */
.invoices__table td.invoices__links {
  white-space: nowrap;
  text-align: right;
}
/* Styled like the app History page's "Export Again" button, with its download icon (owner,
   2026-10-25: 'The "Download" link on the /payment-history page needs to be styled like the
   Export Again button on the history page, but it should read "Invoice" with the same icon').
   Every value is transcribed from that button as rendered at 1280px: the app's font stack,
   14px weight 500 on a 20px line with 0.007em tracking, 8px 12px of padding, 6px corners, an
   8px gap, the primary #be624b darkening to #a8533e on hover, and a 14px icon in a 17.5px box.
   Approved by the owner 2026-10-25 under the 18px floor: the button is the app's, at the app's
   size. */
.invoices__links a {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 12px;
  border-radius: 6px;
  background: #be624b;
  color: #fff;
  font-family: "SF Pro", Roboto, Inter, system-ui, -apple-system, sans-serif;
  font-size: 14px;
  font-weight: 500;
  line-height: 20px;
  letter-spacing: 0.007em;
  text-decoration: none;
  transition: background-color 0.15s ease;
}
.invoices__links a:hover {
  background: #a8533e;
  color: #fff;
}
.invoices__links a svg {
  flex: none;
  width: 17.5px;
  height: 14px;
}
.invoices__links a:focus-visible {
  outline: 2px solid var(--coral);
  outline-offset: 0;
}
.invoices__more {
  margin-top: 20px;
}
/* The column header row is #2A2A2C with white text (owner, 2026-10-25), in place of the shared
   `table.plain` header band. The selector outranks `table.plain thead th` (0,1,3) at (0,2,3), so
   it holds whatever order the rules sit in, and it is scoped to this table so no other
   `table.plain` changes. The last cell is an empty <td> (a header cell with no name fails axe), so
   it is named here too to keep the row one color. */
.invoices table.invoices__table thead th,
.invoices table.invoices__table thead td {
  background: #2a2a2c;
  color: #ffffff;
}


/* =========================================================================
   SITE-WIDE DARK THEME (owner, 2026-11-04: "When a user chooses the dark theme it should be
   applied consistently throughout their entire web user experience from marketing site through
   the web app ... let's see what it looks like as a baseline for polishing later").

   BASELINE, NOT FINISHED. /js/theme.js reads the app's `lj_theme_<env>` cookie before first
   paint and, for `dark`, puts `theme-dark` AND `inverted` on <html>. `inverted` is the per-section
   dark palette above, so redefining the tokens at the root carries almost every component;
   the rules below only catch the light literals that cascade cannot reach. The section-level
   light islands (`.inverted.band--dark` and friends) are compound selectors on a section and so
   do NOT fire from <html>: dark bands stay dark on the dark page. Polish belongs here.
   ========================================================================= */
html.theme-dark { color-scheme: dark; }
html.theme-dark .site-header { background: rgba(35, 35, 35, 0.94); }
html.theme-dark .site-header.is-scrolled {
  background: rgba(27, 27, 27, 0.92);
  border-bottom-color: var(--line);
}
html.theme-dark .hero--snug h1,
html.theme-dark .flow__head h3,
html.theme-dark .card__head h3,
html.theme-dark .account__name {
  color: var(--ink);
}
/* The account dropdown's links and their currentColor icons in dark mode (owner, 2026-11-04: "The
   links in the account dropdown need to be #f1f1f1"); the base rule's #232323 literal vanished on
   the dark menu. The :not keeps Sign Out's own danger red, which this specificity would otherwise
   beat. */
html.theme-dark .account__item:not(.account__item--danger),
html.theme-dark .account__item:not(.account__item--danger):hover,
html.theme-dark .account__item:not(.account__item--danger):focus {
  color: #f1f1f1;
}

/* THE LOGO'S CORAL IN DARK MODE (owner, 2026-11-04): the avatar placeholder, the "Get your time
   back." band and every primary action button "should match the color of 'LotJitsu' in the
   logo", and primary buttons carry #ffffff labels. #CB5F48 is sampled from the wordmark in
   /images/LotJitsu-logo-light-mode.png (the brand's Coral Orange). Literals, not --coral, written
   while the inverted palette still brightened --coral to #d17b63 for text; since the owner's
   one-orange rule later the same day (see :root) --coral is #CB5F48 in both themes, and these
   simply agree with it. They beat `.inverted .btn--primary`'s charcoal label from the fill rule.
   Hover is one step deeper, as in light mode. */
html.theme-dark .site-avatar--initials,
html.theme-dark .band--coral {
  background: #cb5f48;
  color: #ffffff;
}
html.theme-dark .btn--primary,
html.theme-dark .site-nav .site-nav__catalogs {
  background: #cb5f48;
  color: #ffffff;
}
html.theme-dark .btn--primary:hover,
html.theme-dark .site-nav .site-nav__catalogs:hover {
  background: #b5513c;
  color: #ffffff;
}
/* The dialogs' own submit buttons (.sales-dialog__send, .login-dialog__submit) were in the two
   lists above until later on 2026-11-04, when the owner ruled that every modal keeps its
   light-mode colours in dark mode (the "light island" block at the end of this file). */
/* Except ON the coral band, where a coral button would vanish: it keeps the band's own white
   button with a coral label, as in light mode (`.band--coral .btn--primary`). */
html.theme-dark .band--coral .btn--primary {
  background: #ffffff;
  color: #cb5f48;
}
html.theme-dark .band--coral .btn--primary:hover {
  background: #f3e9e6;
  color: #b5513c;
}
/* The "Install the app..." download cards in dark mode (owner, 2026-11-04: 'When on dark theme,
   the "Download the app..." CTAs need to use the same background color of our primary action
   buttons #C1533A and the font color should be #ffffff'; later the same day, "Change the
   background color of all the 'Install the app...' CTAs to #48AC9C", the brand's Teal Green,
   dark mode only). Card ground, border, heading and subline; the store badges are official art
   and stay as they are. (0,2,1)/(0,2,2) beat the `.inverted .card--dark` and
   `.inverted .card--download h2/p` island rules above. */
html.theme-dark .card--download {
  background: #48ac9c;
  border-color: #48ac9c;
  color: #ffffff;
}
html.theme-dark .card--download h2,
html.theme-dark .card--download p {
  /* Owner, 2026-11-04: 'Change the font color of "Install the app and get started today.
     LotJitsu works seamlessly across mobile and desktop." to #919191, and remove all shadow
     behind this text.' Replaces #ffffff and the 7px #338678 glow. Then, the same day, after
     #919191 came out at 1.15:1 on this card's #48ac9c: "try #232323". And then: "Bring the
     shadow back, but change the color to #ffffff": the same glow on every side, now white. Its
     7px went to 15px after: "Change the distance of the shadow to 15px", then "change distance
     to 20px and opacity to 35%", then "change both shadows to 40% opacity" (45% tried, back to 40%). The text then went from
     #232323 to #1B2323 (owner, 2026-11-05). */
  color: #1b2323;
  text-shadow: 0 0 20px rgba(255, 255, 255, 0.4);
}

/* The home flow's arrows between the phones in dark mode (owner, 2026-11-04: "On dark mode the
   arrows between the phone steps needs to be #686765"); light keeps #A5A4A2. */
html.theme-dark .flow__step + .flow__step::after { background: #686765; }
/* The /pricing "What are you paying today?" card's hover border in dark mode (owner, 2026-11-04:
   "this border should be much darker"). The inverted palette never re-sets --line-hover, so the
   card kept the light theme's #B7B1AB, a bright ring on the charcoal. #4A4744 is one step above
   its resting --line (#3D3B39 in dark), so the hover still reads without the bright ring. */
html.theme-dark .pricing-page .cmp__calc-card:hover { border-color: #4a4744; }
/* ---- /pricing in dark mode (owner, 2026-11-04: "needs a better dark theme color treatment") --
   The compare table is a port of the app's plans.css and carries its own light literals: the
   near-white zebra on every other row, the five pastel belt tints behind the belt names and
   under the signed-in column, and a few fixed greys for the numbers and the yearly amount. Those
   survived the page-wide inversion while the text around them went light, so half the rows
   were white with white labels. Each literal gets its dark value here, in the order it appears
   above; the selectors repeat the originals exactly so they win on specificity plus order. */
html.theme-dark .pricing-page {
  --belt-tint-white: #3a3a3a;
  --belt-tint-yellow: #4a3812;
  --belt-tint-green: #15402a;
  --belt-tint-blue: #1a2e5c;
  /* Near-neutral, not navy (owner, 2026-11-04: "Remove some of the saturation from these black
     belt colors while on dark mode. Currently it looks dark blue, needs to look closer to
     black"); it was #30343f. Same treatment for the column's strip, pill and knot below. */
  --belt-tint-black: #313339;
  --belt-outline: rgba(241, 241, 241, 0.4);
  --danger: #f28b82;
  --danger-wash: #3a1f1d;
}
/* The zebra and the foot: a step DOWN from the #2b2b2b cell, as the light stripe is a step down
   from white. The signed-in column's tints (below) share the stripe's specificity and come later,
   so they keep winning. */
html.theme-dark .pricing-page
  table.plain
  tbody:not(.cmp__group)
  tr:nth-child(even of :not(.cmp__yearly))
  th,
html.theme-dark .pricing-page
  table.plain
  tbody:not(.cmp__group)
  tr:nth-child(even of :not(.cmp__yearly))
  td,
html.theme-dark .pricing-page table.plain .cmp__group tr:nth-child(even) th,
html.theme-dark .pricing-page table.plain .cmp__group tr:nth-child(even) td,
html.theme-dark .pricing-page table.plain tfoot td {
  background: #262626;
}
html.theme-dark .pricing-page table.plain tbody tr > td:is([data-current="white"][data-current][data-current]),
html.theme-dark .pricing-page table.plain tfoot tr > td:is([data-current="white"][data-current][data-current]),
html.theme-dark .pricing-page table.plain tbody tr > td:is([data-current="black"][data-current][data-current]),
html.theme-dark .pricing-page table.plain tfoot tr > td:is([data-current="black"][data-current][data-current]) {
  background: var(--belt-tint-white);
}
html.theme-dark .pricing-page table.plain tbody tr > td:is([data-current="yellow"][data-current][data-current]),
html.theme-dark .pricing-page table.plain tfoot tr > td:is([data-current="yellow"][data-current][data-current]) {
  background: var(--belt-tint-yellow);
}
html.theme-dark .pricing-page table.plain tbody tr > td:is([data-current="green"][data-current][data-current]),
html.theme-dark .pricing-page table.plain tfoot tr > td:is([data-current="green"][data-current][data-current]) {
  background: var(--belt-tint-green);
}
html.theme-dark .pricing-page table.plain tbody tr > td:is([data-current="blue"][data-current][data-current]),
html.theme-dark .pricing-page table.plain tfoot tr > td:is([data-current="blue"][data-current][data-current]) {
  background: var(--belt-tint-blue);
}
/* The Black Belt column's outline and pill are #111827, which vanishes on charcoal. */
html.theme-dark .pricing-page .cmp__current-outline,
html.theme-dark .pricing-page .cmp__current-outline[data-current="black"] {
  border-color: #d2d3d6;
}
/* The Black Belt's #111827 (and #1f2937 hover) read as navy on charcoal: the same darkness with
   most of the blue taken out (owner, 2026-11-04, see --belt-tint-black above). The strip, the
   pill and the knot's --belt all move together so the column stays one colour; the ring keeps
   the pill visible against the cell. */
html.theme-dark .pricing-page .plan--black .plan__strip {
  background: #171a20;
}
html.theme-dark .pricing-page .plan--black .plan__pill {
  background: #171a20;
  color: #fff;
  box-shadow: 0 0 0 1px rgba(241, 241, 241, 0.35);
}
html.theme-dark .pricing-page .plan--black .plan__pill:hover {
  background: #262a31;
  color: #fff;
}
html.theme-dark .pricing-page .plan--black .belt-knot {
  --belt: #171a20;
}
/* Fixed greys: the lot counts, the yearly amount, the read-only plan state. */
html.theme-dark .pricing-page table.plain .cmp__num { color: var(--muted); }
html.theme-dark .pricing-page table.plain .cmp__price strong.cmp__price--yearly { color: var(--ink); }
html.theme-dark .pricing-page .plan__state { color: var(--ink); }
/* The pressed Monthly/Yearly option: the avatar's coral, #CB5F48, written while the inverted
   palette still brightened --coral to #D17B63 (owner, 2026-11-04: "needs to use the same primary action
   orange as we're using for the profile avatar photo placeholder circle in the header"). Its label
   read --card, now charcoal, so it is white like every other coral fill in dark mode. */
html.theme-dark .pricing-page .billing__option[aria-pressed="true"] {
  background: #cb5f48;
  border-color: #cb5f48;
  color: #ffffff;
}
/* The "Save 15%" badge's text too (owner, 2026-11-04: "should also be the same primary action
   orange color"); it read --coral-hover, which the inverted palette lifts to #DD9179. */
html.theme-dark .pricing-page .billing__badge {
  color: #cb5f48;
}
/* The unverified-email notice's cream (#fffaf0) goes to a deep warm tint. */
html.theme-dark .verify-notice { background: #3a2e22; }
/* The platform group's subheading row takes its white label from --card, now charcoal. */
html.theme-dark .pricing-page table.plain tbody th.cmp__section { color: #ffffff; }
/* "Got Questions?" keeps its #F1F1F1 label on hover in dark mode (owner, 2026-11-04: hovering
   "should not change the color of the font"). `.inverted .btn--outline:hover` above turns every
   outline button's hover label to --surface, charcoal in dark mode, and at equal specificity it
   came later than `.btn--outline.sales-chat:hover`, so the teal fill got a charcoal label on
   hover. (0,3,1) beats that (0,3,0). The hover fill and border are unchanged. The "Sales Chat"
   face site.js swaps in is inside the same link, so it is covered too. */
html.theme-dark .btn--outline.sales-chat:hover { color: #f1f1f1; }

/* The platform guides' callout ("LotJitsu never signs in to HiBid.") in dark mode (owner,
   2026-11-04: "a section in the page content that is not formatting correctly for dark theme").
   The guide pages carry a token set of their own on `.guide-head, .guide, .compat-head, .compat`
   (above, ~line 6911) with light literals; the callout's cream fill and sand border stayed cream
   on the dark page while the text inside went light, so it read as an empty box. The card
   charcoal and a line that shows on it. The other guide tokens are left as they are, except the
   "At a glance" card's row dividers (its only user), which read too bright on charcoal: #444444
   (owner, 2026-11-04). The content column's lines take the same (owner, 2026-11-04: "Similar
   color for the lines used in the 2nd (content body) column"): the pull quote's rules above and
   below, and the step list's top lines (`--guide-rule`; the last step's coral line is its own). */
html.theme-dark :is(.guide-head, .guide, .compat-head, .compat) {
  --guide-callout-border: #5a554f;
  --guide-callout-fill: #2b2b2b;
  --guide-dl-rule: #444444;
  --guide-rule: #444444;
  --guide-quote-rule: #444444;
}

/* The Support card's tiles (Help Docs, Ask Support, Suggest on /contact; the two on help
   articles) in dark mode (owner, 2026-11-04: "The text on these 3 buttons in the right column
   should be dark colored on dark mode"). The tile stays white, solid on /contact and 60% on an
   article, but its label read --ink, near-white in dark mode, so the words vanished. The
   light-mode ink, as a literal, at rest and on hover ((0,2,1) beats the hover rule's (0,2,0)).
   The coral icon is unchanged. */
html.theme-dark .support-card__tile {
  color: #1d1a18;
}
/* Every Support card tile, the help articles' two and /contact's three, has one fill in dark mode
   (owner, 2026-11-04: "the buttons in the 3rd column of help articles need to be styled the same
   as those buttons on the Contact page", then "a shade darker than it is now", chosen as #F1F1F1
   with #E8E8E8 on hover, for both pages, dark mode only; the hover then "Another shade darker":
   #DCDCDC, then "one more shade darker": #D0D0D0; and its 1px line Coral Orange, #CB5F48: "the 1px
   outline shown onHover of these right-column buttons should be #CB5F48"). The article tiles' 60% white read as
   grey on charcoal. Size stays each page's own; /contact's third-larger scale is /contact only.
   `.support-card` is in the selector to reach (0,3,1), past /contact's own fill rule (0,3,0)
   and its hover (0,4,0). */
html.theme-dark .support-card .support-card__tile {
  background: #f1f1f1;
}
html.theme-dark .support-card .support-card__tile:hover {
  background: #d0d0d0;
  border-color: #cb5f48;
}

/* =========================================================================
   EVERY MODAL AND EVERY FORM FIELD IS LIGHT, IN DARK MODE TOO (owner, 2026-11-04: "For all
   elements used in the modals we need to copy the colors used in light mode to dark mode. For
   now they should not be different", then "Same logic applies to ALL other form fields").

   HOW. The dark theme is the `inverted` palette on <html>, so every <dialog> and every field
   inherited its tokens and rendered half-dark: light panels with near-white labels, washed-out
   buttons, dark-mode ink typed into a white search box. This block is the stylesheet's own
   "light island" idiom (see `.inverted .calc__out div` above) applied to each dialog and each
   field: the token set `.inverted` changes is restated at its :root value, so every rule inside
   that reads a token renders exactly as in light mode, and `color-scheme: light` makes the
   browser's own parts (the dialog's default Canvas/CanvasText, carets, autofill, select popups,
   scrollbars, the number spinners) match too. Below it, each `.inverted ...` or
   `html.theme-dark ...` rule that can reach INTO a dialog or a field is put back to its
   light-mode value, one by one.

   WHAT THIS REPLACED. The Savings Calculator dialog had a dark palette of its own for a few
   hours on 2026-11-04 (charcoal body, dark tiles), superseded by this rule; the dialogs' submit
   buttons were in the coral primary-button list above, likewise.

   The tokens `.inverted` leaves alone (--field-bg, --teal, --sky, --dark, --line-hover, ...)
   need no restating. The pricing page's tokens are included because `html.theme-dark
   .pricing-page` redefines them on <body>, which every dialog on /pricing sits inside.
   ========================================================================= */
html.theme-dark dialog,
html.theme-dark input,
html.theme-dark textarea,
html.theme-dark select,
html.theme-dark .blog-search__field {
  color-scheme: light;
  --coral: #cb5f48;
  --coral-hover: #a8533e;
  --coral-soft: #cb5f48;
  --green: #349744;
  --ink: #1d1a18;
  --btn-ink: #232323;
  --btn-outline: #6a6361;
  --body-text: #3d3835;
  --muted: #6a6360;
  --surface: #edebe8;
  --band: #e9e1de;
  --band-deep: #e7e0d9;
  --card: #ffffff;
  --line: #e2dad3;
  --shadow: 0 10px 30px rgba(29, 26, 24, 0.08);
  --belt-tint-white: #f2f2f2;
  --belt-tint-yellow: #fcf2d8;
  --belt-tint-green: #def2e6;
  --belt-tint-blue: #e3ebfc;
  --belt-tint-black: #dedfe1;
  --belt-outline: rgba(29, 26, 24, 0.4);
  --danger: #b3261e;
  --danger-wash: #fbeceb;
}
/* The search box's wrapper paints nothing and holds no text of its own, but what it inherits is
   the page's colour; stated as light mode computes it, so the box is identical to the last
   property. The contact form's labels are NOT in the island: they are page text beside the
   field, and light-mode ink would vanish on the dark page. */
html.theme-dark .blog-search__field {
  color: var(--body-text);
}
/* The /support index's bar on the dark theme, from the web app's header bar (owner, 2026-11-07):
   #373737 at rest, #3f3d3d on hover, and the light #e0dfdb with a coral border while focused.
   `[data-help-search]` is on the index's form and not on the article aside's, so the aside keeps
   --field-bg. */
html.theme-dark [data-help-search] .help-search__input {
  background: #e0dfdb;
  color: #1a1716;
}
html.theme-dark [data-help-search] .help-search__input:not(:focus) {
  background: #373737;
  border-color: #373737;
}
html.theme-dark [data-help-search] .help-search__input:not(:focus):hover {
  background: #3f3d3d;
  border-color: #3f3d3d;
}
html.theme-dark [data-help-search] .blog-search__icon { color: #848484; }
html.theme-dark [data-help-search] .blog-search__field:focus-within .blog-search__icon { color: #232323; }
html.theme-dark [data-help-search] .help-search__input::placeholder { color: #848484; }
html.theme-dark [data-help-search] .help-search__input:focus::placeholder { color: #232323; }
/* The /contact form's fields, the same colours and dark theme only (owner, 2026-11-07). */
html.theme-dark .contact-form .field input,
html.theme-dark .contact-form .field textarea {
  background: #e0dfdb;
  color: #1a1716;
}
/* `.inverted .btn--outline:hover` turns the hover label to --surface; light mode's is #fff.
   `.inverted .btn--primary` gives the fill a charcoal label and the coral rule above gives it
   #CB5F48; light mode is #c16850 / #c4573a with a white label (lines 265 and 278). */
html.theme-dark dialog .btn--outline:hover {
  color: #fff;
}
html.theme-dark dialog .btn--primary {
  background: #c16850;
  color: #fff;
}
html.theme-dark dialog .btn--primary:hover {
  background: #c4573a;
  color: #fff;
}
/* The calculator's fields: `.inverted .calc input[type="number"]` sets --line and #1d1a18 where
   the dialog's own rule says #d1cbc8 and light mode leaves the text to the browser. */
html.theme-dark dialog .calc input[type="number"] {
  border-color: #d1cbc8;
  color: revert;
}
/* Placeholders `.inverted` recolours to --muted; light mode has no rule for them at all. */
html.theme-dark .field input::placeholder,
html.theme-dark .field textarea::placeholder,
html.theme-dark dialog .calc input::placeholder {
  color: revert;
}
/* The calculator's result tiles: `.inverted .calc__out div` flips them to a light island and
   recolours the figures; light mode is the three dark tiles with #f1f1f1 figures (lines 3687,
   3693-3694, 3705-3707). The tile's own color is inherited there, and nothing in it shows it. */
html.theme-dark dialog .calc__out div {
  background-color: #232323;
  color: inherit;
}
html.theme-dark dialog .calc__out--before div {
  background-color: #454545;
}
html.theme-dark dialog .calc__out--savings div {
  background-color: #50988a;
}
html.theme-dark dialog .calc__out strong,
html.theme-dark dialog .calc__out span {
  color: #f1f1f1;
}
html.theme-dark dialog .calc__out--savings strong,
html.theme-dark dialog .calc__out--savings span {
  color: #fff;
}
/* The zebra `.inverted` darkens, and the Black Belt knot the pricing block above desaturates. */
html.theme-dark dialog table.plain tbody tr:nth-child(even) {
  background: #fdfbfa;
}
html.theme-dark dialog .plan--black .belt-knot {
  --belt: #111827;
}
