CSS reference
Every rule in the source stylesheet, grouped by the section and sub-section comments it sits under, with the comment above it as its note.
Generated from src/input.css on every build. The source has
4793 lines, 1073 rules and 73 custom properties. Tailwind
compiles it to assets/, and to
assets/ for the style guide and this page.
To change this page, edit the comments in src/input.css:
/* ==== */ banners open a section, /* --- Title: text --- */
opens a sub-section, and a comment directly above a rule becomes its note.
Custom properties
73 custom properties on :root. Dark is the value from
:root[data-theme="dark"] and the prefers-color-scheme block
(listed twice only where the two disagree).
| Token | Light | Dark | Elsewhere · line |
|---|---|---|---|
| Palette — identity, never changes | |||
| --navy | #0F1E4A | — | 43 |
| --sky | #4FACFE | — | 44 |
| --cyan | #00F2FE | — | 45 |
| --seagrass | #0BA678 | — | 46 |
| --coral | #FE1E1A | — | 47 |
| --lantern | #E07A00 | — | 48 |
| --sand | #FFC93C | — | 49 |
| --midnight | #0A0F24 | — | 50 |
| --paper | #F5FBFF | — | 51 |
| Semantic — light | |||
| --bg | #FEFEFE | #0A0F24 | @media print › :root:root:root: #FFFFFF 54 |
| --surface | #FDFEFF | #101A3A | @media print › :root:root:root: #FFFFFF 55 |
| --ink | #0F1E4A | #EAF0FF | @media print › :root:root:root: #0F1E4A 56 |
| --ink-soft | #3C4A73 | #BCCCF0 | @media print › :root:root:root: #3C4A73 57 |
| --line | #DDE4F2 | #25315C | @media print › :root:root:root: #C9D3E6 58 |
| --line-soft | #EAEFF9 | #1B274A | @media print › :root:root:root: #DDE4F2 59 |
| --tag-bg | #EAF3FF | #16234A | @media print › :root:root:root: #FFFFFF 60 |
| --warn-bg | #EDF4FE | #241C0C | @media print › :root:root:root: #FFFFFF 61 |
| --warn-line | #C3D8F2 | #4A3A14 | @media print › :root:root:root: #A9B8D4 62 |
| --warn-ink | #2F4468 | #E8C878 | @media print › :root:root:root: #2F4468 63 |
| Type — the label face as a custom property, for the handful of rules that need the family from a var() rather than theme(). Resolved from the Tailwind theme so tailwind.config.js stays the single source of truth for the stack. | |||
| --font-sans | theme('fontFamily.label') | — | 68 |
| Inline link color — a darkened Sky that clears WCAG AA (6.5:1) on the light ground; nav / buttons / footer set their own color and are unaffected. | |||
| --link | #0B5FA5 | var(--sky) | @media print › :root:root:root: #0B5FA5 72 |
| Accent-as-ink: darker palette steps for when an accent carries text or a focus ring on the light ground (raw Sky is 2.4:1, Lantern 3.0:1). Each clears 4.3:1 on its own 12% wash. Dark theme points them back at the palette. | |||
| --sky-ink | #1B6AB4 | var(--sky) | @media print › :root:root:root: #1F74C4 77 |
| --seagrass-ink | #087A58 | #1FC493 | @media print › :root:root:root: #087A58 78 |
| --lantern-ink | #A85A00 | var(--lantern) | @media print › :root:root:root: #A85A00 79 |
| --coral-ink | #C4140F | #FF6A66 | @media print › :root:root:root: #C4140F 80 |
| --ba-after-bg | var(--seagrass-ink) | var(--seagrass) | @media print › :root:root:root: var(--seagrass-ink) 81 |
| --ba-after-ink | #FFFFFF | var(--midnight) | @media print › :root:root:root: #FFFFFF 82 |
| Sticky header height: logo + py-4 (2rem) + 1px border. One source for scroll-padding and every sticky offset; logo height is set from it below. | |||
| --logo-h | 3.25rem | — | @media (min-width: 480px) › :root: 5.46875rem @media (min-width: 1024px) › :root: 6.25rem 86 |
| --header-h | calc(var(--logo-h) + 2rem + 1px) | — | @media screen › :root[data-text]: calc((var(--logo-h) + 2rem) / var(--text-scale) + 1px) 87 |
| --ring | var(--navy) | var(--sky) | @media print › :root:root:root: #0F1E4A 89 |
| --focus-on-dark | var(--sky) | — | 90 |
| Component roles. Every light/dark difference a component has is a token here, so the two dark blocks below are the only place dark is defined. | |||
| --brand-mark | url('../logomarks/marks/advocate-beacon-primary.svg') | url('../logomarks/marks/advocate-beacon-dark.svg') | @media print › :root:root:root: url('../logomarks/marks/advocate-beacon-primary.svg') 94 |
| --brand-ink | var(--navy) | #EAF0FF | @media print › :root:root:root: var(--navy) 95 |
| --brand-sub | var(--sky-ink) | #6FC3FF | @media print › :root:root:root: var(--sky-ink) 96 |
| Sky itself is 2.4:1 on white | |||
| --brand-foot-mark | url('../logomarks/marks/advocate-beacon-deep-tone-alt.svg') | url('../logomarks/marks/advocate-beacon-reversed.svg') | 97 |
| --brand-foot-ink | var(--navy) | var(--paper) | 98 |
| --brand-foot-sub | #1E2B5A | var(--cyan) | 99 |
| #2E3F72 was 4.18:1 on the Sky footer | |||
| --cta-bg | var(--navy) | var(--sky) | 100 |
| .btn-primary | |||
| --cta-ink | var(--paper) | var(--navy) | 101 |
| --cta-hover-bg | var(--midnight) | var(--sky) | 102 |
| --cta-hover-filter | none | brightness(1.08) | 103 |
| --accent-hover-bg | var(--navy) | var(--paper) | 104 |
| Sky-at-rest controls: path CTAs, drawer tab | |||
| --accent-hover-ink | var(--paper) | var(--navy) | 105 |
| --nav-current | var(--navy) | var(--sky) | 106 |
| --path-photo-opacity | 1 | 0.5 | 107 |
| --choice-accent | var(--sky) | — | 108 |
| --theme-icon-light | block | none | 109 |
| --theme-icon-dark | none | block | 110 |
| Site footer: a Sky ground with Navy ink in light (Navy 6.6:1, #20305C 5.3:1 on Sky); Midnight with Paper in dark. | |||
| --footer-bg | var(--sky) | var(--midnight) | 114 |
| --footer-ink | var(--navy) | var(--paper) | 115 |
| --footer-dim | #20305C | color-mix(in srgb, var(--paper) 74%, transparent) | 116 |
| --footer-title | #20305C | color-mix(in srgb, var(--paper) 80%, transparent) | 117 |
| --footer-link | var(--navy) | var(--sky) | 118 |
| --footer-accent | var(--navy) | var(--sky) | 119 |
| --footer-accent-ink | var(--paper) | var(--navy) | 120 |
| --footer-sw-off | #20305C | color-mix(in srgb, var(--paper) 62%, transparent) | 121 |
| --footer-edge | transparent | var(--line) | 122 |
| Contact drawer + mobile nav ground. In light these resolve to the page's own tokens (computed here at :root, so re-pointing them in scope is not circular); dark inverts both panels to a Sky ground (Navy on Sky ≈ 6.6:1). | |||
| --panel-bg | var(--bg) | var(--sky) | 127 |
| --panel-surface | var(--surface) | #EAF3FF | 128 |
| --panel-ink | var(--ink) | var(--navy) | 129 |
| --panel-ink-soft | var(--ink-soft) | #20305C | 130 |
| --panel-line | var(--line) | #2E3F72 | 131 |
| --panel-line-soft | var(--line-soft) | #A9D6FF | 132 |
| --panel-tag-bg | var(--tag-bg) | #CFE6FF | 133 |
| --panel-link | var(--link) | var(--navy) | 134 |
| --panel-ring | var(--ring) | var(--navy) | 135 |
| --panel-accent | var(--choice-accent) | var(--navy) | 136 |
| --panel-cta-bg | var(--cta-bg) | var(--navy) | 137 |
| --panel-cta-ink | var(--cta-ink) | var(--paper) | 138 |
| --panel-cta-hover-bg | var(--cta-hover-bg) | var(--midnight) | 139 |
| --panel-cta-hover-filter | var(--cta-hover-filter) | none | 140 |
| Set only outside the light :root block. | |||
| --text-scale | — | — | @media screen › :root[data-text="lg"]: 1.125 @media screen › :root[data-text="xl"]: 1.25 @media screen › :root[data-text="xxl"]: 1.5 297 |
| --text-unscale | — | — | @media screen › :root[data-text="lg"]: 0.8889 @media screen › :root[data-text="xl"]: 0.8 @media screen › :root[data-text="xxl"]: 0.6667 297 |
Tailwind theme
The utilities in the HTML come from these keys in tailwind.config.js
(everything except screens extends Tailwind's defaults). Content scanned:
./site/*.html, ./src/partials/*.html, ./src/js/**/*.js.
Breakpoints
| sm | 480px |
|---|---|
| md | 768px |
| nav | 930px |
| lg | 1024px |
| xl | 1200px |
Colors
| navy | #0F1E4A |
|---|---|
| sky | #4FACFE |
| cyan | #00F2FE |
| coral | #FE1E1A |
| midnight | #0A0F24 |
| paper | #F5FBFF |
| surface | var(--surface) |
| ink | var(--ink) |
| ink-soft | var(--ink-soft) |
| line | var(--line) |
| line-soft | var(--line-soft) |
Font families
| logo | "Tenor Sans", ui-sans-serif, "Gill Sans", "Trebuchet MS", system-ui, sans-serif |
|---|---|
| label | Questrial, ui-sans-serif, "Century Gothic", "Avenir Next", system-ui, sans-serif |
| body | Ysabeau, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif |
| mono | "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace |
Font sizes
| lede | 1.3125rem · line-height 1.45 |
|---|---|
| h4 | 1.4375rem · line-height 1.3 |
| h3 | clamp(1.4375rem, 1.15rem + 1.2vw, 1.875rem) · line-height 1.25 |
| h2 | clamp(1.875rem, 1.4rem + 1.9vw, 2.5rem) · line-height 1.18 |
| h1 | clamp(2.25rem, 1.4rem + 3.4vw, 3.625rem) · line-height 1.08 |
Max widths
| canvas | 1000px |
|---|---|
| measure | 62ch |
| prose | 68ch |
Gaps
| gutter | 24px |
|---|
Radii
| card | 12px |
|---|---|
| tile | 10px |
Shadows
| card | 0 1px 2px rgba(10, 15, 36, 0.06), 0 4px 12px rgba(10, 15, 36, 0.06) |
|---|---|
| lift | 0 2px 8px rgba(10, 15, 36, 0.08), 0 12px 28px rgba(10, 15, 36, 0.10) |
Easing
| calm | cubic-bezier(0.22, 1, 0.36, 1) |
|---|
Preamble
Coastal Healthcare Advocates — source stylesheet
Build: npm run build (watch while developing: npm run dev)
Output: site/assets/
site/assets/
Both are git-ignored, generated by scripts/build-css.js from this one file and the
same tailwind.config.js; only the content scanned for used classes differs, which
keeps the style guide's .sg-* reference classes out of the stylesheet visitors load.
5 rules · starts at 1
Base
49 rules · starts at 33
Tokens #
Verbatim from the Coastal Brand Guidelines artifact (§01, §07)
| Selector | Note | Line |
|---|---|---|
:root, .sg-theme-light | .sg-theme-light rides along so the style guide's light panel can show the light palette while the page itself is dark. Custom properties inherit from the nearest defining ancestor, so the panel's own declarations win inside it. | 41 |
:root:not([data-theme="light"])@media (prefers-color-scheme: dark) | Dark: explicit toggle wins in both directions; system pref applies when unset. | 146 |
:root[data-theme="dark"], .sg-theme-dark | Same values as the media block above — keep the two in lockstep. .sg-theme-dark is the style guide's dark panel; it takes these values rather than repeating them, so dark is still defined in only two places. | 210 |
:root@media (min-width: 480px) | 271 | |
:root@media (min-width: 1024px) | 272 | |
* | 274 | |
*, ::before, ::after | Keep even Tailwind's Preflight defaults on the style-guide palette. | 276 |
::placeholder | 277 | |
html | 279 | |
:root[data-text]@media screen | Text size (Site preferences → data-text on <html>, set by theme.js). Scales the root, so every rem-sized thing grows with the text. At Larger and Largest (xl / xxl) the page also keeps its phone layout and hamburger menu at every width (scripts/postcss/text-layout.js rewrites the width media queries); Default and Large keep the normal responsive layout. The header bar is zoomed back to its usual size, since a 1.5× logo would crowd it; the menu panel inside it is zoomed up again, since it's reading matter. Screen only: print sets its own root size. | 293 |
:root[data-text="lg"]@media screen | 297 | |
:root[data-text="xl"]@media screen | 298 | |
:root[data-text="xxl"]@media screen | 299 | |
:root[data-text] .site-header@media screen | 300 | |
:root[data-text] .site-header .canvas-wide@media screen | 301 | |
:root[data-text] #mobile-nav@media screen | 302 | |
:root[data-text] .site-header .header-icon-btn@media screen | The header's round buttons too: a zoomed-down 44px would miss the touch-target minimum, and their icons are read at the chosen size. | 305 |
:root[data-text] .brand-lockup-footer@media screen | The footer lockup is a fixed-geometry logo, not text to read. | 307 |
:root[data-text] :is(a, code)@media screen | Long addresses and paths can't wrap at a space. | 309 |
:root[data-text] :is(h1, h2, h3, h4)@media screen | So can a single long word in a big heading ("Responsibility") or a button ("Consultation") on a phone, and the style guide's type specimens. | 312 |
:root[data-text] .btn@media screen | A button's label is an anonymous flex item, which only shrinks below its longest word with anywhere. | 315 |
:root[data-text] .sg-specimen > *@media screen | 316 | |
:root[data-text] .fineprint-doc@media screen and (max-width: 599px) | Phones and small tablets: the sample statement's three-column table can't fit scaled type, so the mock document keeps its usual size there. | 321 |
body | 324 | |
h1, h2, h3, h4 | §04 Heading Sizes. All headings = Questrial 400 (matches H1). | 336 |
h1 | Each step is at least 1.25x the one below (body 18px, H4 23px, H3 23-30px, H2 30-40px, H1 36-58px), so size alone separates the roles; leading tightens as the size grows. | 347 |
h2 | 352 | |
h3 | 353 | |
h4 | 354 | |
.section h2[id$="-h"]::after, .section--tight h2[id$="-h"]::after | Section headings carry a short accent rule beneath them — left-aligned, 8rem, in Sky. Hidden on the centered navy Schedule heading; re-centered under the centered Intake heading. | 359 |
#schedule-h::after | 369 | |
.text-center > h2[id$="-h"]::after | 370 | |
p | 372 | |
a | Inline / prose links read as links: distinct color + underline. Components that are links but shouldn't look like body links (.nav-link, .btn, .link-quiet, .site-footer a) all set their own color + text-decoration. | 377 |
a:hover | 384 | |
.text-link-inline | A JS action (opens the Site preferences dialog) mentioned inline in prose, styled to match the surrounding text link exactly. | 387 |
.text-link-inline:hover | 396 | |
.on-dark a:not([class]) | Classless links on a dark ground (e.g. the navy Schedule section) need the light-on-dark link color instead of the darkened Sky. | 399 |
a[href^="http"]:not([class]):not([href*="coastalhealthcareadvocates.org"])::after@media screen | Plain text links that leave the site get a trailing "↗" (e.g. ada.gov). Classed links — buttons, icon circles, the GNA badge — carry their own icon, and absolute links to our own domain aren't external. Drawn as a mask so it takes the link color, in the sprite's 1.75px round stroke. Screen only: print swaps ::after for the URL in brackets. | 407 |
a[href^="http"]:not([class]):not([href*="coastalhealthcareadvocates.org"])::after@media screen and (prefers-reduced-motion: no-preference) | 420 | |
a[href^="http"]:not([class]):not([href*="coastalhealthcareadvocates.org"]):hover::after@media screen and (prefers-reduced-motion: no-preference) | 423 | |
abbr[title] | Abbreviations: a quiet dotted underline in running text signals the title expansion. Inside links and headings it would stack on the link underline or clutter the type, so it's dropped there. | 431 |
:where(a, button, h1, h2, h3, h4) abbr[title] | 438 | |
::selection | 440 | |
:where(a, button, input, textarea, select, summary, [tabindex]):focus-visible | 442 | |
.on-dark :where(a, button, input, textarea, select, summary, [tabindex]):focus-visible | On dark grounds the navy ring vanishes — switch to sky. | 448 |
*, *::before, *::after@media (prefers-reduced-motion: reduce) | Reduced motion: drop spatial movement and choreography, keep the short color/opacity feedback that tells a visitor an action registered. | 455 |
.hero-intro [data-rise], .hero-intro .hero-atmos::before@media (prefers-reduced-motion: reduce) | 464 | |
html@media (prefers-reduced-motion: no-preference) | 472 |
Components
887 rules · starts at 476
General #
| Selector | Note | Line |
|---|---|---|
.canvas | 480 | |
.canvas-wide | Header / footer breathe a little wider than the 1000px reading column. | 487 |
.canvas, .canvas-wide@media (min-width: 480px) | 494 | |
.section | padding-block only — the hero puts .section and .canvas on one element, and a padding: X 0 shorthand there would wipe .canvas's padding-inline. | 499 |
.section--tight | 500 | |
.lede | §Type: .lede — Ysabeau 21px / 1.45, --ink-soft, max 60ch | 503 |
.prose-measure | 510 | |
.prose-measure > * + * | 511 | |
.meta | Small supporting text. Body typeface — IBM Plex Mono is reserved for actual figures (see .num, .price-fee, .step-num). | 515 |
.num | Figures: prices, counts, code values. IBM Plex Mono otherwise runs only in form error messages (.field-error, .error-summary). Phone numbers stay in the body face — Mono's full-width space and brackets read as gaps there. | 525 |
code | File paths and token names have no spaces to wrap at; let them break anywhere rather than push a phone-width page sideways. | 532 |
Buttons #
| Selector | Note | Line |
|---|---|---|
.btn | 535 | |
.btn:hover | A hair of lift on hover; the press (:active, below in source so it wins) dips back down. Reduced motion drops the transform via the global rule. | 559 |
.btn:active | 560 | |
.btn-primary | Primary CTA — Navy fill, Paper text on light. On dark the page ground is near-navy, so the CTA flips to Sky fill / Navy text to stay legible. | 564 |
.btn-primary:hover | 569 | |
.btn-ghost | 575 | |
.btn-ghost:hover | 580 | |
.btn-on-dark | 582 | |
.btn-on-dark:hover | 587 | |
.btn-ghost-on-dark | 589 | |
.btn-ghost-on-dark:hover | 594 | |
.link-quiet | 599 | |
.link-quiet:hover | 610 | |
.link-quiet svg | A trailing icon (the "→" on "Talk about referrals") nudges in its direction of travel on hover. Leading icons — the PDF marks — are :first-child, so :last-child leaves them alone. | 614 |
.link-quiet:hover svg:last-child | 615 | |
.link-quiet svg.icon-back | "← Go back" (secondary.js): a smaller leading arrow that nudges left, its direction of travel. Same specificity as the rule above, so it wins by order. | 618 |
.link-quiet:hover svg.icon-back | 619 |
Surfaces #
| Selector | Note | Line |
|---|---|---|
.card | 622 | |
.tile | 628 | |
.tile:hover | Hover acknowledges the pointer with the same small lift the how-cards use; under reduced motion the global rule drops the transform and keeps the border + elevation, so the feedback still reads. | 639 |
:is(.card, .tile, .how-card-inner) :is(h2, h3, h4) | Headings inside a card/tile drop the tracking for readability at small sizes. | 645 |
.callout | 648 | |
.icon | Sizing only. Stroke vs. fill is set per-symbol in the sprite so the two solid brand glyphs (LinkedIn, Facebook) stay filled. | 656 |
.icon-lg | 657 | |
.icon-pdf | 658 |
Pricing choices #
The four ways in, above the detailed table
| Selector | Note | Line |
|---|---|---|
.price-choices | 671 | |
.price-choices@media (min-width: 40rem) | 677 | |
.price-choice | 678 | |
.price-choice h3 | 687 | |
.price-choice p | 688 | |
.price-choice--start | The free call is where everyone should begin: tinted ground and a sky rule. | 690 |
.price-choice-kicker | 694 | |
.price-choice .price-choice-kicker | 700 | |
.price-choice-fee | 701 | |
.price-choice-fee--free | 708 | |
.price-choice-cta | 713 |
Pricing table #
| Selector | Note | Line |
|---|---|---|
.price-table | 716 | |
.price-table th, .price-table td | 722 | |
.price-table thead th | 728 | |
.price-table thead th:last-child | 737 | |
.price-col-name | 738 | |
.price-col-fee | 739 | |
.price-group | 741 | |
.price-table tbody:first-of-type .price-group | 752 | |
.price-table tbody td, .price-table tbody th[scope="row"] | 754 | |
.price-table th[scope="row"] | 757 | |
.price-table td | 763 | |
.price-table .price-fee | full-strength ink for older readers | 765 |
.price-table td.price-fee--free | 772 | |
.price-unit | 778 | |
.price-fee-label | "Fee:" label for the stacked layout — a real, visually-shown span (not CSS content), hidden while the Fee column header is present. | 786 |
.table-wrap:has(.price-table)@media (max-width: 40rem) | Below 40rem three columns can't fit, so each service stacks into its own block instead of forcing a horizontal scroll that hides the fee. The table elements carry explicit ARIA table roles in index.html, so display:block here doesn't strip the row/header relationships from assistive tech. | 793 |
.price-table@media (max-width: 40rem) | 794 | |
.price-table colgroup, .price-table thead@media (max-width: 40rem) | 795 | |
.price-table tbody, .price-table tr, .price-table th, .price-table td@media (max-width: 40rem) | 797 | |
.price-table tr:has(th[scope="row"])@media (max-width: 40rem) | 802 | |
.price-table tr:has(th[scope="row"]) + tr:has(th[scope="row"])@media (max-width: 40rem) | 803 | |
.price-table tbody th[scope="row"], .price-table tbody td@media (max-width: 40rem) | 806 | |
.price-table td@media (max-width: 40rem) | 809 | |
.price-table td.price-fee@media (max-width: 40rem) | 810 | |
.price-fee-label@media (max-width: 40rem) | 814 |
Definition-list rows (bio credentials) #
Header #
| Selector | Note | Line |
|---|---|---|
.site-header | 846 | |
.site-header@supports not (backdrop-filter: blur(1px)) | 857 | |
.site-header.scrolled | Picks up a soft elevation once the page has scrolled off the top, so the bar separates from the content passing under it (main.js toggles .scrolled). On the near-black dark ground this shadow is invisible — the border carries it there — which is fine. | 863 |
.site-header.header-hidden | visibility (delayed to the end of the slide) takes the links out of the tab order, so keyboard focus can never land on an off-screen nav. | 867 |
.brand-lockup | Logo — the horizontal lockup rebuilt with live type, so the company name is real text (reader mode, translation, copy, search) rather than an image. Only the mark is a picture, CSS-swapped by theme (no flash, no JS). Every length is in --u, one unit of the outlined SVG's 271.27 × 82 artboard, so the lockup scales from --logo-h and matches logo-horizontal-*.svg: mark at x 8, rule at x 79, wordmark at x 91.1 with baselines 40.22 and 61.22. --u multiplies lengths instead of setting a tiny em base, so a browser minimum-font-size preference can't blow the geometry up. | 884 |
.brand-lockup::before | The Advocate Beacon: the mark files are 120 × 132, drawn at half size. | 898 |
.brand-lockup::after | The rule between mark and wordmark: wordmark ink at 28%. | 907 |
.brand-lockup-name | Line boxes are 1em tall; the offsets put each baseline where the SVG has it (Tenor Sans ascent .92 / descent .25; Ysabeau typo metrics 1.054 / .289). | 919 |
.brand-lockup-name > :first-child | 929 | |
.brand-lockup-name > :last-child | 936 | |
.brand-lockup-footer | Footer colorway: deep-tone-alt on the light footer, reversed on the dark. | 944 |
.header-icon-btn | 951 | |
.header-icon-btn:hover | 963 | |
.header-icon-btn svg | 964 | |
#nav-toggle | The hamburger wears the menu panel's tokens so it matches the panel it opens: page colors in light mode, a Sky fill with Navy lines in dark mode. | 967 |
#nav-toggle:hover | 972 | |
#nav-toggle[aria-expanded="true"] svg | 973 | |
#mobile-nav, #contact-modal:not(.is-lightbox) | Mobile menu — a compact panel hung off the toggle button, not a full-width sheet. Its containing block is the .relative wrapper around the button, so it sits right under the hamburger. JS unhides it, then adds .is-open a frame later to run the entrance. | 979 |
#mobile-nav | 1002 | |
#mobile-nav.is-open | 1023 | |
#mobile-nav .nav-link | 1024 | |
#mobile-nav .nav-link::after | 1030 | |
#mobile-nav .nav-link:hover, #mobile-nav .nav-link:focus-visible | no underline-wipe inside the panel | 1031 |
#mobile-nav@media (prefers-reduced-motion: reduce) | 1034 | |
#mobile-nav.is-open@media (prefers-reduced-motion: reduce) | 1035 | |
nav[aria-label="Primary"] | Primary desktop nav — four links (Services, About, How it works, Pricing), sized up from the mobile-panel links' 1rem to 1.125rem since dropping "Who we help" and FAQs left room to size up. It appears at the nav screen (tailwind.config.js); below that the hamburger takes over — re-measure that breakpoint if the links, their size, the icons or the logo change again. | 1044 |
nav[aria-label="Primary"] .nav-link | 1047 | |
.theme-icon-light | 1051 | |
.theme-icon-dark | 1052 | |
.nav-link | 1054 | |
.nav-link::after | underline wipes in from the left on hover / keyboard focus | 1065 |
.nav-link:hover::after, .nav-link:focus-visible::after | 1077 | |
.nav-link[aria-current="true"] | Scrollspy — the link for the section currently under the viewport holds its underline and takes the accent color. | 1082 |
.nav-link[aria-current="true"]::after | 1083 |
Hero atmosphere #
Faint horizon + beacon glow, all decorative
Motion #
One authored moment: the hero steadies as the beacon comes up, once per session. Everything else is quiet feedback and continuity.
| Selector | Note | Line |
|---|---|---|
.js.hero-intro .hero-atmos::before | Focal entrance (JS-gated; content is fully visible without it). | 1123 |
.js.hero-intro [data-rise] | 1126 | |
[data-reveal]@media (prefers-reduced-motion: no-preference) | Scroll-in: "coming into the light". Each [data-reveal] element plays once, on its own, as it crosses into view, so a tall section unfolds as the visitor reads down it rather than all at once at its top edge. Headings clear from a faint haze and their Beacon Sky rule sweeps out like a beam; the content under them follows. The value picks the treatment: (bare) the element rises in heading soft focus-in, then the accent rule draws out from its start list direct children rise in turn; their line icons settle after pair two side-by-side cards drift in from their own sides checks list items rise in turn and each tick draws itself left to right rows table rows fade up in turn photo fades in while the picture eases back from a slight zoom main.js arms .reveal-pending (the hidden start state), swaps in .reveal-in to play, then clears it; it also writes --reveal-i on staggered items. Fill mode is backwards, so once an animation ends the element is back on its own styles and hover lifts work immediately. No JS, a failed script, or reduced motion: nothing is armed, nothing is hidden. Opacity starts at 0.01, not 0 — see rise-in. | 1161 |
.js :is([data-reveal=""], [data-reveal="heading"], [data-reveal="photo"]).reveal-pending, .js :is([data-reveal="list"], [data-reveal="pair"]).reveal-pending > *, .js [data-reveal="checks"].reveal-pending > li, .js [data-reveal="rows"].reveal-pending tbody tr@media (prefers-reduced-motion: no-preference) | 1163 | |
.js [data-reveal="heading"].reveal-pending::after@media (prefers-reduced-motion: no-preference) | 1167 | |
.js [data-reveal=""].reveal-in@media (prefers-reduced-motion: no-preference) | 1169 | |
.js [data-reveal="heading"].reveal-in@media (prefers-reduced-motion: no-preference) | 1173 | |
.js [data-reveal="heading"].reveal-in::after@media (prefers-reduced-motion: no-preference) | 1176 | |
.js .text-center > [data-reveal="heading"].reveal-in::after@media (prefers-reduced-motion: no-preference) | 1180 | |
.js :is([data-reveal="list"], [data-reveal="pair"]).reveal-in > *, .js [data-reveal="checks"].reveal-in > li@media (prefers-reduced-motion: no-preference) | 1182 | |
.js [data-reveal="list"].reveal-in > * > :is(.service-icon, .icon-lg, .icon:first-child)@media (prefers-reduced-motion: no-preference) | Line icons settle a beat after their row arrives. | 1188 |
.js [data-reveal="checks"].reveal-in > li > .icon@media (prefers-reduced-motion: no-preference) | 1192 | |
.js [data-reveal="pair"].reveal-in > :first-child@media (prefers-reduced-motion: no-preference) › @media (min-width: 640px) | The two entry paths come in from their own sides; stacked, they rise. | 1199 |
.js [data-reveal="pair"].reveal-in > :last-child@media (prefers-reduced-motion: no-preference) › @media (min-width: 640px) | 1200 | |
.js .ba-grid[data-reveal="pair"].reveal-in > .ba-panel--after@media (prefers-reduced-motion: no-preference) › @media (min-width: 640px) | Before/after: once Before has landed, After slides out from beneath it into its own column, the fix drawn out from under the problem. It waits hidden (visibility, not opacity) so it can't show through Before while Before is still fading in; Before sits above it for the whole move. | 1206 |
.js [data-reveal="rows"].reveal-in tbody tr@media (prefers-reduced-motion: no-preference) | 1211 | |
.js [data-reveal="photo"].reveal-in@media (prefers-reduced-motion: no-preference) | 1217 | |
.js [data-reveal="photo"].reveal-in img@media (prefers-reduced-motion: no-preference) | 1220 | |
::view-transition-group(root), ::view-transition-old(root), ::view-transition-new(root) | Continuity: light↔dark blends like dusk, but only during an explicit toggle. Where the browser has View Transitions, theme.js runs the switch inside one and these two rules time the cross-fade; the second lets a press through to the page while it runs, so a quick second toggle isn't swallowed. They hold no class on purpose: nothing else on the site starts a view transition. | 1265 |
::view-transition | 1268 | |
.theme-transition, .theme-transition *@media (prefers-reduced-motion: no-preference) | The fallback for browsers without them: the .theme-transition class is added for ~320ms by theme.js. Every element animating its own colors is far more work per frame, which is why it is only the fallback. Reduced-motion visitors get the instant swap they had before. | 1274 |
.theme-switch-ico@media (prefers-reduced-motion: no-preference) | 1278 | |
.theme-transition :is(.theme-switch-knob, .theme-orb-ico)@media (prefers-reduced-motion: no-preference) | The crossfade above flattens every transition to color-only while it runs, which would snap the switch knob and the header orb's swipe. | 1281 |
.theme-switch:active .theme-switch-ico | 1287 | |
.framed | 1289 | |
.what-intro::after | Soft-edged photo that sits beside the "what is an advocate" intro copy. The radial mask feathers every edge so it reads as atmosphere behind the text rather than a placed, framed asset. It floats right at every width so the copy wraps around it; on extremely narrow screens there is no room for a readable text column beside it, so it is hidden. .what-intro carries the clearfix. | 1303 |
.vignette-figure | 1304 | |
.vignette-figure img | Sizing/loading is handled the hero way — a <picture> with 640w/1024w sources and sizes="(min-width:1024px) 40vw, 100vw", plus .block/.h-auto/ .w-full utilities on the <img>. This rule only adds the vignette skin. | 1313 |
.vignette-figure@media (max-width: 400px) | Extremely narrow: drop it — a floated image would leave the copy in a column too thin to read. | 1321 |
.vignette-figure@media (min-width: 768px) | 1324 | |
.vignette-figure@media (min-width: 1024px) | 1327 | |
.what-intro > .callout | From 1024px the photo stands 1rem taller than the copy beside it, which left it half the air above the callout that the copy has. The callout clears the float, so the photo's own 2rem bottom margin is what holds it off. | 1332 |
Path cards (the two entry paths) #
| Selector | Note | Line |
|---|---|---|
.path-card | 1335 | |
.path-card::before | Calm full-bleed photo behind the whole card under a heavy surface wash, so navy body copy keeps its contrast. Decorative; images are same-origin. | 1339 |
.path-card > * | 1356 | |
.path-card .btn-ghost | Path CTAs read as light blue at rest in both themes, matching the "Get Started" tab; hover deepens to navy in light mode and lifts to white in dark mode. | 1361 |
.path-card .btn-ghost:hover | 1366 | |
.path-card--crisis | JPEG is the safe fallback; @supports upgrades to AVIF/WebP where the image-set() type() syntax is understood (Chrome 113+, Safari 17+, FF 89+). | 1374 |
.path-card--referrer | 1375 | |
.path-card--crisis@supports (background-image: image-set(url('x.webp') type('image/webp'))) | 1378 | |
.path-card--referrer@supports (background-image: image-set(url('x.webp') type('image/webp'))) | 1385 | |
.path-card::before | 1394 | |
.path-card::before@media (prefers-contrast: more) | 1397 |
How it works #
Pinned note-cards on ruled paper, joined by a marching-ants dashed thread (adapted from a 21st.dev "how-it-works")
| Selector | Note | Line |
|---|---|---|
.step-num | 1402 | |
.how-flow | 1411 | |
.how-flow::before | faint notebook rule behind the cards, faded out at both edges | 1414 |
.how-steps | 1426 | |
.how-steps > :nth-child(1) | --how-accent tints pins + borders; --how-ink is the contrast-safe step for the numerals. | 1436 |
.how-steps > :nth-child(2) | 1437 | |
.how-steps > :nth-child(3) | 1438 | |
.how-steps > :nth-child(4) | 1439 | |
.how-step | 1441 | |
.how-card | white "photo" frame around a tinted inner panel | 1444 |
.how-card-inner | 1454 | |
.how-card-inner h3 | 1463 | |
.how-pin | pushpin straddling the top edge of the frame | 1466 |
.how-pin svg | 1478 | |
.how-spine | connector: dashed path stretched over the flow (preserveAspectRatio="none", non-scaling-stroke keeps the dash rhythm even under the non-uniform stretch) | 1482 |
.how-spine path | 1483 | |
.how-weave-line | Resting / no-JS state: the marching dashes, exactly as before. | 1491 |
.how-weave-trace | The continuous trace is only used for the JS-driven draw-on (below). | 1493 |
.how-steps@media (min-width: 768px) | 1496 | |
.how-step@media (min-width: 768px) | 1505 | |
.how-step:nth-child(1)@media (min-width: 768px) | cards pushed to the outer edges; the thread weaves the gap between them | 1507 |
.how-step:nth-child(2)@media (min-width: 768px) | 1508 | |
.how-step:nth-child(3)@media (min-width: 768px) | 1509 | |
.how-step:nth-child(4)@media (min-width: 768px) | 1510 | |
.how-step:nth-child(odd) .how-card@media (min-width: 768px) | a loose, hand-pinned tilt that straightens on hover | 1513 |
.how-step:nth-child(even) .how-card@media (min-width: 768px) | 1514 | |
.how-card:hover@media (min-width: 768px) | 1515 | |
.how-spine@media (min-width: 768px) | 1517 | |
.how-weave-line@media (prefers-reduced-motion: no-preference) | Marching-ants thread + a one-shot lift-in when the section scrolls into view. .js-draw is added by JS only (and never under reduced motion), so with no JS everything shows in its finished state. | 1533 |
.how-flow.js-draw .how-weave-line@media (prefers-reduced-motion: no-preference) | With JS driving the section, the thread is drawn between the pinned cards as they lift in, then hands over to the marching rhythm. The loop only runs while the flow is on screen — nothing marches out of view. | 1539 |
.how-flow.js-draw .how-weave-trace@media (prefers-reduced-motion: no-preference) | 1540 | |
.how-flow.js-draw.is-drawn .how-weave-trace@media (prefers-reduced-motion: no-preference) | 1546 | |
.how-flow.js-draw.is-drawn .how-weave-line@media (prefers-reduced-motion: no-preference) | 1552 | |
.how-flow.js-draw.is-drawn.is-onscreen .how-weave-line@media (prefers-reduced-motion: no-preference) | 1556 | |
.how-flow.js-draw .how-step@media (prefers-reduced-motion: no-preference) | 1558 | |
.how-flow.js-draw.is-drawn .how-step@media (prefers-reduced-motion: no-preference) | 1559 | |
.how-flow.js-draw.is-drawn .how-step:nth-child(2)@media (prefers-reduced-motion: no-preference) | 1564 | |
.how-flow.js-draw.is-drawn .how-step:nth-child(3)@media (prefers-reduced-motion: no-preference) | 1565 | |
.how-flow.js-draw.is-drawn .how-step:nth-child(4)@media (prefers-reduced-motion: no-preference) | 1566 |
FAQ accordion #
| Selector | Note | Line |
|---|---|---|
.faq-layout | Two-column split: the heading + a "more questions" note held on the left, a single stacked accordion of hairline-divided rows on the right. Heading and question type stay on the standard scale (H2 / H3). | 1573 |
.faq-layout@media (min-width: 900px) | 1575 | |
.faq-aside@media (min-width: 900px) | 1580 | |
.faq-more | 1582 | |
.faq-list | 1584 | |
.faq-group-h | 1585 | |
.faq-group-h:first-child | 1594 | |
.faq-item | Each row is a native <details>: the browser shows and hides the answer on the summary's click, Enter or Space, with no script and no animation. | 1597 |
.faq-item:last-child | 1598 | |
.faq-trigger | 1599 | |
.faq-trigger::-webkit-details-marker | 1613 | |
.faq-q | The question stays a real h4, so the heading outline keeps it, but takes its color from the summary: the base heading rule would pin it to --ink and lose the hover. | 1617 |
.faq-trigger:hover | --link, not Sky: Sky text on the light ground is only 2.4:1. | 1619 |
.faq-trigger .faq-sign | 1620 | |
.faq-item:not([open]) .faq-sign-minus, .faq-item[open] .faq-sign-plus | The sign carries both symbols and [open] picks one: a plus closed, a minus open. A half turn on the swap makes the change read as a morph rather than a cut, and a minus is unchanged by 180deg so it still lands flat. | 1628 |
.faq-item[open] .faq-sign | 1630 | |
.faq-panel | 1632 | |
.faq-panel > * | 1636 |
"Get Started" drawer tab #
Fixed to the right edge, vertical text, opens the contact drawer. z-index sits below #contact-backdrop (90), so the drawer's backdrop covers it while the drawer is open.
| Selector | Note | Line |
|---|---|---|
.drawer-tab | 1641 | |
.drawer-tab:hover | Light blue at rest in both themes; hover deepens to navy in light mode and lifts to white in dark mode. | 1669 |
.drawer-tab:focus-visible | --ring is Navy on light / Sky on dark: ≥3:1 against the page either way. | 1675 |
.drawer-tab.is-tucked, .js .drawer-tab.tab-wait | Tucked away above the Services section and once the in-page intake form (or the closing region) is on screen. .tab-wait holds it tucked from first paint until main.js has measured the page (JS only, so no-JS keeps it). | 1679 |
.drawer-tab@media (max-width: 479px) | Phones: a right-edge tab would sit over body copy inside the 1.5rem gutter, so the CTA becomes a full-width bar pinned to the bottom of the viewport (the brief's "CTA pinned to the bottom on scroll"). | 1690 |
.drawer-tab.is-tucked, .js .drawer-tab.tab-wait@media (max-width: 479px) | 1704 | |
.drawer-tab:focus-visible@media (max-width: 479px) | 1706 | |
.site-footer@media (max-width: 479px) | keep the footer's last lines clear of the bar when it is showing | 1708 |
Contact drawer (right-anchored slide-out) #
Opened with dialog.show(), not showModal(): a showModal() dialog (and its ::backdrop) render in the browser's top layer, which paints above *every* normal stacking context regardless of z-index -- and that hid the very- high-z-index challenge overlay Google's reCAPTCHA used to append to <body> for an image challenge. The form now uses Cloudflare Turnstile, whose challenge stays inside the widget, so that constraint is gone; the plain fixed-position dialog + real sibling backdrop element (below) stay because they work and are tested. modal.js handles the inert-background, focus- trap, and Escape/outside-click behavior that showModal() would give for free.
| Selector | Note | Line |
|---|---|---|
#contact-modal | 1722 | |
#contact-backdrop | 1739 | |
#contact-backdrop[hidden] | 1746 | |
#contact-modal[open] | 1747 | |
#contact-backdrop:not([hidden]) | 1748 | |
#contact-modal[open]@media (prefers-reduced-motion: reduce) | 1755 | |
#contact-backdrop:not([hidden])@media (prefers-reduced-motion: reduce) | 1756 | |
#contact-modal.is-closing[open] | Closing: modal.js adds .is-closing and only closes the dialog once this finishes. The drawer slides back out the edge it came from, quicker than it arrived and accelerating away; the backdrop fades with it and stops catching clicks, since the page behind is already live again. | 1762 |
#contact-backdrop.is-closing:not([hidden]) | 1763 | |
#contact-modal.is-closing[open]@media (prefers-reduced-motion: reduce) | 1773 |
Lightbox variant #
The same dialog, centered instead of edge-anchored. A data-variant="lightbox" open button adds .is-lightbox (see modal.js).
| Selector | Note | Line |
|---|---|---|
#contact-modal.is-lightbox | 1777 | |
#contact-modal.is-lightbox .modal-scroll | 1786 | |
#contact-modal.is-lightbox[open] | 1790 | |
#contact-modal.is-lightbox.is-closing[open] | 1797 | |
#contact-modal.is-lightbox[open]@media (prefers-reduced-motion: reduce) | 1805 | |
#contact-modal.is-lightbox.is-closing[open]@media (prefers-reduced-motion: reduce) | 1806 | |
.modal-scroll | 1809 | |
.modal-close | Pinned to the dialog, not the scroller, so it stays put while the content scrolls beneath it. z-index 1 keeps it above that content (a hovered .btn's transform makes its own stacking context and would otherwise paint over it) but under .modal-confirm (z-index 2), which covers it while asking. | 1814 |
.modal-close:hover | 1830 |
Forms #
| Selector | Note | Line |
|---|---|---|
.field | 1837 | |
.field-label | 1838 | |
.field-hint | 1846 | |
.req | 1847 | |
.input, .textarea, .select | Ink, so Coral keeps one meaning (errors) | 1848 |
.textarea | 1861 | |
.input:focus, .textarea:focus, .select:focus | 1862 | |
.input[aria-invalid="true"], .textarea[aria-invalid="true"], .select[aria-invalid="true"] | 1863 | |
.field-error | Error text stays --ink for AA; the coral border and the leading triangle carry the "error" signal visually. For assistive tech it's aria-invalid on the field plus this element's id joining the field's aria-describedby (contact-form.js), and focus moving to the error summary on a failed submit. | 1870 |
.field-error:not([hidden]) | 1871 | |
.field-error:not([hidden])::before | 1872 | |
.choice | 1873 | |
.choice input | 1880 | |
.choice input[type="checkbox"]:not(:checked) | An empty checkbox is drawn from the text fields' own tokens. The browser's, under color-scheme: dark, is a flat neutral grey that reads as disabled on Navy. Once ticked it is the native control again, tinted by accent-color. | 1884 |
.choice input[type="checkbox"][aria-invalid="true"]:not(:checked) | The same 2px Coral border an empty required field gets. | 1891 |
fieldset | 1892 | |
[data-turnstile-field][hidden] | Cloudflare's Turnstile widget (contact-form.js renders it "flexible": full width, never under 300px, 65px tall -- or "compact", 150x140, where the card is narrower than 300px). Reserve the height so the card doesn't jump a second time when the iframe arrives, and let it scroll rather than push the card wider if it still doesn't fit. The field is authored hidden and appears on the visitor's first edit; .field's display: block sits in this layer and so outranks Preflight's [hidden], hence the explicit rule. | 1901 |
[data-turnstile-field] | 1902 | |
[data-turnstile] | 1903 | |
.hp-field | 1905 | |
.form-note | 1913 | |
.error-summary | 1921 | |
.error-summary a | 1932 | |
.error-summary strong | 1933 |
Site preferences dialog #
Theme, text size, animations, reader mode, analytics. Built by prefs.js and opened with showModal(), so the browser supplies the backdrop, the inert page and Escape. Centered like the contact lightbox, on the page's own tokens.
| Selector | Note | Line |
|---|---|---|
.prefs-dialog | 1939 | |
.prefs-dialog::backdrop | 1951 | |
.prefs-dialog[open] | 1955 | |
.prefs-dialog[open]::backdrop | 1956 | |
.prefs-dialog[open]@media (prefers-reduced-motion: reduce) | 1958 | |
html:has(.prefs-dialog[open]) | 1960 | |
.prefs-body | 1961 | |
.prefs-body@media (min-width: 480px) | 1966 | |
.prefs-title | The heading repeats the gear that opened the dialog. | 1968 |
.prefs-title svg | 1969 | |
.prefs-lede | 1970 | |
.prefs-group | 1971 | |
.prefs-label | 1972 | |
.prefs-hint | 1979 | |
.prefs-seg | Segmented radios: the input stays in the tab order (arrow keys move between options) but is drawn by its label. | 1983 |
.prefs-seg-option | 1993 | |
.prefs-seg-option input | 2007 | |
.prefs-seg-option:hover | 2014 | |
.prefs-seg-option:has(input:checked) | 2015 | |
.prefs-seg-option:has(input:focus-visible) | 2016 | |
.prefs-seg--text | Text size: 2 × 2 on phones, one row from 480px. | 2021 |
.prefs-seg--text@media (min-width: 480px) | 2022 | |
.prefs-seg--text [data-value="lg"] | Each option previews its own step (the root scale on :root[data-text]). | 2024 |
.prefs-seg--text [data-value="xl"] | 2025 | |
.prefs-seg--text [data-value="xxl"] | 2026 | |
.prefs-rows | 2028 | |
.prefs-row | 2029 | |
.prefs-row | When the label and switch can't share a line (a narrow phone at the Largest text size), the switch drops below the text instead of squeezing it. | 2039 |
.prefs-row-text | 2040 | |
.prefs-row .prefs-label | 2041 | |
.prefs-switch | On/off switch: a native checkbox (role="switch") drawn as a pill, in the same track + knob shape as the footer's theme switch. | 2045 |
.prefs-switch::before | 2058 | |
.prefs-switch:checked | 2070 | |
.prefs-switch:checked::before | 2071 | |
.prefs-switch:disabled | 2072 | |
.prefs-switch:focus-visible | 2073 | |
.prefs-actions | 2075 | |
.prefs-actions .btn | 2082 | |
.prefs-actions .btn@media (min-width: 480px) | 2083 |
Reader mode (Site preferences → data-reader="on" on <html>) #
One plain column for reading: every layout grid in <main> collapses to a single track in a narrower measure, decoration (mazes, the How-it-works thread and pins, the lit beacon, the hero glow) is removed, tinted section grounds and card shadows go flat, and the leading opens up. Content photos are hidden too, same as the aria-hidden treatment they get for Safari's built-in Reader View — this mode should read like a plain HTML page, text only. The navy Schedule band keeps its ground, since its type is set for it. The header, footer and pinned CTA are untouched.
| Selector | Note | Line |
|---|---|---|
:root[data-reader="on"] main :is(.framed, .vignette-figure, .hero-byline-frame) | 2094 | |
:root[data-reader="on"] main | 2097 | |
:root[data-reader="on"] main :is(.canvas, .canvas-wide) | 2098 | |
:root[data-reader="on"] main .sg-shell | The style guide's shell: one column in the same measure, contents on top. | 2100 |
:root[data-reader="on"] main .sg-toc | 2101 | |
:root[data-reader="on"] main :is(.grid, .grid-9, .service-rows, .who-list, .price-choices, .how-steps, .faq-layout, .fineprint-body, .ba-grid) | 2102 | |
:root[data-reader="on"] main .ba-panel--before .ba-desc | Same phone treatment as the ba-grid's own narrow breakpoint: Before shows titles only, so the stacked panels reach the answer sooner, and the two panels drop the side-by-side subgrid row alignment (a >=640px-only rule that would otherwise still apply once the columns above collapse to one). | 2110 |
:root[data-reader="on"] main :is(.ba-panel, .ba-list) | 2111 | |
:root[data-reader="on"] main :is(.grid, .grid-9, .how-steps, .who-list) > * | 2115 | |
:root[data-reader="on"] main :is(.maze-hero, .lost-maze, .maze-cross, .how-spine, .how-pin, .beacon-lit) | 2120 | |
:root[data-reader="on"] main :is(.hero-atmos, .how-flow, .trust-stack)::before | 2123 | |
:root[data-reader="on"] main .trust-stack :is(.trust-stack-title, .trust-item strong, .trust-item p) | 2124 | |
:root[data-reader="on"] main .how-step | 2127 | |
:root[data-reader="on"] main .service-row | 2128 | |
:root[data-reader="on"] main .path-card::before | The path cards' photo wash is decoration. | 2130 |
:root[data-reader="on"] #top > .canvas.section | The hero keeps extra bottom room for its glow; use the normal rhythm. | 2132 |
:root[data-reader="on"] main .vignette-figure | The "What" photo floats beside the copy, and the value list (its own flex box) squeezes into the leftover strip. Stack it above the text instead. | 2135 |
:root[data-reader="on"] main .faq-more | 2141 | |
:root[data-reader="on"] main :is(.about-rail, .faq-aside) | Sticky side rails would slide over the stacked copy below them. | 2143 |
:root[data-reader="on"] main :is(.faq-layout, .fineprint-body) | Their desktop column gaps become row gaps once stacked. | 2145 |
:root[data-reader="on"] main .section:not(.on-dark) | 2146 | |
:root[data-reader="on"] main :is(.card, .tile, .path-card, .how-card, .how-card-inner, .price-choice, .framed) | 2150 | |
:root[data-reader="on"] main .faq-aside | 2155 | |
:root[data-reader="on"] main .ba-list::before | 2156 | |
:root[data-reader="on"] main :is(p, li) | 2157 |
Hero actions #
One primary button + the phone number, on the first screen
| Selector | Note | Line |
|---|---|---|
.hero-actions | 2415 | |
.hero-tagline | Tagline: a quieter second lede under the main one. | 2422 |
.hero-cta-note | CTA note: sits under the button, not beside it, inside the wrapping flex row. | 2424 |
.hero-creds | Credentials beside the CTA: plain text, not a link. | 2431 |
.trust-stack | Trust stack: five reasons in a row under the hero; two columns on tablets, one list on phones. Each item gets a brand accent. No rule above it: the hero maze runs underneath and a full-width line cut across it. The maze is knocked back instead of ruled off — a soft wash of the page ground fades it under the strip, and every glyph carries a small halo of the same ground, so walls stop at the letterforms rather than crossing them. Both are page-ground colored, so they follow the theme. | 2445 |
.trust-stack::before | Paints over .maze-hero: both sit at z-index -1 in the hero's stacking context (.hero-atmos isolates), and this comes later in tree order. | 2452 |
.trust-stack :is(.trust-stack-title, .trust-item strong, .trust-item p) | 2461 | |
.trust-stack-title | 2464 | |
.trust-list | 2472 | |
.trust-list@media (min-width: 640px) | 2479 | |
.trust-list@media (min-width: 1024px) | 2480 | |
.trust-item | 2481 | |
.trust-item strong | 2490 | |
.trust-item--years | 2497 | |
.trust-item--sky | 2498 | |
.trust-item--seagrass | 2499 | |
.trust-item--lantern | 2500 | |
.trust-item--navy | 2501 | |
.trust-ico | 2502 | |
.hero-byline | Byline: Lindsey's face on the first screen, linking to her full bio. At every width, so the text-only phone hero still has a person in it. | 2511 |
.hero-byline-frame | Circular mask framed on her face: the <picture> is the circle and the portrait inside is scaled up and shifted so hair-to-chin fills it. | 2521 |
.hero-byline-photo | 2532 | |
.hero-byline-text | 2540 | |
.hero-byline-name | 2541 | |
.hero-byline:hover .hero-byline-name | 2548 | |
.hero-byline-meta | 2549 | |
.path-note | Plain-sentence reassurance inside the crisis path card. | 2552 |
Services as rows, not a tile grid #
Icon in its own column, two columns of rows from md up. Each row takes a palette accent (Seagrass, Lantern, Sky, repeating): a tinted icon tile, a soft glow behind the icon and matching bullet markers. --svc tints marks and washes; --svc-ink is the contrast-safe step.
| Selector | Note | Line |
|---|---|---|
.service-rows | 2563 | |
.service-rows@media (min-width: 768px) | 2564 | |
.service-rows > :nth-child(3n+1) | 2565 | |
.service-rows > :nth-child(3n+2) | 2566 | |
.service-rows > :nth-child(3n) | 2567 | |
.service-row | 2568 | |
.service-row > .service-icon | 2577 | |
.service-row > .service-icon .icon-lg | 2585 | |
.service-row > :not(.service-icon) | 2586 | |
.service-row p | 2587 |
Who we help #
A photo card per audience on a Sky-tinted band. One column on phones, two from 640px, four in a row from 1024px (below). Photos: Unsplash, credited on about-this-site.html; responsive derivatives from scripts/fetch-photos.sh.
About #
The page's showcase. Large portrait, editorial rail.
What is a healthcare advocate #
The four value items as a quiet list
| Selector | Note | Line |
|---|---|---|
.value-list | 2620 | |
.value-list > li | 2621 | |
.value-list > li > .icon | 2622 | |
.value-list > li:nth-child(1) > .icon | 2623 | |
.value-list > li:nth-child(2) > .icon | 2624 | |
.value-list > li:nth-child(3) > .icon | 2625 | |
.value-list > li:nth-child(4) > .icon | 2626 | |
.value-list p | 2627 | |
.value-list > li > .icon@media (min-width: 480px) | 2629 | |
.ba-grid | Before/after: two framed panels, a header band over a white list, stacked on phones and side by side from 640px. Side by side, the panels share row tracks (subgrid) so each "before" row lines up with its "after" row. | 2635 |
.ba-panel | 2640 | |
.ba-panel--after | 2649 | |
.ba-head | 2650 | |
.ba-head h3 | 2659 | |
.ba-head p | 2660 | |
.ba-list | 2661 | |
.ba-panel--after .ba-list | The one-colour Beacon as a faint green watermark filling the After list, painted under the rows (isolation keeps z-index -1 above the white fill). | 2671 |
.ba-panel--after .ba-list::before | 2672 | |
.ba-list > li | Narrow: the marker leads the title and the description takes the full width. From 640px the marker gets its own column beside both. | 2685 |
.ba-list > li:first-child | 2693 | |
.ba-desc | 2694 | |
.ba-mark | Red cross / green tick discs; the palette hues clear 3:1 behind white. | 2696 |
.ba-mark--yes | 2705 | |
.ba-mark svg | 2706 | |
.ba-title | 2715 | |
.ba-desc | 2716 | |
.ba-panel--before .ba-desc@media screen and (max-width: 639.98px) | Phones (and Larger / Largest text): Before shows its titles only, so the stacked panels reach the answer sooner. | 2720 |
.ba-grid@media (min-width: 640px) | 2723 | |
.ba-panel@media (min-width: 640px) | 2728 | |
.ba-panel--before@media (min-width: 640px) | Before stays on top so After can slide out from beneath it. | 2730 |
.ba-list@media (min-width: 640px) | 2731 | |
.ba-head@media (min-width: 640px) | 2732 | |
.ba-list@media (min-width: 640px) | 2733 | |
.ba-list > li@media (min-width: 640px) | 2734 | |
.ba-list > li > .ba-desc@media (min-width: 640px) | 2735 | |
.ba-mark@media (min-width: 640px) | 2736 | |
.ba-mark@media (min-width: 640px) | 2737 | |
.field-count | Non-breaking spaces (contact-form.js) keep "n of 600" together; the count can still wrap before "characters" in a very narrow column. | 2750 |
.field-count.is-near | 2751 | |
.field-count.is-over | 2752 | |
.modal-quickbook | Contact dialog: book or call first, message form as the alternative | 2759 |
.modal-quickbook[hidden] | 2760 | |
.modal-quickbook-lede | 2761 | |
.modal-quickbook-actions | 2762 | |
.modal-quickbook-actions .btn | Labels stay on one line (so the Calendly mark sits level with the text); when both don't fit side by side, the buttons stack instead. | 2765 |
.modal-divider | 2766 | |
.modal-divider::before, .modal-divider::after | 2774 | |
.modal-confirm | Discard check: dims the dialog's own content (made inert by modal.js) and centers a small card over it while the visitor decides. | 2779 |
.modal-confirm[hidden] | 2788 | |
.modal-confirm-card | 2789 | |
.modal-confirm-actions | 2799 | |
.modal-confirm-actions .btn | 2800 |
"The light comes on" #
The one delight moment — When a message reaches Lindsey, the Advocate Beacon mark appears in the success panel and its light comes up behind it: a soft Beacon Sky glow blooms, one slow beam sweeps round, and it settles to a steady glow. The mark itself is never altered (brand geometry); the light is drawn behind it. Runs when the panel is unhidden (display:none -> shown restarts the keyframes), once per send, never loops. A consultation booked in the inline Calendly embed is the same moment and gets the same light ([data-calendly-booked], with the Reversed mark where it sits on the navy booking band). Reduced motion: the glow fades in, no sweep, no scale.
| Selector | Note | Line |
|---|---|---|
.beacon-lit | 2812 | |
.beacon-lit-mark | 2821 | |
.beacon-lit::before | steady glow | 2828 |
.beacon-lit::after | the sweeping beam: a soft wedge of light, feathered by a radial mask | 2841 |
:is([data-contact-success], [data-calendly-booked]):not([hidden]) .beacon-lit::before@media (prefers-reduced-motion: no-preference) | 2858 | |
:is([data-contact-success], [data-calendly-booked]):not([hidden]) .beacon-lit::after@media (prefers-reduced-motion: no-preference) | 2861 | |
:is([data-contact-success], [data-calendly-booked]):not([hidden]) .beacon-lit-mark@media (prefers-reduced-motion: no-preference) | 2864 | |
:is([data-contact-success], [data-calendly-booked]):not([hidden]) .beacon-lit::before@media (prefers-reduced-motion: reduce) | 2869 |
Hero maze #
A generated maze (#maze-hero) behind the whole hero, anchored to its bottom-right corner. It fades in toward the right, so the copy column stays clean and the maze runs around and under the illustration. Phones get the same right-hand fade. Walls only, static; non-scaling strokes hold 1.75px.
Fine print, read #
A sample statement beside three plain notes. The statement is a paper document (Surface, hairline, Lift); flagged lines take a Sand highlighter wash, and the reviewer's marks are Sky Ink pen strokes (an underline, a ring) drawn in the sprite's round caps. Numbered pins tie each mark to its note. Everything is visible without JS or motion.
| Selector | Note | Line |
|---|---|---|
.fineprint-head p | 2920 | |
.fineprint-body | 2921 | |
.fineprint-body@media (min-width: 820px) | 2929 | |
.fineprint-notes p.fineprint-note | 2931 | |
.fineprint-doc | 2938 | |
.fineprint-doc@media (min-width: 480px) | 2947 | |
.fineprint-doc-top | 2949 | |
.fineprint-doc-title | 2955 | |
.fineprint-stamp | 2962 | |
.fineprint-doc-meta | 2975 | |
.fineprint-table | 2985 | |
.fineprint-table thead th | 2991 | |
.fineprint-table td | 3001 | |
.fineprint-table td:first-child | 3007 | |
.fineprint-table td:first-child@media (min-width: 480px) | 3013 | |
.fineprint-table .fp-amt | 3014 | |
.fineprint-table thead .fp-amt | 3021 | |
.fineprint-table tfoot th, .fineprint-table tfoot td | 3022 | |
.fineprint-table tfoot th | 3028 | |
.fineprint-doc@container fineprint (max-width: 320px) | The narrowest phones: with 320px or less to work in (a 320–360px screen, or wider ones at the larger text sizes), tighter tracking, padding and figures keep all three columns inside the document, and at a larger text size it borrows a little of the page gutter. A container query, so it follows the space actually left at every text size (text-layout.js only rewrites @media). | 3040 |
.fineprint-table thead th@container fineprint (max-width: 320px) | 3041 | |
.fineprint-table :is(thead, tbody, tfoot) :is(th, td)@container fineprint (max-width: 320px) | 3042 | |
.fineprint-table td:first-child@container fineprint (max-width: 320px) | 3043 | |
.fineprint-table .fp-amt@container fineprint (max-width: 320px) | 3044 | |
:root[data-text] .fineprint-doc@container fineprint (max-width: 320px) | 3045 | |
.fineprint-table tr.is-flagged td | Sand highlighter behind a flagged line. Each cell paints its own slice of one row-wide gradient (main.js sets --fp-row-w and each cell's --fp-x), so a single --fp-sweep on the row moves one edge across all three cells in one stroke. Without the measurements each cell just carries the full wash. | 3052 |
.fineprint [data-note="1"] | 3059 | |
.fineprint [data-note="2"] | 3060 | |
.fineprint [data-note="3"] | 3061 | |
.fp-pin | 3063 | |
.fineprint-table .fp-pin | 3077 | |
.fineprint-table .fp-pin@media (min-width: 480px) | 3078 | |
.fp-mark | 3080 | |
.fp-ink | 3081 | |
.fp-ink path | 3082 | |
.fp-ink--under | 3090 | |
.fp-ink--ring | 3091 | |
.fineprint-notes | 3093 | |
.fineprint-notes > li | 3094 | |
.fineprint-notes .fp-pin | 3099 | |
.fineprint-notes h3 | 3100 | |
.fineprint-notes p | 3101 | |
.fineprint-notes@media (min-width: 820px) | 3102 | |
.fineprint[data-active="1"] [data-note="1"] .fp-pin, .fineprint[data-active="2"] [data-note="2"] .fp-pin, .fineprint[data-active="3"] [data-note="3"] .fp-pin | Pointing at a note (or its line) lights the pair. | 3105 |
.fineprint[data-active="1"] li[data-note="1"] h3, .fineprint[data-active="2"] li[data-note="2"] h3, .fineprint[data-active="3"] li[data-note="3"] h3 | 3108 | |
.js [data-reveal="marks"].reveal-pending@media (prefers-reduced-motion: no-preference) | 3113 | |
.js [data-reveal="marks"].reveal-pending .fp-ink path@media (prefers-reduced-motion: no-preference) | 3114 | |
.js [data-reveal="marks"].reveal-pending tr.is-flagged@media (prefers-reduced-motion: no-preference) | 3115 | |
.js [data-reveal="marks"].reveal-pending .fineprint-table .fp-pin@media (prefers-reduced-motion: no-preference) | 3116 | |
.js [data-reveal="marks"].reveal-in@media (prefers-reduced-motion: no-preference) | 3118 | |
.js [data-reveal="marks"].reveal-in tr.is-flagged@media (prefers-reduced-motion: no-preference) | One stroke per row: a quick start, an even pull across, a soft lift at the end. | 3122 |
.js [data-reveal="marks"].reveal-in .fineprint-table .fp-pin@media (prefers-reduced-motion: no-preference) | 3126 | |
.js [data-reveal="marks"].reveal-in .fp-ink path@media (prefers-reduced-motion: no-preference) | 3130 |
Who we help #
From 1024px the four audiences stand in a row.
| Selector | Note | Line |
|---|---|---|
.who-list@media (min-width: 1024px) | 3146 |
Credentials #
As the rows arrive, a Sky rule runs along each one, in reading order, and fades back to the hairline.
Booking band #
The maze, solved, full width along the bottom of the navy ground. On the index it has a strip of its own under the buttons (rule below), so no copy crosses a wall; the style guide's masthead still uses it as a background, anchored to the band's bottom edge and fading out upward, so it takes no space of its own. It scales with the page, so both walls always show; a smaller maze stands in on phones so the cells stay readable. A Beacon Sky ring waits outside the entrance in the left wall; the route crosses to the exit in the right wall and ends at a dot. main.js adds .route-pending, then .is-routed once the footer comes into view: ring, route, dot. No JS: all shown. Reduced motion: shown without the draw (global rule).
| Selector | Note | Line |
|---|---|---|
.schedule-band | 3184 | |
.maze-cross | 3185 | |
.maze-cross--lg | 3203 | |
.maze-cross--sm | 3204 | |
.maze-cross--lg@media (max-width: 640px) | 3206 | |
.maze-cross--sm@media (max-width: 640px) | 3207 | |
#schedule .maze-cross | On the index the maze is the last thing in the band, in the flow: it takes the room its aspect ratio asks for, so the copy and buttons end above it instead of running across the walls. The negative bottom margin lands it flush with the band's edge, in the padding .section leaves under the buttons (the clamp repeats .section's), and goes with the maze where the maze is hidden (reader mode, print), leaving that padding as the band's ordinary bottom. | 3215 |
.maze-cross use | 3219 | |
.maze-route | 3220 | |
.maze-cross--sm .maze-route | 3221 | |
.maze-route-start | 3222 | |
.maze-route-end | 3223 | |
.maze-cross--lg .maze-route@media (max-width: 1023px) | Between phones and desktops the wide maze scales down to about half. | 3226 |
.maze-cross--lg .maze-route-start@media (max-width: 1023px) | 3227 | |
.maze-cross.route-pending .maze-route | 3229 | |
.maze-cross.route-pending :is(.maze-route-start, .maze-route-end) | 3230 | |
.maze-cross.route-pending.is-routed .maze-route | 3231 | |
.maze-cross.route-pending.is-routed .maze-route-start | 3235 | |
.maze-cross.route-pending.is-routed .maze-route-end | 3239 |
Calendly, inline #
The scheduler framed in the page by calendly.js, in the booking band and in a panel of the contact dialog. Until the frame is in, a mount holds only its authored fallback, a plain link out; .is-live swaps that for the frame plus a one-line way out for a visitor whose browser refuses it. Calendly's page is transparent around its own white card, so the mount adds no card of its own. Its colors need a paid plan: the card is white in both themes. The mount's width picks Calendly's layout, and with it the size (its breakpoints are px widths of the frame, measured 2026-09-18 with the event details hidden): - under 650px, the phone layout: the page fills the frame, 602px tall, and a picked day opens its times as a second view; - 650 to 999px, a stacked card: 711px, growing past 1300px when a day is picked. Avoided: it is the largest of the three; - 1000px and up, the desktop layout: a 680px card in a transparent page, 660px tall and fixed, because the times scroll inside the card. So the mount is capped at 640px, and the booking band widens it to 1040px where the viewport has room (clear of the 1000px edge; the band clips any overflow).
| Selector | Note | Line |
|---|---|---|
.calendly-embed | 3262 | |
#schedule .calendly-embed@media (min-width: 1072px) | 1040px + a 1rem gutter each side. The Larger / Largest text sizes keep the phone layout (text-layout.js rewrites this query), and so the 640px mount. | 3266 |
#schedule .calendly-embed.is-live@media (min-width: 1072px) | The desktop layout brings a 66px top margin of its own. | 3272 |
#schedule .calendly-frame@media (min-width: 1072px) | 3273 | |
.calendly-frame | The height is only a start, matched to the layout's first report so nothing jumps: calendly.js sets the real one from the frame's own reports, step by step. color-scheme matches Calendly's page; without it the dark theme makes the browser paint an opaque backdrop behind the frame. | 3279 |
.calendly-loading | Sits where Calendly's card will land and is covered by it; calendly.js removes it when the frame has loaded. | 3291 |
.calendly-alt | 3297 | |
.on-dark .calendly-alt | 3298 | |
.calendly-embed.is-live .calendly-alt | 3299 | |
.calendly-embed.is-live .calendly-fallback | 3300 | |
.calendly-embed.is-booked .calendly-fallback | Booked: calendly.js has taken the frame out and unhidden the authored thanks (.calendly-booked), which is all the mount shows. It is the same moment as a sent message, so the beacon's light comes on the same way (rules below, "The light comes on"). In the dialog, modal.js marks the booking block too, and everything in it but the mount steps aside. | 3306 |
.calendly-booked[hidden] | 3307 | |
.calendly-booked:focus | 3308 | |
.modal-quickbook.is-booked > :not(.calendly-embed) | 3309 | |
:root[data-booked] :is([data-calendly]:not(.is-booked), [data-calendly-toggle], [data-calendly-offer])@media screen | Once booked, not asked again: calendly.js remembers a booking (localStorage) and sets data-booked on <html>, and this takes the invitations to book away: every mount but the one showing its thanks, every toggle, and whatever is marked data-calendly-offer (the success panel's "Book time with us"). Copy marked data-calendly-after is authored hidden and stands in where a sentence needs rewording. The footer's Calendly icon is left alone, and following it brings all of this back. A rule and not script, so a dialog modal.js imports later needs no setup. Screen only: paper keeps the band's fallback link. | 3319 |
:root[data-booked] [data-calendly-after]@media screen | 3320 | |
.calendly-embed.calendly-embed--panel | In the dialog: a gap under the buttons. The drawer is 30rem wide, so this is always Calendly's phone layout. | 3324 |
.calendly-embed--panel .calendly-loading | 3325 |
"Get Started" tab #
The first time it comes out it leans a little further than its resting place and settles back, once per page view.
404 #
A small maze with its route drawn through to the way out
| Selector | Note | Line |
|---|---|---|
.lost-maze | 3351 | |
.lost-maze path | 3352 | |
.lost-maze-walls | 3353 | |
.lost-maze-route | 3354 | |
.lost-maze-end | 3355 | |
.lost-maze-route@media (prefers-reduced-motion: no-preference) | 3357 | |
.lost-maze-end@media (prefers-reduced-motion: no-preference) | 3358 | |
.maze-hero, .maze-cross@media print | 3366 | |
.trust-stack::before@media print | 3367 | |
.trust-stack :is(.trust-stack-title, .trust-item strong, .trust-item p)@media print | 3368 |
Legal pages #
| Selector | Note | Line |
|---|---|---|
.legal-prose | 3372 | |
.legal-prose h2 | 3373 | |
.legal-prose h3 | 3374 | |
.legal-prose p, .legal-prose li | 3375 | |
.legal-prose > p + p | 3376 | |
.legal-prose ul, .legal-prose ol | 3377 | |
.legal-prose ul | 3378 | |
.legal-prose ol | 3379 | |
.legal-prose li | 3380 | |
.legal-prose li::marker | 3381 | |
.legal-prose a | 3382 | |
.legal-prose :where(h1, h2, h3)[id] | 3383 | |
.legal-prose address | 3384 | |
.legal-toc | 3391 | |
.legal-toc ol | 3392 | |
.legal-prose code | 3393 | |
.legal-prose .table-wrap | 3401 | |
.legal-prose table | 3407 | |
.legal-prose th, .legal-prose td | 3408 | |
.legal-prose thead th | 3409 | |
.legal-prose tbody tr:first-child td, .legal-prose tbody tr:first-child th | 3418 | |
.legal-prose td:last-child, .legal-prose th:last-child | 3419 | |
.legal-prose .sig-grid | 3420 | |
.legal-prose .sig-grid@media (min-width: 480px) | 3425 | |
.legal-prose .sig-card | 3426 | |
.legal-prose .sig-card p | 3432 | |
.legal-prose .sig-card .role | 3433 | |
.legal-prose .sig-card .role em | 3442 | |
.legal-prose .fill | 3443 | |
.legal-prose .checklist | 3444 | |
.legal-prose .checklist li | 3445 | |
.legal-prose .checklist li::before | 3446 | |
.todo-flag | 3447 | |
.todo-flag strong | 3456 | |
.todo-flag ul, .todo-flag ol | 3457 | |
.legal-toc .footer-col-title | 3458 |
Style guide #
The bar. The internal pages' own bar: the mark, the page's name, links between the guide and the two code references, and the theme and Site preferences buttons. Deliberately not the site header, so nobody takes these pages for public ones. Navy in both themes, like the booking band, so it borrows the dark palette (.sg-theme-dark on the element) and the header's round buttons work on it unchanged. Not sticky: a reference page is scrolled, not navigated from the top.
| Selector | Note | Line |
|---|---|---|
.sg-bar | 3467 | |
.sg-bar@media (min-width: 480px) | The bar's content keeps the shell's edges: 2rem of padding, or whatever centers it on the same 1116px once the window is wider than the shell. | 3479 |
.sg-bar-home | 3480 | |
.sg-bar-home img | 3481 | |
.sg-bar-title | 3482 | |
.sg-bar-nav | 3488 | |
.sg-bar .sg-bar-nav a | Label-voice links. The current page holds the nav's 2px Sky underline. Two classes, to outrank .on-dark's Sky for classless links. | 3491 |
.sg-bar .sg-bar-nav a:hover, .sg-bar .sg-bar-nav a[aria-current] | 3499 | |
.sg-bar-nav a[aria-current]::after | 3500 | |
.sg-bar-tools | 3508 | |
.sg-bar@media (max-width: 479px) | On a phone the links and the two buttons share the second row. | 3511 |
.sg-bar-nav@media (max-width: 479px) | 3512 | |
.sg-bar-btn | The header sets its theme and gear buttons at 2.25rem; here there is room for the full 44px. A class of its own rather than a descendant selector: Tailwind keeps a rule when any class in it is used, and .header-icon-btn is, on every page. | 3518 |
:root[data-text] .sg-bar@media screen | At the larger text sizes the bar holds a fixed-geometry mark and round buttons, so it is zoomed back like the site header, and its buttons up again. | 3526 |
:root[data-text] .sg-bar-btn@media screen | 3527 |
Style guide #
The masthead. The guide opens on a real component: the booking band's navy ground with its maze, solved, along the bottom edge (.schedule-band and .maze-cross, unchanged), and the title block above it. main.js isn't loaded here, so the route is simply shown drawn.
Style guide #
Shell and contents. A sticky contents list beside one reading column. The list is numbered because the numbers are how sections get referred to ("see 9, Buttons"); the headings carry no numeral. Below 1024px, and at the Larger and Largest text sizes, it is one column and the contents becomes a two-column block at the top.
| Selector | Note | Line |
|---|---|---|
.sg-shell | 3557 | |
.sg-shell@media (min-width: 480px) | 3566 | |
.sg-shell@media (min-width: 1024px) | 3568 | |
.sg-toc-title | 3570 | |
.sg-toc ol | 3579 | |
.sg-toc li | 3580 | |
.sg-toc li:last-child | 3581 | |
.sg-toc a | 3582 | |
.sg-toc a::before | Out of flow, so the current link's underline stops at its words. | 3592 |
.sg-toc a:hover | 3602 | |
.sg-toc a[aria-current] | The section being read, set by style-guide.js: full-strength ink and the 2px Sky underline the site nav gives its current link. | 3605 |
.sg-toc ol@media (min-width: 560px) | 3612 | |
.sg-toc@media (min-width: 1024px) | 3614 | |
.sg-toc ol@media (min-width: 1024px) | 3622 | |
:root:has(.sg-shell) | The bar isn't sticky, so an in-page jump needs none of the site header's scroll padding. | 3626 |
Style guide #
Sections and prose. One hairline and a generous gap above each section, more space above a heading than below it. The prose rules carry no specificity and skip any element with a class, so a real component shown as a specimen is never restyled by the page around it.
| Selector | Note | Line |
|---|---|---|
.sg-main | 3632 | |
.sg-main > section | 3633 | |
.sg-main > section > h2::after | Every section heading carries the site's 8rem Sky rule. | 3639 |
:where(.sg-main) h3:not([class]) | 3648 | |
:where(.sg-main) h4:not([class]) | 3649 | |
:where(.sg-main) p:not([class]) | 3650 | |
.sg-lede | A section's one opening sentence: .lede's size and measure, in full-strength ink. | 3652 |
:where(.sg-main) :is(ul, ol):not([class]) | 3653 | |
:where(.sg-main) :is(ul, ol):not([class]) > li + li | 3660 | |
.sg-main code | Code is code: the mono face, on a quiet chip. A chip moves to the next line whole, and only breaks inside when it is longer than the line. | 3663 |
.sg-main pre | 3675 | |
.sg-main pre code | 3687 | |
.sg-note | 3688 | |
.sg-foot | 3689 |
Style guide #
Named rules. A short list that closes a section: each rule's name in the heading face over its sentence. Names are sentence case, not tracked capitals, because several run to a full clause.
Style guide #
Ledgers. Reference tables are ledgers, not grids: label-voice column heads over hairline rows, no cell borders, no zebra. .sg-scroll wraps any ledger that may be wider than a phone, so the page itself never scrolls sideways.
| Selector | Note | Line |
|---|---|---|
.sg-scroll | 3716 | |
.sg-table | 3717 | |
.sg-scroll > .sg-table | 3718 | |
.sg-table th, .sg-table td | 3719 | |
.sg-table :is(th, td):last-child | 3725 | |
.sg-table td | 3726 | |
.sg-table tbody th | 3727 | |
.sg-table thead th | 3728 | |
.sg-table tbody tr:last-child > * | 3741 | |
.sg-table tbody th > code | A token or selector in a ledger's narrow first column never breaks at its hyphens. | 3743 |
.sg-table tr.sg-group th | A group heading inside a long ledger (the token table's families). | 3745 |
.sg-table--tokens | 3754 | |
.sg-table--tokens td:last-child | 3755 | |
.sg-table--tokens td:has(> .sg-chip) | A value cell keeps its chip and its hex on one line. | 3757 |
.sg-chip | A small square of a color beside its name or value. | 3759 |
.sg-tag | Marks a palette color that appears in the mark itself. | 3769 |
.sg-ratio | A measured contrast ratio, with the level it clears printed after it in words, never by color. | 3784 |
.sg-ratio > span | 3785 | |
.sg-ratio[data-grade^="Below"] > span | scripts/build-docs.js writes the grade onto the cell as data-grade. | 3794 |
.sg-pair | A contrast sample: real ink on a real ground. Each one sits in a .sg-theme-light or .sg-theme-dark scope and paints with var(), inline because every row is a different pair, so a sample can't drift from its tokens. | 3798 |
.sg-pair b | 3808 | |
.sg-pair--edge | 3809 | |
.sg-ringbox | A drawn sample of the 3px focus ring, for the contrast ledger. | 3811 |
Style guide #
Palette strips. Each color family is a contiguous strip of steps over a ledger that names every one. The name and value sit under the color in ordinary ink, so no caption depends on the color above it. The grounds are the brand's fixed hex values, theme-independent.
| Selector | Note | Line |
|---|---|---|
.sg-tints | 3825 | |
.sg-tint | 3826 | |
.sg-tints@media (min-width: 640px) | 3827 | |
.sg-tint@media (min-width: 640px) | 3827 | |
.sg-tint > i | 3828 | |
.sg-tint > i@media (min-width: 640px) | 3829 | |
.sg-tint:first-child > i | 3830 | |
.sg-tint:last-child > i | 3831 | |
.sg-tint:only-child > i | 3832 | |
.sg-tint--edge > i | A step too close to the page ground to show its own edge. | 3834 |
.sg-tint > b | 3835 | |
.sg-main .sg-tint > code | 3845 | |
.sg-chip-navy | The palette tokens never change with the theme, so these point at them rather than repeating the hex. Deep-tone·alt has no token: its value is the README's. | 3849 |
.sg-chip-sky | 3850 | |
.sg-chip-cyan | 3851 | |
.sg-chip-coral | 3852 | |
.sg-chip-midnight | 3853 | |
.sg-chip-paper | 3854 | |
.sg-chip-seagrass | 3855 | |
.sg-chip-lantern | 3856 | |
.sg-chip-sand | 3857 | |
.sg-chip-deep-tone | 3858 | |
.sg-ground-paper | 3859 | |
.sg-ground-midnight | 3860 | |
.sg-ground-navy | 3861 | |
.sg-favicon-dark | 3862 |
Style guide #
Specimen plates. A specimen sits on the ground it ships on. The base plate is Surface with a hairline; the navy plate is the booking band's ground and takes .on-dark beside it for the Sky focus ring. .sg-theme-light and .sg-theme-dark (defined with the tokens) re-point a plate at one palette, whatever theme the page is in.
| Selector | Note | Line |
|---|---|---|
.sg-plate | 3869 | |
.sg-plate--navy | 3877 | |
.sg-plate--navy :is(h3, h4) | 3878 | |
.sg-plate-cap | 3879 | |
.sg-row | 3880 | |
.sg-ring-demo | A forced focus ring, for showing the ring without asking the reader to tab. | 3882 |
.sg-plate--navy .sg-ring-demo | 3883 | |
.sg-inert | A specimen that must not act: the button keeps its look and loses its pointer. | 3885 |
.sg-tile | Mark and lockup tiles are fixed to their documented ground per assets/logomarks/README.txt, so they stay correct in either theme. | 3889 |
.sg-tile img | 3900 | |
.sg-tile p | 3901 | |
.sg-tile .sg-tile-hex | 3909 | |
.sg-tiles | 3910 | |
.sg-tiles--5@media (min-width: 768px) | 3911 | |
.sg-tiles--2@media (max-width: 479px) | 3912 |
Style guide #
Type specimens. The faces at poster size, then each role set live from its own token beside its name and numbers.
| Selector | Note | Line |
|---|---|---|
.sg-faces | 3916 | |
.sg-faces@media (min-width: 640px) | 3917 | |
.sg-face | 3918 | |
.sg-face-big | 3919 | |
.sg-face-name | 3920 | |
.sg-face-note | 3921 | |
.sg-face-alpha | 3922 | |
.sg-face--logo :is(.sg-face-big, .sg-face-alpha) | 3923 | |
.sg-face--label :is(.sg-face-big, .sg-face-alpha) | 3924 | |
.sg-face--mono :is(.sg-face-big, .sg-face-alpha) | 3925 | |
.sg-specimens | 3927 | |
.sg-specimen | 3928 | |
.sg-specimen:last-child | 3935 | |
.sg-specimen@media (min-width: 768px) | 3937 | |
.sg-specimen-meta | 3939 | |
.sg-specimen-meta > b | 3940 | |
.sg-sample | 3950 | |
.sg-spec-h1 | 3951 | |
.sg-spec-h2 | 3952 | |
.sg-spec-h3 | 3953 | |
.sg-spec-h4 | 3954 | |
.sg-spec-row | 3955 | |
.sg-spec-caption | 3956 | |
.sg-spec-label-sm | 3957 | |
.sg-spec-overline | 3958 |
Style guide #
Depth and shape specimens. One card per shadow or radius, drawn with the real value.
Grid demo #
The real .grid-9 tracks at the real gutter, so the strip is the thing it documents rather than a picture of it. It sits in a .table-wrap with a min-width, which is how the guide handles anything too wide to fold.
| Selector | Note | Line |
|---|---|---|
.sg-grid-demo | 3988 | |
.sg-grid-demo + .sg-grid-demo | 3993 | |
.sg-grid-demo > * | 3994 | |
.sg-grid-demo > .sg-grid-fill | The two filled spans borrow token pairs that are already contrast-checked in both themes. The *-ink accents point back at the raw palette in dark, so Paper on them would land near 2.3:1 — these pairs put dark ink on the bright ground instead, the way the primary button and the Before/After panel do. | 4010 |
.sg-grid-demo > .sg-grid-fill--alt | 4014 | |
.sg-grid-demo > * | The demo documents the >=1024px behavior, so it keeps its nine tracks at every width — .grid-9's own collapse rule would otherwise flatten it here and the spec would illustrate nothing. | 4021 |
.sg-grid-demo > .sg-span-3 | 4022 | |
.sg-grid-demo > .sg-span-4 | 4023 | |
.sg-grid-demo > .sg-span-5 | 4024 | |
.sg-grid-demo > .sg-span-6 | 4025 | |
.sg-grid-demo > .sg-span-9 | 4026 | |
.sg-grid-demo > .sg-grid-ghost | Unused columns: outlined, not filled, so white space reads as space. | 4028 |
.sg-grid-label, .sg-wire-frames > div > p | Annotations over a diagram, in the label voice. The wireframes caption every frame the same way, so their captions take this by position rather than carrying the class on 42 elements. | 4036 |
.sg-grid-label:first-child, .sg-wire-frames > div > p | 4046 |
Theme panels #
The same card rendered in each palette, side by side, so light and dark can be compared without toggling the page.
Layout wireframes #
One schematic per layout the site actually uses. Plain boxes on the real tokens — a picture of the tracks, not a copy of the section, so a diagram stays true when the copy inside it changes. Each frame sets its own tracks through the .sg-wf-row* classes rather than Tailwind's grid, which keeps reader mode's flatten-everything rule off the diagrams: a wireframe collapsed to one column illustrates nothing. The vocabulary is flat, not BEM: every block and every row variant carries the shared base through the :is() lists below, so markup writes class="sg-wf-card sg-wf-tall" rather than repeating a base class on 230 elements. Order matters — each base rule comes before the variants that override it.
| Selector | Note | Line |
|---|---|---|
.sg-wire | 4087 | |
.sg-wire:first-child | 4088 | |
.sg-wire-head | 4089 | |
.sg-wire-head h4 | The layout's name and, beside it, where it lives. | 4091 |
.sg-wire-where | 4092 | |
.sg-wire-frames | The wide frame with the phone frame beside it. Both stack on a narrow screen, and the phone frame keeps its width there — it reads as a phone because it is narrow, so letting it fill the column would lose the point. Widths are in rem, so the whole diagram grows with the text-size setting instead of being zoomed back down. | 4103 |
.sg-wire-frames@media (min-width: 640px) | 4105 | |
.sg-wire-frames--trio@media (min-width: 640px) | 4106 | |
.sg-wire-frames--duo@media (min-width: 640px) | 4107 | |
.sg-wire-frame | 4109 | |
.sg-wire-frame--phone | 4118 | |
.sg-wf-key-list | The key: each block style beside what it stands for. | 4121 |
.sg-wf-key-list@media (min-width: 560px) | 4122 | |
.sg-wf-key-list@media (min-width: 900px) | 4123 | |
.sg-wf-key-list > li | 4124 | |
.sg-wf-canvas | The reading column drawn inside the viewport: dashed, because the canvas is a width limit, not a thing you can see. The inset stands in for the cap plus the side padding — a proportion, not a measurement. | 4136 |
.sg-wf-canvas--wide | 4145 | |
.sg-wf-band | A band whose ground runs the full width with only its content in the canvas — the closing booking panel. It re-points the block tokens rather than reaching in with descendant selectors, so a CTA on the navy ground still reads as a CTA, the way .btn-primary does on dark. | 4150 |
.sg-wf-band--dark | 4156 | |
.sg-wf-stack | Rows and stacks inside a frame. A row that has real proportions — the hero's 4:5, the fine print's 1.45:1 — states them; the rest split evenly. | 4167 |
:is(.sg-wf-row, .sg-wf-row-4, .sg-wf-row-5, .sg-wf-row-hero, .sg-wf-row-rail, .sg-wf-row-aside, .sg-wf-row-doc, .sg-wf-row-icon, .sg-wf-row-bar) | 4168 | |
.sg-wf-row-4 | 4176 | |
.sg-wf-row-5 | 4177 | |
.sg-wf-row-hero | 4178 | |
.sg-wf-row-rail | 4179 | |
.sg-wf-row-aside | 4180 | |
.sg-wf-row-doc | 4181 | |
.sg-wf-row-icon | 4182 | |
.sg-wf-row-bar | 4183 | |
.sg-wf-tight | Closes a stack or a row up; reads the same on either. | 4185 |
.sg-wf-gap | The empty middle track of a bar row: the space between the two ends, with nothing in it to draw. | 4188 |
.sg-wf-measure | A block held narrower than the column it sits in — the intake card. | 4190 |
.sg-wf-zig | How it works: the same four grid areas the real steps take, cards pinned to the outer edges of a two-column block with the thread weaving the gap. | 4194 |
.sg-wf-zig::before | 4200 | |
.sg-wf-zig > * | width, not max-width: these are flex containers, and justify-self would otherwise shrink them to their label. | 4210 |
.sg-wf-zig > :nth-child(1) | 4211 | |
.sg-wf-zig > :nth-child(2) | 4212 | |
.sg-wf-zig > :nth-child(3) | 4213 | |
.sg-wf-zig > :nth-child(4) | 4214 | |
.sg-wf-float::after | Float demos: the vignette photo, and About between 450 and 699px. | 4217 |
.sg-wf-float > .sg-wf-floated | 4218 | |
.sg-wf-float > :not(.sg-wf-floated) | 4219 | |
:is(.sg-wf-b, .sg-wf-xs, .sg-wf-tall, .sg-wf-xl, .sg-wf-card, .sg-wf-photo, .sg-wf-key, .sg-wf-alt, .sg-wf-ghost, .sg-wf-bleed, .sg-wf-floated) | The blocks themselves. Labels are set small and centered; a block that runs out of room clips rather than pushing the diagram out of shape. The two tokens let a band re-point every block inside it in one place. | 4224 |
.sg-wf-xs | 4241 | |
.sg-wf-tall | 4242 | |
.sg-wf-xl | 4243 | |
.sg-wf-key | The filled blocks borrow token pairs already contrast-checked in both themes, same as the grid demo's spans. | 4246 |
.sg-wf-alt | 4247 | |
.sg-wf-card | 4248 | |
.sg-wf-ghost | Dashed = space the layout leaves empty, or a block that isn't there at this width. Never a thing you can see on the page. | 4251 |
.sg-wf-photo | Photography, hatched so it doesn't read as another panel. | 4253 |
.sg-wf-bleed | A block that bleeds off the right edge of the frame — the hero photo. | 4258 |
.sg-wf-overlay | Overlay frame: the contact drawer over a dimmed page, plus the tab that opens it. The scrim and panel are absolutely placed the way the real ones are fixed, so the diagram keeps the drawer's proportion (min(30rem, 100vw) against the viewport). | 4264 |
.sg-wf-scrim | 4265 | |
.sg-wf-drawer | 4271 | |
.sg-wf-tab | 4283 |
Style guide on paper #
Kept here, inside the layer, rather than in the PRINT section at the end of the file. That section is outside @layer and so is never purged; these rules name only the guide's classes, and in here they land in style-guide.css alone. They follow the furniture above, so they still win on source order. Every selector must carry a class: one rule without one (an attribute selector, say) and Tailwind keeps the whole @media block in both stylesheets.
| Selector | Note | Line |
|---|---|---|
.sg-bar@media print | The bar is navigation, so it goes; the masthead's navy becomes plain type, and the contents folds to two columns. | 4304 |
.sg-toc a[aria-current]@media print | 4305 | |
.sg-mast@media print | 4306 | |
.sg-mast h1, .sg-mast-lede@media print | 4307 | |
.sg-mast-maze, .sg-mast-key@media print | 4308 | |
.sg-shell@media print | 4309 | |
.sg-toc@media print | 4310 | |
.sg-toc ol@media print | 4311 | |
.sg-main > section@media print | 4312 | |
.sg-main :is(h2, h3, h4)@media print | 4313 | |
.sg-scroll@media print | 4314 | |
.sg-scroll > .sg-table@media print | 4315 | |
.sg-table tr, .sg-specimen, .sg-rules > li, .sg-plate, .sg-tile, .sg-theme-panel@media print | 4316 | |
.sg-tint > i, .sg-chip, .sg-pair, .sg-plate--navy, .sg-theme-panel, .sg-theme-swatches > span, .sg-grid-demo > *, .sg-tile, .sg-favicon-dark@media print | Color is the content of these, so they print. | 4318 |
.sg-wf-b, .sg-wf-xs, .sg-wf-tall, .sg-wf-xl, .sg-wf-photo, .sg-wf-key, .sg-wf-alt, .sg-wf-bleed, .sg-wf-floated, .sg-wf-band, .sg-wf-tab, .sg-wf-scrim@media print | So are the wireframes. Their blocks are filled shapes, and a printer that drops the fills leaves a page of empty outlines. | 4332 |
.sg-wire@media print | The wide frame and the phone frame side by side, so a layout and its phone form stay on one line of the printed page. | 4341 |
.sg-wire-frames@media print | 4342 | |
.sg-wire-frames--trio@media print | 4343 | |
.sg-wire-frames--duo@media print | 4344 |
Utilities
25 rules · starts at 4348
General #
| Selector | Note | Line |
|---|---|---|
.on-dark | 4352 | |
.grid-9 | 4353 | |
.grid-9@media (max-width: 1023px) | 4355 | |
.grid-9 > *@media (max-width: 1023px) | 4356 | |
#top .grid-9@media (max-width: 1023px) | Hero: keep the framed photo beside the heading below the lg breakpoint too, mirroring the desktop split, instead of dropping it full-width under the copy. Tablet range keeps the whole illustration at its natural ratio. | 4362 |
#top .grid-9 > div:nth-of-type(2)@media (max-width: 1023px) | 4363 | |
#top .grid-9@media (max-width: 499px) | Below 500px the hero image itself is hidden (index.html), so collapse back to a single column instead of reserving the 38% track for nothing. | 4368 |
#top .grid-9 > div:nth-of-type(2)@media (max-width: 767px) | Below the md breakpoint the column is too narrow for the full 4:3 illustration, so the figure stretches to the copy's height and the photo covers it. The focus sits just left of the figures (~78%): they stay in frame at every width, and the maze on their left fills more of the strip as the screen widens, less as it narrows. | 4376 |
#top .grid-9 > div:nth-of-type(2) .framed img@media (max-width: 767px) | 4377 | |
#top .grid-9 > div:nth-of-type(2)@media (min-width: 768px) | Wherever the hero photo shows, run it flush to the browser's right edge: pull it across .canvas's 2rem padding plus the gutter that opens once .canvas hits its 1000px cap. Square off the right side so it reads as bleeding off-screen. The people sit on the right, so they stay in view. | 4390 |
#top .grid-9 > div:nth-of-type(2) .framed@media (min-width: 768px) | 4391 | |
#top .grid-9 > div:nth-of-type(2) .framed img@media (min-width: 768px) | 4392 | |
#top .grid-9 > div:nth-of-type(2)@media (min-width: 500px) | 4399 | |
#top .grid-9 > div:nth-of-type(2) .framed@media (min-width: 500px) | 4402 | |
#about .grid-9@media (min-width: 700px) | About: at >=700px the left column (heading + portrait) is one sticky rail beside the scrolling bio. The rail only needs room for the ~200px portrait, so it gets a fixed track rather than a 4/9 share that would strand a gap before the bio. Between 450 and 699 the rail goes full-width and the portrait floats right so the bio wraps around it. Below 450 everything stacks. | 4413 |
#about .about-rail@media (min-width: 700px) | 4414 | |
#about .grid-9@media (min-width: 450px) and (max-width: 699px) | 4417 | |
#about .grid-9::after@media (min-width: 450px) and (max-width: 699px) | 4418 | |
#about .about-rail > .meta@media (min-width: 450px) and (max-width: 699px) | 4419 | |
#about .about-rail > figure@media (min-width: 450px) and (max-width: 699px) | 4420 | |
#about .grid-9@media (min-width: 1024px) | 4423 | |
.on-dark-soft | Paper text at reduced strength, for copy on the navy/midnight grounds. | 4426 |
.rise-0 | Stagger index for [data-rise] entrance animations. | 4428 |
.rise-1 | 4429 | |
.rise-3 | 4430 |
Unlayered, so it follows Tailwind's utilities and wins on source order. Written for the browser default of "background graphics" off: anything that has to reach paper is carried by text color or a border, never a fill. Margin boxes are Chromium-only; other engines fall back to the footer URL.
107 rules · starts at 4433
General #
| Selector | Note | Line |
|---|---|---|
@page | 4440 | |
:root:root:root@media print | Paper is always the light theme, whatever the screen was set to. The triple :root outranks both dark blocks ((0,2,0) each). | 4460 |
html@media print | Every rem shrinks together: 18px body copy lands at ~11pt. | 4486 |
body@media print | 4487 | |
*, *::before, *::after@media print | 4489 | |
.reveal-pending, .reveal-pending > *, .reveal-pending tr, [data-rise], .how-flow .how-step@media print | Entrance start-states hold content invisible until it scrolls into view. | 4496 |
.reveal-pending::after@media print | 4501 | |
.table-wrap, [data-turnstile-field]@media print | Scroll containers don't fragment across pages; let them flow. | 4504 |
Screen-only chrome and interactions #
| Selector | Note | Line |
|---|---|---|
body::after, .skip-link, .site-header nav, #nav-toggle, .drawer-tab, #contact-backdrop, #contact-modal, #intake, .path-card ul, p:has(> button[data-open-contact]), button[data-open-contact], p:has(> a[href$=".pdf"]), p:has(> a.link-quiet[href="index.html"]), .hero-atmos::before, .path-card::before, .how-flow::before, .how-spine, .how-pin, .ba-list::before, .faq-sign, .theme-switch, .theme-orb, .site-footer nav, .site-footer .brand-lockup, .site-footer div:has(> a > .icon)@media print | 4508 |
Type and rhythm #
| Selector | Note | Line |
|---|---|---|
h1, h2, h3, h4@media print | 4535 | |
p, li, dd@media print | 4536 | |
img, svg, figure, tr, .card, .callout, .ba-list > li, .dl-rows > div@media print | 4537 | |
a[href^="#"], a[href^="tel:"], a[href^="mailto:"]@media print | 4539 | |
main a[href^="http"]:not(.btn):not([href^="https://coastalhealthcareadvocates.org"])::after@media print | 4542 | |
.canvas, .canvas-wide@media print | 4549 | |
main > section@media print | 4551 | |
.section, .section--tight@media print | 4552 | |
.ba-mark@media print | The before/after marks are a colored disc with a white glyph; print drops background fills, so redraw them as an ink glyph inside a thin ring. | 4557 |
.ba-mark--yes@media print | 4562 | |
.ba-mark svg@media print | 4563 | |
.section h2[id$="-h"]::after, .section--tight h2[id$="-h"]::after@media print | The Sky accent rule under section headings is a fill; redraw it as a border. | 4566 |
Header #
The logo alone, once, at the top of page one
Hero #
The photo is atmosphere, not content — drop it and let the heading column take the full width.
What is an advocate #
The vignette photo is atmosphere, not content
Services #
Two columns of rows
Who we help #
About #
The portrait is a people photo, not content
How it works #
A flat 2x2 of cards, each keyed by its step color
Pricing #
| Selector | Note | Line |
|---|---|---|
.price-choices@media print | 4659 | |
.price-choice@media print | 4660 | |
.price-choice-fee@media print | 4661 | |
.price-choice-cta@media print | 4662 | |
.price-choice--start@media print | 4663 | |
.price-table@media print | 4664 | |
.price-table thead@media print | 4665 | |
.price-table th, .price-table td@media print | 4666 | |
.price-group@media print | 4668 |
FAQs #
Every answer open
| Selector | Note | Line |
|---|---|---|
.faq-layout@media print | 4671 | |
.faq-aside@media print | 4672 | |
.faq-more@media print | 4673 | |
.faq-item@media print | 4674 | |
.faq-trigger@media print | 4675 | |
.faq-panel > *@media print | 4676 | |
.faq-item::details-content@media print | A closed <details> keeps its answer in ::details-content, which the browser hides with content-visibility. Kept in a rule of its own: a browser that doesn't know the pseudo-element (before Chrome 131, Safari 18.4 and Firefox 143) drops the whole rule, and prints only the questions. | 4681 |
Closing booking panel #
Navy fill becomes a navy outline
| Selector | Note | Line |
|---|---|---|
main > #schedule@media print | 4684 | |
#schedule :is(h2, p, .text-paper, .on-dark-soft)@media print | 4692 | |
#schedule .schedule-actions@media print | 4693 | |
#schedule .btn@media print | 4694 | |
#schedule .btn .icon@media print | 4707 | |
#schedule a.btn[href*="calendly.com"]::after@media print | 4708 | |
.calendly-frame, .calendly-loading, .calendly-embed.is-live .calendly-alt@media print | The inline scheduler doesn't print: the fallback link and its address do. | 4712 |
.calendly-embed.is-live .calendly-fallback@media print | 4715 |
Legal pages #
| Selector | Note | Line |
|---|---|---|
.legal-prose@media print | 4773 | |
.legal-prose h2@media print | 4774 | |
.legal-prose li::marker@media print | 4775 | |
.legal-toc@media print | 4776 | |
.legal-toc ol@media print | 4777 | |
.legal-toc li@media print | 4778 | |
.legal-prose table@media print | 4779 | |
.legal-prose thead@media print | 4780 | |
.legal-prose thead th@media print | 4781 | |
.todo-flag@media print | 4782 | |
.legal-prose .checklist li::before@media print | 4783 | |
.legal-prose .fill@media print | Blanks meant for a pen: dark enough to see, long enough to write on. | 4785 |
.legal-prose .sig-grid@media print | 4786 | |
.legal-prose .sig-card@media print | 4787 | |
.legal-prose .sig-card p@media print | 4788 | |
.legal-prose .sig-card .fill@media print | 4789 |
Keyframes
| Name | Note | Line |
|---|---|---|
beacon-settle | 1130 | |
rise-in | Starts at 0.01, not 0: Chrome skips opacity-0 paints when choosing the Largest Contentful Paint, so a fully transparent hero left Lighthouse with no LCP candidate on slow loads (NO_LCP). 0.01 looks the same. | 1137 |
reveal-rise | 1224 | |
ba-slide-out | Starts exactly under Before: one column width plus the 1rem gap. | 1229 |
reveal-focus | 1233 | |
reveal-rule | 1237 | |
reveal-settle | 1241 | |
reveal-tick | 1245 | |
reveal-fade | 1249 | |
reveal-zoom | 1253 | |
how-march | 1534 | |
drawer-in | 1749 | |
backdrop-in | 1753 | |
drawer-out | 1767 | |
backdrop-out | 1771 | |
lightbox-in | 1793 | |
lightbox-out | 1800 | |
beacon-bloom | 2873 | |
beacon-sweep | 2878 | |
beacon-arrive | 2884 | |
beacon-glow-fade | 2888 | |
fp-highlight | 3134 | |
fp-draw | 3138 | |
dl-read | 3166 | |
tab-arrive | 3343 | |
maze-route | 3359 |
Annotated source
src/input.css as authored, before Tailwind. Line numbers above link here.
/* ============================================================================
Coastal Healthcare Advocates — source stylesheet
Build: npm run build (watch while developing: npm run dev)
Output: site/assets/css/site.css — every page except the style guide
site/assets/css/style-guide.css — style-guide.html and the generated CSS reference
Both are git-ignored, generated by scripts/build-css.js from this one file and the
same tailwind.config.js; only the content scanned for used classes differs, which
keeps the style guide's .sg-* reference classes out of the stylesheet visitors load.
========================================================================== */
/* --- Self-hosted fonts (latin subset). Kept outside @layer so they pass through as-is. --- */
/* Ysabeau is a variable font, so it is declared once with a weight range and serves
400-600 from a single file. Every other family here is static and single-weight
(IBM Plex Mono ships 400 and 500 as two separate files). */
@font-face { font-family: 'IBM Plex Mono'; font-style: normal; font-weight: 400; font-display: swap; src: url('../fonts/ibm-plex-mono-400.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: 'IBM Plex Mono'; font-style: normal; font-weight: 500; font-display: swap; src: url('../fonts/ibm-plex-mono-500.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: 'Questrial'; font-style: normal; font-weight: 400; font-display: swap; src: url('../fonts/questrial-400.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: 'Tenor Sans'; font-style: normal; font-weight: 400; font-display: swap; src: url('../fonts/tenor-sans-400.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: 'Ysabeau'; font-style: normal; font-weight: 400 600; font-display: swap; src: url('../fonts/ysabeau-400.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@tailwind base;
@tailwind components;
@tailwind utilities;
/* How far the Sand highlighter has swept across a flagged sample-statement row.
Registered so it animates smoothly (an unregistered custom property only flips). */
@property --fp-sweep {
syntax: '<percentage>';
inherits: true;
initial-value: 100%;
}
/* ============================================================================
BASE
========================================================================== */
@layer base {
/* ---- Tokens: verbatim from the Coastal Brand Guidelines artifact (§01, §07) ---- */
/* .sg-theme-light rides along so the style guide's light panel can show the
light palette while the page itself is dark. Custom properties inherit from
the nearest defining ancestor, so the panel's own declarations win inside it. */
:root, .sg-theme-light {
/* Palette — identity, never changes */
--navy: #0F1E4A;
--sky: #4FACFE;
--cyan: #00F2FE;
--seagrass: #0BA678;
--coral: #FE1E1A;
--lantern: #E07A00;
--sand: #FFC93C;
--midnight: #0A0F24;
--paper: #F5FBFF;
/* Semantic — light */
--bg: #FEFEFE;
--surface: #FDFEFF;
--ink: #0F1E4A;
--ink-soft: #3C4A73;
--line: #DDE4F2;
--line-soft: #EAEFF9;
--tag-bg: #EAF3FF;
--warn-bg: #EDF4FE;
--warn-line: #C3D8F2;
--warn-ink: #2F4468;
/* Type — the label face as a custom property, for the handful of rules that
need the family from a var() rather than theme(). Resolved from the Tailwind
theme so tailwind.config.js stays the single source of truth for the stack. */
--font-sans: theme('fontFamily.label');
/* Inline link color — a darkened Sky that clears WCAG AA (6.5:1) on the
light ground; nav / buttons / footer set their own color and are unaffected. */
--link: #0B5FA5;
/* Accent-as-ink: darker palette steps for when an accent carries text or a
focus ring on the light ground (raw Sky is 2.4:1, Lantern 3.0:1). Each
clears 4.3:1 on its own 12% wash. Dark theme points them back at the palette. */
--sky-ink: #1B6AB4;
--seagrass-ink: #087A58;
--lantern-ink: #A85A00;
--coral-ink: #C4140F;
--ba-after-bg: var(--seagrass-ink);
--ba-after-ink: #FFFFFF;
/* Sticky header height: logo + py-4 (2rem) + 1px border. One source for
scroll-padding and every sticky offset; logo height is set from it below. */
--logo-h: 3.25rem;
--header-h: calc(var(--logo-h) + 2rem + 1px);
--ring: var(--navy);
--focus-on-dark: var(--sky);
/* Component roles. Every light/dark difference a component has is a token
here, so the two dark blocks below are the only place dark is defined. */
--brand-mark: url('../logomarks/marks/advocate-beacon-primary.svg');
--brand-ink: var(--navy);
--brand-sub: var(--sky-ink); /* Sky itself is 2.4:1 on white */
--brand-foot-mark: url('../logomarks/marks/advocate-beacon-deep-tone-alt.svg');
--brand-foot-ink: var(--navy);
--brand-foot-sub: #1E2B5A; /* #2E3F72 was 4.18:1 on the Sky footer */
--cta-bg: var(--navy); /* .btn-primary */
--cta-ink: var(--paper);
--cta-hover-bg: var(--midnight);
--cta-hover-filter: none;
--accent-hover-bg: var(--navy); /* Sky-at-rest controls: path CTAs, drawer tab */
--accent-hover-ink: var(--paper);
--nav-current: var(--navy);
--path-photo-opacity: 1;
--choice-accent: var(--sky);
--theme-icon-light: block;
--theme-icon-dark: none;
/* Site footer: a Sky ground with Navy ink in light (Navy 6.6:1, #20305C 5.3:1
on Sky); Midnight with Paper in dark. */
--footer-bg: var(--sky);
--footer-ink: var(--navy);
--footer-dim: #20305C;
--footer-title: #20305C;
--footer-link: var(--navy);
--footer-accent: var(--navy);
--footer-accent-ink: var(--paper);
--footer-sw-off: #20305C;
--footer-edge: transparent;
/* Contact drawer + mobile nav ground. In light these resolve to the page's own
tokens (computed here at :root, so re-pointing them in scope is not circular);
dark inverts both panels to a Sky ground (Navy on Sky ≈ 6.6:1). */
--panel-bg: var(--bg);
--panel-surface: var(--surface);
--panel-ink: var(--ink);
--panel-ink-soft: var(--ink-soft);
--panel-line: var(--line);
--panel-line-soft: var(--line-soft);
--panel-tag-bg: var(--tag-bg);
--panel-link: var(--link);
--panel-ring: var(--ring);
--panel-accent: var(--choice-accent);
--panel-cta-bg: var(--cta-bg);
--panel-cta-ink: var(--cta-ink);
--panel-cta-hover-bg: var(--cta-hover-bg);
--panel-cta-hover-filter: var(--cta-hover-filter);
color-scheme: light;
}
/* Dark: explicit toggle wins in both directions; system pref applies when unset. */
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #0A0F24;
--surface: #101A3A;
--ink: #EAF0FF;
--ink-soft: #BCCCF0;
--line: #25315C;
--line-soft: #1B274A;
--tag-bg: #16234A;
--warn-bg: #241C0C;
--warn-line: #4A3A14;
--warn-ink: #E8C878;
--link: var(--sky);
--ring: var(--sky);
--sky-ink: var(--sky);
--seagrass-ink: #1FC493;
--lantern-ink: var(--lantern);
--coral-ink: #FF6A66;
--ba-after-bg: var(--seagrass);
--ba-after-ink: var(--midnight);
--brand-mark: url('../logomarks/marks/advocate-beacon-dark.svg');
--brand-ink: #EAF0FF;
--brand-sub: #6FC3FF;
--brand-foot-mark: url('../logomarks/marks/advocate-beacon-reversed.svg');
--brand-foot-ink: var(--paper);
--brand-foot-sub: var(--cyan);
--cta-bg: var(--sky);
--cta-ink: var(--navy);
--cta-hover-bg: var(--sky);
--cta-hover-filter: brightness(1.08);
--accent-hover-bg: var(--paper);
--accent-hover-ink: var(--navy);
--nav-current: var(--sky);
--path-photo-opacity: 0.5;
--theme-icon-light: none;
--theme-icon-dark: block;
--footer-bg: var(--midnight);
--footer-ink: var(--paper);
--footer-dim: color-mix(in srgb, var(--paper) 74%, transparent);
--footer-title: color-mix(in srgb, var(--paper) 80%, transparent);
--footer-link: var(--sky);
--footer-accent: var(--sky);
--footer-accent-ink: var(--navy);
--footer-sw-off: color-mix(in srgb, var(--paper) 62%, transparent);
--footer-edge: var(--line);
--panel-bg: var(--sky);
--panel-surface: #EAF3FF;
--panel-ink: var(--navy);
--panel-ink-soft: #20305C;
--panel-line: #2E3F72;
--panel-line-soft: #A9D6FF;
--panel-tag-bg: #CFE6FF;
--panel-link: var(--navy);
--panel-ring: var(--navy);
--panel-accent: var(--navy);
--panel-cta-bg: var(--navy);
--panel-cta-ink: var(--paper);
--panel-cta-hover-bg: var(--midnight);
--panel-cta-hover-filter: none;
color-scheme: dark;
}
}
/* Same values as the media block above — keep the two in lockstep.
.sg-theme-dark is the style guide's dark panel; it takes these values rather
than repeating them, so dark is still defined in only two places. */
:root[data-theme="dark"], .sg-theme-dark {
--bg: #0A0F24;
--surface: #101A3A;
--ink: #EAF0FF;
--ink-soft: #BCCCF0;
--line: #25315C;
--line-soft: #1B274A;
--tag-bg: #16234A;
--warn-bg: #241C0C;
--warn-line: #4A3A14;
--warn-ink: #E8C878;
--link: var(--sky);
--ring: var(--sky);
--sky-ink: var(--sky);
--seagrass-ink: #1FC493;
--lantern-ink: var(--lantern);
--coral-ink: #FF6A66;
--ba-after-bg: var(--seagrass);
--ba-after-ink: var(--midnight);
--brand-mark: url('../logomarks/marks/advocate-beacon-dark.svg');
--brand-ink: #EAF0FF;
--brand-sub: #6FC3FF;
--brand-foot-mark: url('../logomarks/marks/advocate-beacon-reversed.svg');
--brand-foot-ink: var(--paper);
--brand-foot-sub: var(--cyan);
--cta-bg: var(--sky);
--cta-ink: var(--navy);
--cta-hover-bg: var(--sky);
--cta-hover-filter: brightness(1.08);
--accent-hover-bg: var(--paper);
--accent-hover-ink: var(--navy);
--nav-current: var(--sky);
--path-photo-opacity: 0.5;
--theme-icon-light: none;
--theme-icon-dark: block;
--footer-bg: var(--midnight);
--footer-ink: var(--paper);
--footer-dim: color-mix(in srgb, var(--paper) 74%, transparent);
--footer-title: color-mix(in srgb, var(--paper) 80%, transparent);
--footer-link: var(--sky);
--footer-accent: var(--sky);
--footer-accent-ink: var(--navy);
--footer-sw-off: color-mix(in srgb, var(--paper) 62%, transparent);
--footer-edge: var(--line);
--panel-bg: var(--sky);
--panel-surface: #EAF3FF;
--panel-ink: var(--navy);
--panel-ink-soft: #20305C;
--panel-line: #2E3F72;
--panel-line-soft: #A9D6FF;
--panel-tag-bg: #CFE6FF;
--panel-link: var(--navy);
--panel-ring: var(--navy);
--panel-accent: var(--navy);
--panel-cta-bg: var(--navy);
--panel-cta-ink: var(--paper);
--panel-cta-hover-bg: var(--midnight);
--panel-cta-hover-filter: none;
color-scheme: dark;
}
@media (min-width: 480px) { :root { --logo-h: 5.46875rem; } }
@media (min-width: 1024px) { :root { --logo-h: 6.25rem; } }
* { min-width: 0; }
/* Keep even Tailwind's Preflight defaults on the style-guide palette. */
*, ::before, ::after { border-color: var(--line); }
::placeholder { color: var(--ink-soft); opacity: 1; }
html {
scroll-padding-top: calc(var(--header-h) + 1rem);
-webkit-text-size-adjust: 100%;
}
/* Text size (Site preferences → data-text on <html>, set by theme.js). Scales
the root, so every rem-sized thing grows with the text. At Larger and
Largest (xl / xxl) the page also keeps its phone layout and hamburger menu
at every width (scripts/postcss/text-layout.js rewrites the width media
queries); Default and Large keep the normal responsive layout.
The header bar is zoomed back to its usual size, since a 1.5× logo would
crowd it; the menu panel inside it is zoomed up again, since it's reading
matter. Screen only: print sets its own root size. */
@media screen {
:root[data-text] {
font-size: calc(var(--text-scale) * 100%);
--header-h: calc((var(--logo-h) + 2rem) / var(--text-scale) + 1px);
}
:root[data-text="lg"] { --text-scale: 1.125; --text-unscale: 0.8889; }
:root[data-text="xl"] { --text-scale: 1.25; --text-unscale: 0.8; }
:root[data-text="xxl"] { --text-scale: 1.5; --text-unscale: 0.6667; }
:root[data-text] .site-header { zoom: var(--text-unscale); }
:root[data-text] .site-header .canvas-wide { max-width: calc(1180px * var(--text-scale)); }
:root[data-text] #mobile-nav { zoom: var(--text-scale); }
/* The header's round buttons too: a zoomed-down 44px would miss the
touch-target minimum, and their icons are read at the chosen size. */
:root[data-text] .site-header .header-icon-btn { zoom: var(--text-scale); }
/* The footer lockup is a fixed-geometry logo, not text to read. */
:root[data-text] .brand-lockup-footer { zoom: var(--text-unscale); }
/* Long addresses and paths can't wrap at a space. */
:root[data-text] :is(a, code) { overflow-wrap: anywhere; }
/* So can a single long word in a big heading ("Responsibility") or a button
("Consultation") on a phone, and the style guide's type specimens. */
:root[data-text] :is(h1, h2, h3, h4) { overflow-wrap: break-word; hyphens: auto; }
/* A button's label is an anonymous flex item, which only shrinks below its
longest word with `anywhere`. */
:root[data-text] .btn { overflow-wrap: anywhere; hyphens: auto; }
:root[data-text] .sg-specimen > * { overflow-wrap: anywhere; }
}
/* Phones and small tablets: the sample statement's three-column table can't
fit scaled type, so the mock document keeps its usual size there. */
@media screen and (max-width: 599px) {
:root[data-text] .fineprint-doc { zoom: var(--text-unscale); }
}
body {
background: var(--bg);
color: var(--ink);
font-family: theme('fontFamily.body');
font-size: 1.125rem; /* Ysabeau, 16–20px range — 18px for older readers */
line-height: 1.6;
font-weight: 400;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
/* §04 Heading Sizes. All headings = Questrial 400 (matches H1). */
h1, h2, h3, h4 {
color: var(--ink);
font-family: theme('fontFamily.label');
font-weight: 400;
letter-spacing: 0.005em;
text-wrap: balance;
margin: 0;
}
/* Each step is at least 1.25x the one below (body 18px, H4 23px, H3 23-30px,
H2 30-40px, H1 36-58px), so size alone separates the roles; leading
tightens as the size grows. */
h1 {
font-size: theme('fontSize.h1');
line-height: 1.08;
letter-spacing: -0.005em;
}
h2 { font-size: theme('fontSize.h2'); line-height: 1.18; }
h3 { font-size: theme('fontSize.h3'); line-height: 1.25; }
h4 { font-size: theme('fontSize.h4'); line-height: 1.3; }
/* Section headings carry a short accent rule beneath them — left-aligned,
8rem, in Sky. Hidden on the centered navy Schedule heading; re-centered
under the centered Intake heading. */
.section h2[id$="-h"]::after,
.section--tight h2[id$="-h"]::after {
content: "";
display: block;
width: 8rem;
height: 2px;
margin-block: 1.5rem;
background: var(--sky);
border-radius: 1px;
}
#schedule-h::after { display: none; }
.text-center > h2[id$="-h"]::after { margin-inline: auto; }
p { text-wrap: pretty; }
/* Inline / prose links read as links: distinct color + underline. Components
that are links but shouldn't look like body links (.nav-link, .btn, .link-quiet,
.site-footer a) all set their own color + text-decoration. */
a {
color: var(--link);
text-decoration: underline;
text-decoration-color: color-mix(in srgb, currentColor 45%, transparent);
text-underline-offset: 0.18em;
text-decoration-thickness: from-font;
}
a:hover { text-decoration-color: currentColor; }
/* A JS action (opens the Site preferences dialog) mentioned inline in prose,
styled to match the surrounding text link exactly. */
.text-link-inline {
all: unset;
cursor: pointer;
color: var(--link);
text-decoration: underline;
text-decoration-color: color-mix(in srgb, currentColor 45%, transparent);
text-underline-offset: 0.18em;
text-decoration-thickness: from-font;
}
.text-link-inline:hover { text-decoration-color: currentColor; }
/* Classless links on a dark ground (e.g. the navy Schedule section) need the
light-on-dark link color instead of the darkened Sky. */
.on-dark a:not([class]) { color: var(--sky); }
/* Plain text links that leave the site get a trailing "↗" (e.g. ada.gov).
Classed links — buttons, icon circles, the GNA badge — carry their own
icon, and absolute links to our own domain aren't external. Drawn as a
mask so it takes the link color, in the sprite's 1.75px round stroke.
Screen only: print swaps ::after for the URL in brackets. */
@media screen {
a[href^="http"]:not([class]):not([href*="coastalhealthcareadvocates.org"])::after {
content: "";
display: inline-block;
width: 0.7em;
height: 0.7em;
margin-left: 0.2em;
vertical-align: 0.05em;
background-color: currentColor;
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.25' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 18 18 6M8.5 6H18v9.5'/%3E%3C/svg%3E") center / contain no-repeat;
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.25' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 18 18 6M8.5 6H18v9.5'/%3E%3C/svg%3E") center / contain no-repeat;
}
}
@media screen and (prefers-reduced-motion: no-preference) {
a[href^="http"]:not([class]):not([href*="coastalhealthcareadvocates.org"])::after {
transition: transform 180ms cubic-bezier(0.16, 1, 0.3, 1);
}
a[href^="http"]:not([class]):not([href*="coastalhealthcareadvocates.org"]):hover::after {
transform: translate(1px, -1px);
}
}
/* Abbreviations: a quiet dotted underline in running text signals the title
expansion. Inside links and headings it would stack on the link underline or
clutter the type, so it's dropped there. */
abbr[title] {
text-decoration: underline dotted;
text-decoration-color: color-mix(in srgb, currentColor 55%, transparent);
text-underline-offset: 0.18em;
text-decoration-thickness: from-font;
cursor: help;
}
:where(a, button, h1, h2, h3, h4) abbr[title] { text-decoration: none; cursor: inherit; }
::selection { background: rgba(79, 172, 254, 0.28); }
:where(a, button, input, textarea, select, summary, [tabindex]):focus-visible {
outline: 3px solid var(--ring);
outline-offset: 2px;
border-radius: 3px;
}
/* On dark grounds the navy ring vanishes — switch to sky. */
.on-dark :where(a, button, input, textarea, select, summary, [tabindex]):focus-visible {
outline-color: var(--focus-on-dark);
}
/* Reduced motion: drop spatial movement and choreography, keep the short
color/opacity feedback that tells a visitor an action registered. */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
animation-delay: 0ms !important;
transition-duration: 120ms !important;
transition-property: opacity, color, background-color, border-color,
outline-color, box-shadow, text-decoration-color, fill, stroke !important;
scroll-behavior: auto !important;
}
.hero-intro [data-rise],
.hero-intro .hero-atmos::before {
opacity: 1 !important;
transform: none !important;
}
}
@media (prefers-reduced-motion: no-preference) {
html { scroll-behavior: smooth; }
}
}
/* ============================================================================
COMPONENTS
========================================================================== */
@layer components {
.canvas {
width: 100%;
max-width: theme('maxWidth.canvas');
margin-inline: auto;
padding-inline: 1.5rem;
}
/* Header / footer breathe a little wider than the 1000px reading column. */
.canvas-wide {
width: 100%;
max-width: 1180px;
margin-inline: auto;
padding-inline: 1.5rem;
}
@media (min-width: 480px) {
.canvas, .canvas-wide { padding-inline: 2rem; }
}
/* padding-block only — the hero puts .section and .canvas on one element, and a
`padding: X 0` shorthand there would wipe .canvas's padding-inline. */
.section { padding-block: clamp(2.25rem, 1.5rem + 3.25vw, 4rem); }
.section--tight { padding-block: clamp(2rem, 1.4rem + 2vw, 3rem); }
/* §Type: .lede — Ysabeau 21px / 1.45, --ink-soft, max 60ch */
.lede {
font-size: theme('fontSize.lede');
line-height: 1.45;
color: var(--ink-soft);
max-width: 60ch;
}
.prose-measure { max-width: theme('maxWidth.measure'); }
.prose-measure > * + * { margin-top: 1.1em; }
/* Small supporting text. Body typeface — IBM Plex Mono is reserved for
actual figures (see .num, .price-fee, .step-num). */
.meta {
font-family: theme('fontFamily.body');
font-size: 0.9375rem;
letter-spacing: 0.01em;
color: var(--ink-soft);
}
/* Figures: prices, counts, code values. IBM Plex Mono otherwise runs only in
form error messages (.field-error, .error-summary). Phone numbers stay in
the body face — Mono's full-width space and brackets read as gaps there. */
.num {
font-family: theme('fontFamily.mono');
font-feature-settings: 'tnum' 1;
letter-spacing: 0.01em;
}
/* File paths and token names have no spaces to wrap at; let them break
anywhere rather than push a phone-width page sideways. */
code { overflow-wrap: anywhere; }
/* --- Buttons --- */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
min-height: 48px;
/* A label stays on one line wherever it fits; on a narrow phone or at the
larger text sizes it wraps inside the button instead of spilling past it. */
text-align: center;
text-wrap: balance;
padding: 0.75rem 1.4rem;
font-family: theme('fontFamily.label');
font-size: 0.9375rem;
letter-spacing: 0.06em;
text-transform: uppercase;
text-decoration: none;
border-radius: theme('borderRadius.card');
border: 1px solid transparent;
cursor: pointer;
transition: transform 0.18s theme('transitionTimingFunction.calm'),
background-color 0.18s ease, border-color 0.18s ease, color 0.18s ease;
}
/* A hair of lift on hover; the press (:active, below in source so it wins) dips
back down. Reduced motion drops the transform via the global rule. */
.btn:hover { transform: translateY(-1px); }
.btn:active { transform: translateY(1px); }
/* Primary CTA — Navy fill, Paper text on light. On dark the page ground is
near-navy, so the CTA flips to Sky fill / Navy text to stay legible. */
.btn-primary {
background: var(--cta-bg);
color: var(--cta-ink);
border-color: var(--cta-bg);
}
.btn-primary:hover {
background: var(--cta-hover-bg);
border-color: var(--cta-hover-bg);
filter: var(--cta-hover-filter);
}
.btn-ghost {
background: transparent;
color: var(--ink);
border-color: var(--ink);
}
.btn-ghost:hover { background: var(--tag-bg); }
.btn-on-dark {
background: var(--paper);
color: var(--navy);
border-color: var(--paper);
}
.btn-on-dark:hover { filter: brightness(1.05); }
.btn-ghost-on-dark {
background: transparent;
color: var(--paper);
border-color: color-mix(in srgb, var(--paper) 55%, transparent);
}
.btn-ghost-on-dark:hover {
background: color-mix(in srgb, var(--paper) 14%, transparent);
border-color: var(--paper);
}
.link-quiet {
display: inline-flex;
align-items: center;
gap: 0.35rem;
font-family: theme('fontFamily.label');
font-size: 0.9375rem;
letter-spacing: 0.02em;
color: var(--ink);
text-decoration: underline;
text-decoration-color: var(--ink-soft);
}
.link-quiet:hover { text-decoration-color: currentColor; }
/* A trailing icon (the "→" on "Talk about referrals")
nudges in its direction of travel on hover. Leading icons — the PDF marks —
are :first-child, so :last-child leaves them alone. */
.link-quiet svg { transition: transform 180ms cubic-bezier(0.16, 1, 0.3, 1); }
.link-quiet:hover svg:last-child { transform: translateX(2px); }
/* "← Go back" (secondary.js): a smaller leading arrow that nudges left, its
direction of travel. Same specificity as the rule above, so it wins by order. */
.link-quiet svg.icon-back { width: 1rem; height: 1rem; }
.link-quiet:hover svg.icon-back { transform: translateX(-2px); }
/* --- Surfaces --- */
.card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: theme('borderRadius.card');
box-shadow: theme('boxShadow.card');
}
.tile {
background: var(--tag-bg);
border: 1px solid var(--line);
border-radius: theme('borderRadius.tile');
transition: border-color 0.2s ease,
transform 0.2s cubic-bezier(0.16, 1, 0.3, 1),
box-shadow 0.2s ease;
}
/* Hover acknowledges the pointer with the same small lift the how-cards use;
under reduced motion the global rule drops the transform and keeps the
border + elevation, so the feedback still reads. */
.tile:hover {
border-color: var(--sky);
transform: translateY(-3px);
box-shadow: theme('boxShadow.lift');
}
/* Headings inside a card/tile drop the tracking for readability at small sizes. */
:is(.card, .tile, .how-card-inner) :is(h2, h3, h4) {
letter-spacing: 0;
}
.callout {
background: var(--tag-bg);
border: 1px solid var(--line);
border-radius: theme('borderRadius.card');
}
/* Sizing only. Stroke vs. fill is set per-symbol in the sprite so the two
solid brand glyphs (LinkedIn, Facebook) stay filled. */
.icon { width: 1.5rem; height: 1.5rem; flex: none; }
.icon-lg { width: 2rem; height: 2rem; flex: none; }
.icon-pdf { color: var(--coral-ink); }
/* --- Wide content scrolls inside its own bordered container --- */
.table-wrap {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
border: 1px solid var(--line);
border-radius: theme('borderRadius.card');
background: var(--surface);
}
/* --- Pricing choices: the four ways in, above the detailed table --- */
.price-choices {
display: grid;
gap: 1rem;
list-style: none;
padding: 0;
}
@media (min-width: 40rem) { .price-choices { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.price-choice {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 1.35rem 1.25rem 1.4rem;
background: var(--surface);
border: 1px solid var(--line);
border-radius: theme('borderRadius.card');
}
.price-choice h3 { letter-spacing: 0; }
.price-choice p { color: var(--ink); }
/* The free call is where everyone should begin: tinted ground and a sky rule. */
.price-choice--start {
background: var(--tag-bg);
border-top: 3px solid var(--sky);
}
.price-choice-kicker {
font-family: theme('fontFamily.label');
font-size: 0.75rem;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.price-choice .price-choice-kicker { color: var(--ink-soft); }
.price-choice-fee {
font-family: theme('fontFamily.mono');
font-feature-settings: 'tnum' 1;
font-size: 1.5rem;
line-height: 1.2;
margin-block: 0.15rem 0.1rem;
}
.price-choice-fee--free {
font-family: theme('fontFamily.label');
letter-spacing: 0.06em;
text-transform: uppercase;
}
.price-choice-cta { margin-top: auto; align-self: flex-start; }
/* --- Pricing table --- */
.price-table {
width: 100%;
min-width: 32rem;
border-collapse: collapse;
font-size: 1rem;
}
.price-table th,
.price-table td {
text-align: left;
padding: 0.95rem 1.15rem;
vertical-align: top;
}
.price-table thead th {
font-family: theme('fontFamily.label');
font-size: 0.8125rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--ink-soft);
border-bottom: 1px solid var(--line);
white-space: nowrap;
}
.price-table thead th:last-child { text-align: right; }
.price-col-name { width: 24%; }
.price-col-fee { width: 8.5rem; }
.price-group {
font-family: theme('fontFamily.label');
font-size: 0.75rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--ink-soft);
background: var(--tag-bg);
padding-top: 0.6rem;
padding-bottom: 0.6rem;
border-top: 1px solid var(--line);
}
.price-table tbody:first-of-type .price-group { border-top: 0; }
.price-table tbody td,
.price-table tbody th[scope="row"] { border-top: 1px solid var(--line-soft); }
.price-table th[scope="row"] {
font-family: theme('fontFamily.label');
font-weight: 400;
font-size: 1.0625rem;
color: var(--ink);
}
.price-table td { color: var(--ink); font-size: 1rem; } /* full-strength ink for older readers */
.price-table .price-fee {
font-family: theme('fontFamily.mono');
font-feature-settings: 'tnum' 1;
color: var(--ink);
white-space: nowrap;
text-align: right;
}
.price-table td.price-fee--free {
font-family: theme('fontFamily.label');
font-weight: 400; /* Questrial ships 400 only — no faux bold */
letter-spacing: 0.06em;
text-transform: uppercase;
}
.price-unit {
font-family: theme('fontFamily.body');
font-size: 0.875rem;
color: var(--ink-soft);
}
/* "Fee:" label for the stacked layout — a real, visually-shown span (not CSS
content), hidden while the Fee column header is present. */
.price-fee-label { display: none; }
/* Below 40rem three columns can't fit, so each service stacks into its own
block instead of forcing a horizontal scroll that hides the fee. The table
elements carry explicit ARIA table roles in index.html, so display:block
here doesn't strip the row/header relationships from assistive tech. */
@media (max-width: 40rem) {
.table-wrap:has(.price-table) { overflow-x: visible; }
.price-table { min-width: 0; }
.price-table colgroup,
.price-table thead { display: none; }
.price-table tbody,
.price-table tr,
.price-table th,
.price-table td { display: block; }
.price-table tr:has(th[scope="row"]) { padding: 0.85rem 1.15rem; }
.price-table tr:has(th[scope="row"]) + tr:has(th[scope="row"]) {
border-top: 1px solid var(--line-soft);
}
.price-table tbody th[scope="row"],
.price-table tbody td { border-top: 0; padding: 0; }
.price-table td { margin-top: 0.4rem; }
.price-table td.price-fee {
text-align: left;
margin-top: 0.6rem;
}
.price-fee-label {
display: inline;
font-family: theme('fontFamily.label');
font-size: 0.75rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--ink-soft);
}
}
/* --- Definition-list rows (bio credentials) --- */
.dl-rows { border-top: 1px solid var(--line); }
.dl-rows > div {
display: grid;
gap: 0.15rem 1.5rem;
padding: 0.95rem 0;
border-bottom: 1px solid var(--line);
}
.dl-rows > div:first-child { padding-top: 1.1rem; }
@media (min-width: 768px) {
.dl-rows > div { grid-template-columns: 8.5rem 1fr; }
}
.dl-rows dt {
font-family: theme('fontFamily.label');
font-size: 0.8125rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--ink-soft);
}
.dl-rows dd { margin: 0; }
/* --- Header --- */
.site-header {
position: sticky;
top: 0;
z-index: 40;
background: color-mix(in srgb, var(--bg) 92%, transparent);
border-bottom: 1px solid var(--line);
backdrop-filter: saturate(1.4) blur(8px);
transition: transform 0.32s theme('transitionTimingFunction.calm'),
opacity 0.28s ease, box-shadow 0.28s ease;
}
@supports not (backdrop-filter: blur(1px)) {
.site-header { background: var(--bg); }
}
/* Picks up a soft elevation once the page has scrolled off the top, so the
bar separates from the content passing under it (main.js toggles .scrolled).
On the near-black dark ground this shadow is invisible — the border carries
it there — which is fine. */
.site-header.scrolled { box-shadow: 0 4px 16px rgba(10, 15, 36, 0.07); }
/* Retreats as the visitor nears the closing CTA / footer (main.js). */
/* visibility (delayed to the end of the slide) takes the links out of the tab
order, so keyboard focus can never land on an off-screen nav. */
.site-header.header-hidden {
transform: translateY(-100%);
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: transform 0.32s theme('transitionTimingFunction.calm'),
opacity 0.28s ease, box-shadow 0.28s ease, visibility 0s linear 0.32s;
}
/* Logo — the horizontal lockup rebuilt with live type, so the company name is
real text (reader mode, translation, copy, search) rather than an image.
Only the mark is a picture, CSS-swapped by theme (no flash, no JS). Every
length is in --u, one unit of the outlined SVG's 271.27 × 82 artboard, so
the lockup scales from --logo-h and matches logo-horizontal-*.svg: mark at
x 8, rule at x 79, wordmark at x 91.1 with baselines 40.22 and 61.22.
--u multiplies lengths instead of setting a tiny em base, so a browser
minimum-font-size preference can't blow the geometry up. */
.brand-lockup {
--u: calc(var(--logo-h) / 82); /* --logo-h: 3.25rem on phones, stepped up at 480 / 1024 (:root) */
--lk-mark: var(--brand-mark);
--lk-ink: var(--brand-ink);
--lk-sub: var(--brand-sub);
display: flex;
align-items: flex-start;
height: var(--logo-h);
width: calc(271.27 * var(--u));
padding-left: calc(8 * var(--u));
white-space: nowrap;
text-decoration: none;
}
/* The Advocate Beacon: the mark files are 120 × 132, drawn at half size. */
.brand-lockup::before {
content: "";
flex: none;
width: calc(60 * var(--u));
height: calc(66 * var(--u));
margin-top: calc(8 * var(--u));
background: var(--lk-mark) center / contain no-repeat;
}
/* The rule between mark and wordmark: wordmark ink at 28%. */
.brand-lockup::after {
content: "";
order: 1;
flex: none;
width: calc(1.1 * var(--u));
height: calc(58.08 * var(--u));
margin: calc(11.96 * var(--u)) 0 0 calc(11 * var(--u));
background: var(--lk-ink);
opacity: 0.28;
}
/* Line boxes are 1em tall; the offsets put each baseline where the SVG has it
(Tenor Sans ascent .92 / descent .25; Ysabeau typo metrics 1.054 / .289). */
.brand-lockup-name {
order: 2;
display: flex;
flex-direction: column;
margin-left: calc(11 * var(--u));
padding-top: calc(17.675 * var(--u));
line-height: 1;
text-transform: uppercase;
font-kerning: none; /* the outlined wordmark is set unkerned */
}
.brand-lockup-name > :first-child {
font-family: theme('fontFamily.logo');
font-weight: 400;
font-size: calc(27 * var(--u));
letter-spacing: 0.13em;
color: var(--lk-ink);
}
.brand-lockup-name > :last-child {
margin-top: calc(4.631 * var(--u));
font-family: theme('fontFamily.body');
font-weight: 500;
font-size: calc(13.5 * var(--u));
color: var(--lk-sub);
}
/* Footer colorway: deep-tone-alt on the light footer, reversed on the dark. */
.brand-lockup-footer {
--logo-h: 5rem;
--lk-mark: var(--brand-foot-mark);
--lk-ink: var(--brand-foot-ink);
--lk-sub: var(--brand-foot-sub);
}
.header-icon-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
border-radius: 999px;
border: 1px solid var(--line);
background: transparent;
color: var(--ink);
cursor: pointer;
}
.header-icon-btn:hover { background: var(--tag-bg); }
.header-icon-btn svg { transition: transform 220ms cubic-bezier(0.16, 1, 0.3, 1); }
/* The hamburger wears the menu panel's tokens so it matches the panel it opens:
page colors in light mode, a Sky fill with Navy lines in dark mode. */
#nav-toggle {
background: var(--panel-bg);
border-color: var(--panel-line);
color: var(--panel-ink);
}
#nav-toggle:hover { background: var(--panel-tag-bg); }
#nav-toggle[aria-expanded="true"] svg { transform: rotate(90deg); }
/* Mobile menu — a compact panel hung off the toggle button, not a full-width
sheet. Its containing block is the .relative wrapper around the button, so
it sits right under the hamburger. JS unhides it, then adds .is-open a
frame later to run the entrance. */
#mobile-nav,
#contact-modal:not(.is-lightbox) {
--bg: var(--panel-bg);
--surface: var(--panel-surface);
--ink: var(--panel-ink);
--ink-soft: var(--panel-ink-soft);
/* Scrollspy's "current" color (--nav-current) is a page-level token that
tracks --bg's contrast pair, not this panel's inverted one — in dark
theme it resolves to Sky, same as --panel-bg, so the current link's
text matched its own background and vanished. Re-point it at the ink
color already proven readable on --panel-bg. */
--nav-current: var(--panel-ink);
--line: var(--panel-line);
--line-soft: var(--panel-line-soft);
--tag-bg: var(--panel-tag-bg);
--link: var(--panel-link);
--ring: var(--panel-ring);
--choice-accent: var(--panel-accent);
--cta-bg: var(--panel-cta-bg);
--cta-ink: var(--panel-cta-ink);
--cta-hover-bg: var(--panel-cta-hover-bg);
--cta-hover-filter: var(--panel-cta-hover-filter);
}
#mobile-nav {
position: absolute;
top: 100%;
right: 0;
z-index: 50;
width: max-content;
min-width: 11rem;
max-width: calc(100vw - 3rem);
margin-top: 0.4rem;
padding: 0.4rem;
max-height: calc(100dvh - 6rem);
overflow-y: auto;
background: var(--bg);
border: 1px solid var(--line);
border-radius: 15px; /* rounded.panel-inner */
box-shadow: 0 18px 44px -14px rgba(10, 15, 36, 0.32);
transform-origin: top right;
opacity: 0;
transform: translateY(-8px) scale(0.94);
transition: opacity 160ms ease, transform 260ms cubic-bezier(0.16, 1, 0.3, 1);
}
#mobile-nav.is-open { opacity: 1; transform: translateY(0) scale(1); }
#mobile-nav .nav-link {
display: block;
padding: 0.6rem 0.85rem;
border-radius: 8px; /* rounded.field */
white-space: nowrap;
}
#mobile-nav .nav-link::after { content: none; } /* no underline-wipe inside the panel */
#mobile-nav .nav-link:hover,
#mobile-nav .nav-link:focus-visible { background: var(--tag-bg); }
@media (prefers-reduced-motion: reduce) {
#mobile-nav { transition: opacity 120ms ease; transform: none; }
#mobile-nav.is-open { transform: none; }
}
/* Primary desktop nav — four links (Services, About, How it works,
Pricing), sized up from the mobile-panel links' 1rem to 1.125rem since
dropping "Who we help" and FAQs left room to size up. It appears at the
`nav` screen (tailwind.config.js); below that the hamburger takes over —
re-measure that breakpoint if the links, their size, the icons or the
logo change again. */
nav[aria-label="Primary"] {
gap: 1.5rem;
}
nav[aria-label="Primary"] .nav-link {
font-size: 1.125rem;
}
.theme-icon-light { display: var(--theme-icon-light); }
.theme-icon-dark { display: var(--theme-icon-dark); }
.nav-link {
position: relative;
font-family: theme('fontFamily.label');
font-size: 1rem;
letter-spacing: 0.02em;
color: var(--ink);
text-decoration: none;
padding: 0.5rem 0.15rem 0.55rem;
white-space: nowrap;
}
/* underline wipes in from the left on hover / keyboard focus */
.nav-link::after {
content: "";
position: absolute;
left: 0.15rem;
right: 0.15rem;
bottom: 0.35rem;
height: 2px;
background: var(--sky);
transform: scaleX(0);
transform-origin: left;
transition: transform 220ms cubic-bezier(0.16, 1, 0.3, 1);
}
.nav-link:hover::after,
.nav-link:focus-visible::after { transform: scaleX(1); }
/* Scrollspy — the link for the section currently under the viewport
holds its underline and takes the accent color. */
.nav-link[aria-current="true"] { color: var(--nav-current); }
.nav-link[aria-current="true"]::after { transform: scaleX(1); }
/* --- Skip link --- */
.skip-link {
position: absolute;
left: 1rem;
top: -3.5rem;
z-index: 60;
background: var(--navy);
color: var(--paper);
padding: 0.75rem 1rem;
border-radius: 0 0 0.5rem 0.5rem;
transition: top 0.15s ease;
}
.skip-link:focus { top: 0; }
/* --- Hero atmosphere: faint horizon + beacon glow, all decorative --- */
.hero-atmos {
position: relative;
isolation: isolate;
overflow: clip;
}
.hero-atmos::before {
content: "";
position: absolute;
inset: 0;
z-index: -1;
transform-origin: 78% 12%;
background:
radial-gradient(60% 55% at 78% 18%, rgba(79, 172, 254, 0.16), transparent 70%),
radial-gradient(45% 40% at 12% 0%, rgba(0, 242, 254, 0.10), transparent 70%),
linear-gradient(180deg, transparent 62%, var(--tag-bg) 100%);
}
/* ==========================================================================
MOTION — one authored moment: the hero steadies as the beacon comes up,
once per session. Everything else is quiet feedback and continuity.
======================================================================== */
/* Focal entrance (JS-gated; content is fully visible without it). */
.js.hero-intro .hero-atmos::before {
animation: beacon-settle 1200ms cubic-bezier(0.16, 1, 0.3, 1) both;
}
.js.hero-intro [data-rise] {
animation: rise-in 640ms cubic-bezier(0.16, 1, 0.3, 1) both;
animation-delay: calc(var(--i, 0) * 90ms + 60ms);
}
@keyframes beacon-settle {
from { opacity: 0; transform: scale(1.05); }
to { opacity: 1; transform: none; }
}
/* Starts at 0.01, not 0: Chrome skips opacity-0 paints when choosing the
Largest Contentful Paint, so a fully transparent hero left Lighthouse with
no LCP candidate on slow loads (NO_LCP). 0.01 looks the same. */
@keyframes rise-in {
from { opacity: 0.01; transform: translateY(32px); }
to { opacity: 1; transform: none; }
}
/* Scroll-in: "coming into the light". Each [data-reveal] element plays once,
on its own, as it crosses into view, so a tall section unfolds as the
visitor reads down it rather than all at once at its top edge. Headings
clear from a faint haze and their Beacon Sky rule sweeps out like a beam;
the content under them follows. The value picks the treatment:
(bare) the element rises in
heading soft focus-in, then the accent rule draws out from its start
list direct children rise in turn; their line icons settle after
pair two side-by-side cards drift in from their own sides
checks list items rise in turn and each tick draws itself left to right
rows table rows fade up in turn
photo fades in while the picture eases back from a slight zoom
main.js arms .reveal-pending (the hidden start state), swaps in .reveal-in
to play, then clears it; it also writes --reveal-i on staggered items.
Fill mode is `backwards`, so once an animation ends the element is back on
its own styles and hover lifts work immediately. No JS, a failed script, or
reduced motion: nothing is armed, nothing is hidden. Opacity starts at 0.01,
not 0 — see rise-in. */
@media (prefers-reduced-motion: no-preference) {
[data-reveal] { --reveal-ease: cubic-bezier(0.16, 1, 0.3, 1); }
.js :is([data-reveal=""], [data-reveal="heading"], [data-reveal="photo"]).reveal-pending,
.js :is([data-reveal="list"], [data-reveal="pair"]).reveal-pending > *,
.js [data-reveal="checks"].reveal-pending > li,
.js [data-reveal="rows"].reveal-pending tbody tr { opacity: 0.01; }
.js [data-reveal="heading"].reveal-pending::after { transform: scaleX(0); }
.js [data-reveal=""].reveal-in {
animation: reveal-rise 720ms var(--reveal-ease) 60ms backwards;
}
.js [data-reveal="heading"].reveal-in {
animation: reveal-focus 820ms var(--reveal-ease) backwards;
}
.js [data-reveal="heading"].reveal-in::after {
transform-origin: left center;
animation: reveal-rule 1000ms var(--reveal-ease) 260ms backwards;
}
.js .text-center > [data-reveal="heading"].reveal-in::after { transform-origin: center; }
.js :is([data-reveal="list"], [data-reveal="pair"]).reveal-in > *,
.js [data-reveal="checks"].reveal-in > li {
animation: reveal-rise 720ms var(--reveal-ease) backwards;
animation-delay: calc(var(--reveal-i, 0) * 75ms);
}
/* Line icons settle a beat after their row arrives. */
.js [data-reveal="list"].reveal-in > * > :is(.service-icon, .icon-lg, .icon:first-child) {
animation: reveal-settle 640ms var(--reveal-ease) backwards;
animation-delay: calc(var(--reveal-i, 0) * 75ms + 180ms);
}
.js [data-reveal="checks"].reveal-in > li > .icon {
animation: reveal-tick 520ms var(--reveal-ease) backwards;
animation-delay: calc(var(--reveal-i, 0) * 75ms + 280ms);
}
/* The two entry paths come in from their own sides; stacked, they rise. */
@media (min-width: 640px) {
.js [data-reveal="pair"].reveal-in > :first-child { --reveal-x: -28px; --reveal-y: 0px; }
.js [data-reveal="pair"].reveal-in > :last-child { --reveal-x: 28px; --reveal-y: 0px; }
/* Before/after: once Before has landed, After slides out from beneath it
into its own column, the fix drawn out from under the problem. It waits
hidden (visibility, not opacity) so it can't show through Before while
Before is still fading in; Before sits above it for the whole move. */
.js .ba-grid[data-reveal="pair"].reveal-in > .ba-panel--after {
animation: ba-slide-out 980ms var(--reveal-ease) 420ms backwards;
}
}
.js [data-reveal="rows"].reveal-in tbody tr {
animation: reveal-rise 560ms var(--reveal-ease) backwards;
animation-delay: calc(var(--reveal-i, 0) * 55ms + 120ms);
--reveal-y: 6px;
}
.js [data-reveal="photo"].reveal-in {
animation: reveal-fade 900ms ease-out backwards;
}
.js [data-reveal="photo"].reveal-in img {
animation: reveal-zoom 1400ms var(--reveal-ease) backwards;
}
@keyframes reveal-rise {
from { opacity: 0.01; transform: translate(var(--reveal-x, 0px), var(--reveal-y, 18px)); }
to { opacity: 1; transform: none; }
}
/* Starts exactly under Before: one column width plus the 1rem gap. */
@keyframes ba-slide-out {
from { visibility: hidden; transform: translateX(calc(-100% - 1rem)); }
to { visibility: visible; transform: none; }
}
@keyframes reveal-focus {
from { opacity: 0.01; transform: translateY(8px); filter: blur(5px); }
to { opacity: 1; transform: none; filter: none; }
}
@keyframes reveal-rule {
from { transform: scaleX(0); }
to { transform: none; }
}
@keyframes reveal-settle {
from { opacity: 0.01; transform: scale(0.8); }
to { opacity: 1; transform: none; }
}
@keyframes reveal-tick {
from { clip-path: inset(0 100% 0 0); }
to { clip-path: inset(0); }
}
@keyframes reveal-fade {
from { opacity: 0.01; }
to { opacity: 1; }
}
@keyframes reveal-zoom {
from { transform: scale(1.06); }
to { transform: none; }
}
}
/* Continuity: light↔dark blends like dusk, but only during an explicit
toggle. Where the browser has View Transitions, theme.js runs the switch
inside one and these two rules time the cross-fade; the second lets a
press through to the page while it runs, so a quick second toggle isn't
swallowed. They hold no class on purpose: nothing else on the site starts
a view transition. */
::view-transition-group(root),
::view-transition-old(root),
::view-transition-new(root) { animation-duration: 280ms; }
::view-transition { pointer-events: none; }
/* The fallback for browsers without them: the .theme-transition class is
added for ~320ms by theme.js. Every element animating its own colors is
far more work per frame, which is why it is only the fallback.
Reduced-motion visitors get the instant swap they had before. */
@media (prefers-reduced-motion: no-preference) {
.theme-transition, .theme-transition * {
transition: background-color 280ms ease, border-color 280ms ease,
color 280ms ease !important;
}
.theme-switch-ico { transition: color 0.2s ease, transform 260ms cubic-bezier(0.16, 1, 0.3, 1); }
/* The crossfade above flattens every transition to color-only while it
runs, which would snap the switch knob and the header orb's swipe. */
.theme-transition :is(.theme-switch-knob, .theme-orb-ico) {
transition: left 0.24s cubic-bezier(0.16, 1, 0.3, 1),
transform 320ms cubic-bezier(0.16, 1, 0.3, 1),
background-color 280ms ease, color 280ms ease !important;
}
}
.theme-switch:active .theme-switch-ico { transform: rotate(-20deg); }
.framed {
border-radius: theme('borderRadius.card');
border: 1px solid var(--line);
box-shadow: theme('boxShadow.lift');
overflow: hidden;
background: var(--surface);
}
/* Soft-edged photo that sits beside the "what is an advocate" intro copy.
The radial mask feathers every edge so it reads as atmosphere behind the
text rather than a placed, framed asset. It floats right at every width so
the copy wraps around it; on extremely narrow screens there is no room for
a readable text column beside it, so it is hidden. .what-intro carries the
clearfix. */
.what-intro::after { content: ""; display: block; clear: both; }
.vignette-figure {
float: right;
width: 44%;
max-width: 288px;
margin: 0.25rem 0 1rem 1.25rem;
}
/* Sizing/loading is handled the hero way — a <picture> with 640w/1024w
sources and sizes="(min-width:1024px) 40vw, 100vw", plus .block/.h-auto/
.w-full utilities on the <img>. This rule only adds the vignette skin. */
.vignette-figure img {
border-radius: theme('borderRadius.card');
-webkit-mask-image: radial-gradient(82% 96% at 52% 44%, #000 38%, transparent 100%);
mask-image: radial-gradient(82% 96% at 52% 44%, #000 38%, transparent 100%);
}
/* Extremely narrow: drop it — a floated image would leave the copy in a
column too thin to read. */
@media (max-width: 400px) {
.vignette-figure { display: none; }
}
@media (min-width: 768px) {
.vignette-figure { width: 288px; margin: 0.25rem 0 1rem 2rem; }
}
@media (min-width: 1024px) {
.vignette-figure { width: 340px; max-width: none; margin-left: 2.5rem; margin-bottom: 2rem; }
}
/* From 1024px the photo stands 1rem taller than the copy beside it, which left
it half the air above the callout that the copy has. The callout clears the
float, so the photo's own 2rem bottom margin is what holds it off. */
.what-intro > .callout { clear: both; }
/* --- Path cards (the two entry paths) --- */
.path-card { display: flex; flex-direction: column; gap: 1rem; padding: 1.75rem; height: 100%; position: relative; }
/* Calm full-bleed photo behind the whole card under a heavy surface wash,
so navy body copy keeps its contrast. Decorative; images are same-origin. */
.path-card::before {
content: "";
position: absolute;
inset: 0;
border-radius: theme('borderRadius.card');
background:
linear-gradient(
to bottom,
color-mix(in srgb, var(--surface) 68%, transparent) 0%,
color-mix(in srgb, var(--surface) 80%, transparent) 42%,
color-mix(in srgb, var(--surface) 90%, transparent) 100%
),
var(--path-card-bg, none) center / cover no-repeat;
z-index: 0;
pointer-events: none;
}
.path-card > * { position: relative; z-index: 1; }
/* Path CTAs read as light blue at rest in both themes, matching the
"Get Started" tab; hover deepens to navy in light mode and lifts to
white in dark mode. */
.path-card .btn-ghost {
background: var(--sky);
color: var(--navy);
border-color: var(--sky);
}
.path-card .btn-ghost:hover {
background: var(--accent-hover-bg);
color: var(--accent-hover-ink);
border-color: var(--accent-hover-bg);
}
/* JPEG is the safe fallback; @supports upgrades to AVIF/WebP where the
image-set() type() syntax is understood (Chrome 113+, Safari 17+, FF 89+). */
.path-card--crisis { --path-card-bg: url('../img/path-crisis-960.jpg'); }
.path-card--referrer { --path-card-bg: url('../img/path-referrer-960.jpg'); }
@supports (background-image: image-set(url('x.webp') type('image/webp'))) {
.path-card--crisis {
--path-card-bg: image-set(
url('../img/path-crisis-960.avif') type('image/avif'),
url('../img/path-crisis-960.webp') type('image/webp'),
url('../img/path-crisis-960.jpg') type('image/jpeg')
);
}
.path-card--referrer {
--path-card-bg: image-set(
url('../img/path-referrer-960.avif') type('image/avif'),
url('../img/path-referrer-960.webp') type('image/webp'),
url('../img/path-referrer-960.jpg') type('image/jpeg')
);
}
}
.path-card::before { opacity: var(--path-photo-opacity); }
@media (prefers-contrast: more) {
.path-card::before { display: none; }
}
/* --- How it works: pinned note-cards on ruled paper, joined by a
marching-ants dashed thread (adapted from a 21st.dev "how-it-works") --- */
.step-num {
font-family: theme('fontFamily.logo');
font-size: 2.75rem;
line-height: 1;
font-weight: 400;
letter-spacing: 0.02em;
color: var(--how-ink, var(--sky-ink));
}
.how-flow { position: relative; margin-top: 2.5rem; }
/* faint notebook rule behind the cards, faded out at both edges */
.how-flow::before {
content: "";
position: absolute;
inset: -1.5rem 0;
background-image: linear-gradient(var(--ink) 1px, transparent 1px);
background-size: 100% 2rem;
opacity: 0.05;
pointer-events: none;
-webkit-mask-image: linear-gradient(90deg, transparent, #000 14%, #000 86%, transparent);
mask-image: linear-gradient(90deg, transparent, #000 14%, #000 86%, transparent);
}
.how-steps {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 1.75rem;
}
/* per-step accent + inner-panel wash: palette tints mixed 9–12% into the surface */
/* --how-accent tints pins + borders; --how-ink is the contrast-safe step for the numerals. */
.how-steps > :nth-child(1) { --how-accent: var(--sky); --how-ink: var(--sky-ink); --how-wash: color-mix(in srgb, var(--sky) 12%, var(--surface)); }
.how-steps > :nth-child(2) { --how-accent: var(--seagrass); --how-ink: var(--seagrass-ink); --how-wash: color-mix(in srgb, var(--seagrass) 12%, var(--surface)); }
.how-steps > :nth-child(3) { --how-accent: var(--lantern); --how-ink: var(--lantern-ink); --how-wash: color-mix(in srgb, var(--lantern) 12%, var(--surface)); }
.how-steps > :nth-child(4) { --how-accent: var(--coral); --how-ink: var(--coral-ink); --how-wash: color-mix(in srgb, var(--coral) 9%, var(--surface)); }
.how-step { position: relative; width: 100%; max-width: 22rem; }
/* white "photo" frame around a tinted inner panel */
.how-card {
position: relative;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 22px;
padding: 0.5rem;
box-shadow: theme('boxShadow.lift');
transition: rotate 0.25s cubic-bezier(0.16, 1, 0.3, 1),
scale 0.25s cubic-bezier(0.16, 1, 0.3, 1);
}
.how-card-inner {
background: var(--how-wash, var(--tag-bg));
border: 1px solid color-mix(in srgb, var(--how-accent, var(--sky)) 30%, var(--line));
border-radius: 15px;
padding: 1.15rem 1.25rem 1.35rem;
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.how-card-inner h3 { margin-top: 0.15rem; }
/* pushpin straddling the top edge of the frame */
.how-pin {
position: absolute;
top: -0.85rem;
left: 50%;
transform: translateX(-50%);
width: 2rem;
height: 2rem;
display: grid;
place-items: center;
color: var(--how-accent, var(--sky));
filter: drop-shadow(0 2px 3px color-mix(in srgb, var(--ink) 22%, transparent));
}
.how-pin svg { width: 100%; height: 100%; }
/* connector: dashed path stretched over the flow (preserveAspectRatio="none",
non-scaling-stroke keeps the dash rhythm even under the non-uniform stretch) */
.how-spine { display: none; }
.how-spine path {
fill: none;
stroke: var(--sky);
stroke-width: 2.5;
stroke-linecap: round;
vector-effect: non-scaling-stroke;
}
/* Resting / no-JS state: the marching dashes, exactly as before. */
.how-weave-line { stroke-dasharray: 7 7; opacity: 0.45; }
/* The continuous trace is only used for the JS-driven draw-on (below). */
.how-weave-trace { display: none; }
@media (min-width: 768px) {
.how-steps {
display: grid;
grid-template-columns: 1fr 1fr;
column-gap: 2.5rem;
row-gap: 2rem;
align-items: start;
max-width: 40rem;
margin-inline: auto;
}
.how-step { max-width: 21rem; }
/* cards pushed to the outer edges; the thread weaves the gap between them */
.how-step:nth-child(1) { grid-area: 1 / 1; justify-self: start; }
.how-step:nth-child(2) { grid-area: 2 / 2; justify-self: end; }
.how-step:nth-child(3) { grid-area: 3 / 1; justify-self: start; }
.how-step:nth-child(4) { grid-area: 4 / 2; justify-self: end; }
/* a loose, hand-pinned tilt that straightens on hover */
.how-step:nth-child(odd) .how-card { rotate: -2.5deg; }
.how-step:nth-child(even) .how-card { rotate: 2.5deg; }
.how-card:hover { rotate: 0deg; scale: 1.03; z-index: 3; }
.how-spine {
display: block;
position: absolute;
inset: 0;
width: 100%;
height: 100%;
overflow: visible;
z-index: 0;
pointer-events: none;
}
}
/* Marching-ants thread + a one-shot lift-in when the section scrolls into
view. .js-draw is added by JS only (and never under reduced motion), so
with no JS everything shows in its finished state. */
@media (prefers-reduced-motion: no-preference) {
.how-weave-line { animation: how-march 2.6s linear infinite; }
@keyframes how-march { to { stroke-dashoffset: -56; } }
/* With JS driving the section, the thread is drawn between the pinned cards
as they lift in, then hands over to the marching rhythm. The loop only
runs while the flow is on screen — nothing marches out of view. */
.how-flow.js-draw .how-weave-line { opacity: 0; animation-play-state: paused; }
.how-flow.js-draw .how-weave-trace {
display: block;
opacity: 0.5;
stroke-dasharray: 100;
stroke-dashoffset: 100;
}
.how-flow.js-draw.is-drawn .how-weave-trace {
stroke-dashoffset: 0;
opacity: 0;
transition: stroke-dashoffset 1100ms cubic-bezier(0.16, 1, 0.3, 1),
opacity 450ms ease 1050ms;
}
.how-flow.js-draw.is-drawn .how-weave-line {
opacity: 0.45;
transition: opacity 500ms ease 900ms;
}
.how-flow.js-draw.is-drawn.is-onscreen .how-weave-line { animation-play-state: running; }
.how-flow.js-draw .how-step { opacity: 0; transform: translateY(14px); }
.how-flow.js-draw.is-drawn .how-step {
opacity: 1;
transform: none;
transition: opacity 0.5s ease, transform 0.6s cubic-bezier(0.16, 1, 0.3, 1);
}
.how-flow.js-draw.is-drawn .how-step:nth-child(2) { transition-delay: 0.1s; }
.how-flow.js-draw.is-drawn .how-step:nth-child(3) { transition-delay: 0.2s; }
.how-flow.js-draw.is-drawn .how-step:nth-child(4) { transition-delay: 0.3s; }
}
/* --- FAQ accordion --- */
/* Two-column split: the heading + a "more questions" note held on the left,
a single stacked accordion of hairline-divided rows on the right. Heading
and question type stay on the standard scale (H2 / H3). */
.faq-layout { display: grid; gap: 2rem; }
@media (min-width: 900px) {
.faq-layout {
grid-template-columns: minmax(0, 16rem) minmax(0, 1fr);
gap: clamp(2.5rem, 1rem + 8vw, 7rem);
align-items: start;
}
.faq-aside { position: sticky; top: calc(var(--header-h) + 1.5rem); }
}
.faq-more { margin-top: 1rem; max-width: 32ch; color: var(--ink-soft); }
.faq-list { display: flex; flex-direction: column; }
.faq-group-h {
margin: 2rem 0 0.25rem;
font-family: var(--font-sans);
font-size: 0.8rem;
font-weight: 400; /* Questrial ships 400 only — no faux bold */
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--ink-soft);
}
.faq-group-h:first-child { margin-top: 0; }
/* Each row is a native <details>: the browser shows and hides the answer on
the summary's click, Enter or Space, with no script and no animation. */
.faq-item { border-top: 1px solid var(--line); }
.faq-item:last-child { border-bottom: 1px solid var(--line); }
.faq-trigger {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1.25rem;
padding: 1.25rem 0;
font-family: theme('fontFamily.label');
font-size: theme('fontSize.h4');
letter-spacing: 0.005em;
line-height: 1.3;
color: var(--ink);
cursor: pointer;
list-style: none; /* no disclosure triangle: display: flex already drops it in Chromium and Firefox */
}
.faq-trigger::-webkit-details-marker { display: none; } /* Safari draws it separately */
/* The question stays a real h4, so the heading outline keeps it, but takes
its color from the summary: the base heading rule would pin it to --ink
and lose the hover. */
.faq-q { color: inherit; }
/* --link, not Sky: Sky text on the light ground is only 2.4:1. */
.faq-trigger:hover { color: var(--link); }
.faq-trigger .faq-sign {
flex: none;
margin-top: 0.1rem;
transition: transform 0.22s cubic-bezier(0.16, 1, 0.3, 1);
}
/* The sign carries both symbols and [open] picks one: a plus closed, a minus
open. A half turn on the swap makes the change read as a morph rather than
a cut, and a minus is unchanged by 180deg so it still lands flat. */
.faq-item:not([open]) .faq-sign-minus,
.faq-item[open] .faq-sign-plus { display: none; }
.faq-item[open] .faq-sign { transform: rotate(180deg); }
.faq-panel {
max-width: theme('maxWidth.prose');
color: var(--ink-soft);
}
.faq-panel > * { padding-bottom: 1.5rem; }
/* --- "Get Started" drawer tab: fixed to the right edge, vertical text,
opens the contact drawer. z-index sits below #contact-backdrop (90), so
the drawer's backdrop covers it while the drawer is open. --- */
.drawer-tab {
position: fixed;
right: 0;
top: 50%;
transform: translateY(-50%);
z-index: 34;
display: flex;
align-items: center;
justify-content: center;
padding: 1.1rem 0.55rem;
border: 1px solid var(--sky);
border-right: 0;
border-radius: theme('borderRadius.card') 0 0 theme('borderRadius.card');
background: var(--sky);
color: var(--navy);
font-family: theme('fontFamily.label');
font-size: 0.9375rem;
letter-spacing: 0.1em;
text-transform: uppercase;
writing-mode: vertical-rl;
cursor: pointer;
box-shadow: -2px 2px 16px rgba(10, 15, 36, 0.22);
transition: transform 0.22s theme('transitionTimingFunction.calm'),
opacity 0.22s ease, visibility 0.22s,
background-color 0.18s ease, color 0.18s ease;
}
/* Light blue at rest in both themes; hover deepens to navy in light mode
and lifts to white in dark mode. */
.drawer-tab:hover {
background: var(--accent-hover-bg);
color: var(--accent-hover-ink);
border-color: var(--accent-hover-bg);
}
/* --ring is Navy on light / Sky on dark: ≥3:1 against the page either way. */
.drawer-tab:focus-visible { outline: 3px solid var(--ring); outline-offset: 3px; }
/* Tucked away above the Services section and once the in-page intake
form (or the closing region) is on screen. `.tab-wait` holds it tucked from
first paint until main.js has measured the page (JS only, so no-JS keeps it). */
.drawer-tab.is-tucked,
.js .drawer-tab.tab-wait {
transform: translateY(-50%) translateX(110%);
opacity: 0;
visibility: hidden;
pointer-events: none;
}
/* Phones: a right-edge tab would sit over body copy inside the 1.5rem gutter,
so the CTA becomes a full-width bar pinned to the bottom of the viewport
(the brief's "CTA pinned to the bottom on scroll"). */
@media (max-width: 479px) {
.drawer-tab {
top: auto;
bottom: 0;
left: 0;
transform: none;
writing-mode: horizontal-tb;
min-height: 3.25rem;
padding: 0.85rem 1rem calc(0.85rem + env(safe-area-inset-bottom));
border: 0;
border-top: 1px solid color-mix(in srgb, var(--navy) 25%, var(--sky));
border-radius: 0;
font-size: 0.9375rem;
box-shadow: 0 -4px 16px rgba(10, 15, 36, 0.14);
}
.drawer-tab.is-tucked,
.js .drawer-tab.tab-wait { transform: translateY(110%); }
.drawer-tab:focus-visible { outline-offset: -5px; }
/* keep the footer's last lines clear of the bar when it is showing */
.site-footer { padding-bottom: 3.25rem; }
}
/* --- Contact drawer (right-anchored slide-out) ---
Opened with dialog.show(), not showModal(): a showModal() dialog (and its
::backdrop) render in the browser's top layer, which paints above *every*
normal stacking context regardless of z-index -- and that hid the very-
high-z-index challenge overlay Google's reCAPTCHA used to append to <body>
for an image challenge. The form now uses Cloudflare Turnstile, whose
challenge stays inside the widget, so that constraint is gone; the plain
fixed-position dialog + real sibling backdrop element (below) stay because
they work and are tested. modal.js handles the inert-background, focus-
trap, and Escape/outside-click behavior that showModal() would give for
free. */
#contact-modal {
position: fixed;
z-index: 91;
inset: 0 0 0 auto;
width: min(30rem, 100vw);
max-width: 100vw;
height: 100dvh;
max-height: 100dvh;
margin: 0;
padding: 0;
border: 0;
border-left: 1px solid var(--line);
border-radius: 0;
background: var(--bg);
color: var(--ink);
overflow: hidden;
}
#contact-backdrop {
position: fixed;
inset: 0;
z-index: 90;
background: rgba(10, 15, 36, 0.55);
backdrop-filter: blur(2px);
}
#contact-backdrop[hidden] { display: none; }
#contact-modal[open] { animation: drawer-in 300ms cubic-bezier(0.16, 1, 0.3, 1) both; }
#contact-backdrop:not([hidden]) { animation: backdrop-in 220ms ease both; }
@keyframes drawer-in {
from { opacity: 0; transform: translateX(100%); }
to { opacity: 1; transform: none; }
}
@keyframes backdrop-in { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
#contact-modal[open] { animation: backdrop-in 180ms ease both; }
#contact-backdrop:not([hidden]) { animation-name: backdrop-in; }
}
/* Closing: modal.js adds .is-closing and only closes the dialog once this
finishes. The drawer slides back out the edge it came from, quicker than it
arrived and accelerating away; the backdrop fades with it and stops catching
clicks, since the page behind is already live again. */
#contact-modal.is-closing[open] { animation: drawer-out 220ms cubic-bezier(0.7, 0, 0.84, 0) both; }
#contact-backdrop.is-closing:not([hidden]) {
animation: backdrop-out 220ms ease both;
pointer-events: none;
}
@keyframes drawer-out {
from { opacity: 1; transform: none; }
to { opacity: 0; transform: translateX(100%); }
}
@keyframes backdrop-out { from { opacity: 1; } to { opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
#contact-modal.is-closing[open] { animation: backdrop-out 150ms ease both; }
}
/* --- Lightbox variant: the same dialog, centered instead of edge-anchored.
A `data-variant="lightbox"` open button adds `.is-lightbox` (see modal.js). --- */
#contact-modal.is-lightbox {
inset: 0;
margin: auto;
width: min(34rem, 100vw - 2rem);
height: max-content;
max-height: calc(100dvh - 2rem);
border: 1px solid var(--line);
border-radius: theme('borderRadius.card');
}
#contact-modal.is-lightbox .modal-scroll {
height: auto;
max-height: calc(100dvh - 2rem);
}
#contact-modal.is-lightbox[open] {
animation: lightbox-in 240ms cubic-bezier(0.16, 1, 0.3, 1) both;
}
@keyframes lightbox-in {
from { opacity: 0; transform: translateY(8px) scale(0.98); }
to { opacity: 1; transform: none; }
}
#contact-modal.is-lightbox.is-closing[open] {
animation: lightbox-out 180ms cubic-bezier(0.7, 0, 0.84, 0) both;
}
@keyframes lightbox-out {
from { opacity: 1; transform: none; }
to { opacity: 0; transform: translateY(8px) scale(0.98); }
}
@media (prefers-reduced-motion: reduce) {
#contact-modal.is-lightbox[open] { animation: backdrop-in 180ms ease both; }
#contact-modal.is-lightbox.is-closing[open] { animation: backdrop-out 150ms ease both; }
}
.modal-scroll { overflow-y: auto; height: 100dvh; max-height: 100dvh; padding: 1.75rem; }
/* Pinned to the dialog, not the scroller, so it stays put while the content
scrolls beneath it. z-index 1 keeps it above that content (a hovered .btn's
transform makes its own stacking context and would otherwise paint over it)
but under .modal-confirm (z-index 2), which covers it while asking. */
.modal-close {
position: absolute;
z-index: 1;
top: 0.75rem;
right: 0.75rem;
box-shadow: 0 1px 6px rgba(10, 15, 36, 0.16);
background: var(--tag-bg);
border: 1px solid var(--line);
border-radius: 999px;
width: 2.75rem;
height: 2.75rem;
display: grid;
place-items: center;
cursor: pointer;
color: var(--ink);
}
.modal-close:hover { background: var(--line-soft); }
/* The contact drawer (not the centered lightbox) and the mobile nav re-point
the semantic tokens to the --panel-* set (see the #mobile-nav rule); in dark
that inverts both to a Sky ground with a Navy CTA. */
/* --- Forms --- */
.field { display: block; margin-top: 1rem; }
.field-label {
display: block;
font-family: theme('fontFamily.label');
font-size: 0.9375rem;
letter-spacing: 0.01em;
color: var(--ink);
margin-bottom: 0.35rem;
}
.field-hint { display: block; font-size: 0.875rem; color: var(--ink-soft); margin-top: 0.3rem; }
.req { color: var(--ink); font-weight: 600; } /* Ink, so Coral keeps one meaning (errors) */
.input,
.textarea,
.select {
width: 100%;
font: inherit;
font-size: 1rem;
color: var(--ink);
background: var(--surface);
border: 1px solid var(--ink-soft);
border-radius: 0.5rem;
padding: 0.7rem 0.8rem;
min-height: 48px;
}
.textarea { min-height: 7rem; resize: vertical; }
.input:focus, .textarea:focus, .select:focus { border-color: var(--ink); }
.input[aria-invalid="true"],
.textarea[aria-invalid="true"],
.select[aria-invalid="true"] { border-color: var(--coral); border-width: 2px; }
/* Error text stays --ink for AA; the coral border and the leading triangle carry
the "error" signal visually. For assistive tech it's aria-invalid on the field
plus this element's id joining the field's aria-describedby (contact-form.js),
and focus moving to the error summary on a failed submit. */
.field-error { display: none; color: var(--ink); font-family: theme('fontFamily.mono'); font-size: 0.875rem; margin-top: 0.3rem; font-weight: 500; }
.field-error:not([hidden]) { display: block; }
.field-error:not([hidden])::before { content: "\25B2\00A0"; color: var(--coral); }
.choice {
display: flex;
gap: 0.6rem;
align-items: flex-start;
padding: 0.5rem 0;
font-size: 1rem;
}
.choice input { margin-top: 0.2rem; width: 1.15rem; height: 1.15rem; flex: none; accent-color: var(--choice-accent); }
/* An empty checkbox is drawn from the text fields' own tokens. The browser's,
under color-scheme: dark, is a flat neutral grey that reads as disabled on
Navy. Once ticked it is the native control again, tinted by accent-color. */
.choice input[type="checkbox"]:not(:checked) {
appearance: none;
background: var(--surface);
border: 1px solid var(--ink-soft);
border-radius: 3px;
}
/* The same 2px Coral border an empty required field gets. */
.choice input[type="checkbox"][aria-invalid="true"]:not(:checked) { border: 2px solid var(--coral); }
fieldset { border: 0; padding: 0; margin: 0; }
/* Cloudflare's Turnstile widget (contact-form.js renders it "flexible":
full width, never under 300px, 65px tall -- or "compact", 150x140, where
the card is narrower than 300px). Reserve the height so the card doesn't
jump a second time when the iframe arrives, and let it scroll rather than
push the card wider if it still doesn't fit. The field is authored hidden
and appears on the visitor's first edit; .field's display: block sits in
this layer and so outranks Preflight's [hidden], hence the explicit rule. */
[data-turnstile-field][hidden] { display: none; }
[data-turnstile-field] { overflow-x: auto; }
[data-turnstile] { min-height: 65px; }
.hp-field {
position: absolute !important;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden; clip: rect(0 0 0 0);
white-space: nowrap; border: 0;
}
.form-note {
font-size: 0.875rem;
color: var(--ink-soft);
background: var(--tag-bg);
border: 1px solid var(--line);
border-radius: 0.5rem;
padding: 0.75rem 0.9rem;
}
.error-summary {
border: 2px solid var(--coral);
background: color-mix(in srgb, var(--coral) 10%, var(--surface));
color: var(--ink);
font-family: theme('fontFamily.mono');
font-size: 0.875rem; /* Caption size, same as .field-error — the Readable-First floor */
border-radius: 0.5rem;
padding: 0.9rem 1rem;
margin-top: 1.25rem;
margin-bottom: 1rem;
}
.error-summary a { color: var(--ink); }
.error-summary strong { font-weight: 500; } /* Plex Mono ships 400/500 — no faux bold */
/* --- Site preferences dialog: theme, text size, animations, reader mode,
analytics. Built by prefs.js and opened with showModal(), so the browser
supplies the backdrop, the inert page and Escape. Centered like the
contact lightbox, on the page's own tokens. --- */
.prefs-dialog {
width: min(32rem, 100vw - 2rem);
max-height: calc(100dvh - 2rem);
margin: auto;
padding: 0;
border: 1px solid var(--line);
border-radius: theme('borderRadius.card');
background: var(--bg);
color: var(--ink);
box-shadow: 0 18px 44px -14px rgba(10, 15, 36, 0.32);
overflow: hidden;
}
.prefs-dialog::backdrop {
background: rgba(10, 15, 36, 0.55);
backdrop-filter: blur(2px);
}
.prefs-dialog[open] { animation: lightbox-in 240ms cubic-bezier(0.16, 1, 0.3, 1) both; }
.prefs-dialog[open]::backdrop { animation: backdrop-in 220ms ease both; }
@media (prefers-reduced-motion: reduce) {
.prefs-dialog[open] { animation: backdrop-in 180ms ease both; }
}
html:has(.prefs-dialog[open]) { overflow: hidden; }
.prefs-body {
max-height: calc(100dvh - 2rem);
overflow-y: auto;
padding: 1.25rem;
}
@media (min-width: 480px) { .prefs-body { padding: 1.75rem; } }
/* The heading repeats the gear that opened the dialog. */
.prefs-title { display: flex; align-items: center; gap: 0.6rem; }
.prefs-title svg { flex: none; width: 1.1em; height: 1.1em; color: var(--sky-ink); }
.prefs-lede { margin-top: 0.5rem; font-size: 1rem; color: var(--ink-soft); }
.prefs-group { margin-top: 1.5rem; }
.prefs-label {
display: block;
font-family: theme('fontFamily.label');
font-size: 1rem;
letter-spacing: 0.02em;
color: var(--ink);
}
.prefs-hint { margin-top: 0.15rem; font-size: 0.9375rem; line-height: 1.45; color: var(--ink-soft); }
/* Segmented radios: the input stays in the tab order (arrow keys move
between options) but is drawn by its label. */
.prefs-seg {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.25rem;
margin-top: 0.6rem;
padding: 0.25rem;
border: 1px solid var(--line);
border-radius: theme('borderRadius.card');
background: var(--surface);
}
.prefs-seg-option {
position: relative;
display: grid;
place-items: center;
min-height: 44px;
padding: 0.4rem 0.75rem;
border-radius: 8px;
font-size: 1rem;
text-align: center;
overflow-wrap: anywhere;
color: var(--ink);
cursor: pointer;
transition: background-color 0.18s ease, color 0.18s ease;
}
.prefs-seg-option input {
position: absolute;
inset: 0;
opacity: 0;
margin: 0;
cursor: pointer;
}
.prefs-seg-option:hover { background: var(--tag-bg); }
.prefs-seg-option:has(input:checked) { background: var(--cta-bg); color: var(--cta-ink); }
.prefs-seg-option:has(input:focus-visible) {
outline: 3px solid var(--ring);
outline-offset: 2px;
}
/* Text size: 2 × 2 on phones, one row from 480px. */
.prefs-seg--text { grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (min-width: 480px) { .prefs-seg--text { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
/* Each option previews its own step (the root scale on :root[data-text]). */
.prefs-seg--text [data-value="lg"] { font-size: 1.125rem; }
.prefs-seg--text [data-value="xl"] { font-size: 1.25rem; }
.prefs-seg--text [data-value="xxl"] { font-size: 1.5rem; line-height: 1.2; }
.prefs-rows { margin-top: 1.25rem; border-top: 1px solid var(--line-soft); }
.prefs-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding-block: 0.9rem;
border-bottom: 1px solid var(--line-soft);
}
/* When the label and switch can't share a line (a narrow phone at the Largest
text size), the switch drops below the text instead of squeezing it. */
.prefs-row { flex-wrap: wrap; }
.prefs-row-text { flex: 1 1 10rem; }
.prefs-row .prefs-label { cursor: pointer; }
/* On/off switch: a native checkbox (role="switch") drawn as a pill, in the
same track + knob shape as the footer's theme switch. */
.prefs-switch {
appearance: none;
flex: none;
position: relative;
width: 3.25rem;
height: 1.9rem;
margin: 0;
border: 1px solid var(--ink-soft);
border-radius: 999px;
background: var(--line-soft);
cursor: pointer;
transition: background-color 0.18s ease, border-color 0.18s ease;
}
.prefs-switch::before {
content: "";
position: absolute;
top: 50%;
left: 0.2rem;
width: 1.35rem;
height: 1.35rem;
border-radius: 999px;
background: var(--ink-soft);
transform: translateY(-50%);
transition: left 220ms cubic-bezier(0.16, 1, 0.3, 1), background-color 0.18s ease;
}
.prefs-switch:checked { background: var(--cta-bg); border-color: var(--cta-bg); }
.prefs-switch:checked::before { left: calc(100% - 1.35rem - 0.2rem); background: var(--cta-ink); }
.prefs-switch:disabled { opacity: 0.55; cursor: not-allowed; }
.prefs-switch:focus-visible { outline: 3px solid var(--ring); outline-offset: 2px; }
.prefs-actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 0.75rem;
margin-top: 1.5rem;
}
.prefs-actions .btn { flex: 1 1 auto; }
@media (min-width: 480px) { .prefs-actions .btn { flex: 0 0 auto; } }
/* --- Reader mode (Site preferences → data-reader="on" on <html>) ---
One plain column for reading: every layout grid in <main> collapses to a
single track in a narrower measure, decoration (mazes, the How-it-works
thread and pins, the lit beacon, the hero glow) is removed, tinted section
grounds and card shadows go flat, and the leading opens up. Content photos
are hidden too, same as the aria-hidden treatment they get for Safari's
built-in Reader View — this mode should read like a plain HTML page, text
only. The navy Schedule band keeps its ground, since its type is set for
it. The header, footer and pinned CTA are untouched. */
:root[data-reader="on"] main :is(.framed, .vignette-figure, .hero-byline-frame) {
display: none !important;
}
:root[data-reader="on"] main { line-height: 1.75; }
:root[data-reader="on"] main :is(.canvas, .canvas-wide) { max-width: 44rem; }
/* The style guide's shell: one column in the same measure, contents on top. */
:root[data-reader="on"] main .sg-shell { grid-template-columns: minmax(0, 1fr) !important; max-width: 44rem; }
:root[data-reader="on"] main .sg-toc { position: static; max-height: none; overflow: visible; }
:root[data-reader="on"] main :is(.grid, .grid-9, .service-rows, .who-list, .price-choices,
.how-steps, .faq-layout, .fineprint-body, .ba-grid) {
grid-template-columns: minmax(0, 1fr) !important;
}
/* Same phone treatment as the ba-grid's own narrow breakpoint: Before shows
titles only, so the stacked panels reach the answer sooner, and the two
panels drop the side-by-side subgrid row alignment (a >=640px-only rule
that would otherwise still apply once the columns above collapse to one). */
:root[data-reader="on"] main .ba-panel--before .ba-desc { display: none; }
:root[data-reader="on"] main :is(.ba-panel, .ba-list) {
grid-row: auto !important;
grid-template-rows: none !important;
}
:root[data-reader="on"] main :is(.grid, .grid-9, .how-steps, .who-list) > * {
grid-column: auto !important;
grid-row: auto !important;
justify-self: stretch !important;
}
:root[data-reader="on"] main :is(.maze-hero, .lost-maze, .maze-cross, .how-spine, .how-pin, .beacon-lit) {
display: none !important;
}
:root[data-reader="on"] main :is(.hero-atmos, .how-flow, .trust-stack)::before { content: none; }
:root[data-reader="on"] main .trust-stack :is(.trust-stack-title, .trust-item strong, .trust-item p) {
text-shadow: none;
}
:root[data-reader="on"] main .how-step { max-width: none; }
:root[data-reader="on"] main .service-row { background: none; }
/* The path cards' photo wash is decoration. */
:root[data-reader="on"] main .path-card::before { background: var(--surface); }
/* The hero keeps extra bottom room for its glow; use the normal rhythm. */
:root[data-reader="on"] #top > .canvas.section { padding-bottom: clamp(2.25rem, 1.5rem + 3.25vw, 4rem); }
/* The "What" photo floats beside the copy, and the value list (its own flex
box) squeezes into the leftover strip. Stack it above the text instead. */
:root[data-reader="on"] main .vignette-figure {
float: none;
width: 100%;
max-width: 20rem;
margin: 0 0 1.5rem;
}
:root[data-reader="on"] main .faq-more { max-width: none; }
/* Sticky side rails would slide over the stacked copy below them. */
:root[data-reader="on"] main :is(.about-rail, .faq-aside) { position: static !important; }
/* Their desktop column gaps become row gaps once stacked. */
:root[data-reader="on"] main :is(.faq-layout, .fineprint-body) { gap: 2rem; }
:root[data-reader="on"] main .section:not(.on-dark) {
background: var(--bg) !important;
background-image: none !important;
}
:root[data-reader="on"] main :is(.card, .tile, .path-card, .how-card, .how-card-inner, .price-choice, .framed) {
box-shadow: none !important;
transform: none !important;
rotate: none !important;
}
:root[data-reader="on"] main .faq-aside { position: static; }
:root[data-reader="on"] main .ba-list::before { display: none; }
:root[data-reader="on"] main :is(p, li) { text-align: left; }
/* --- Footer — Sky ground with Navy ink in light, Midnight with Paper in dark
(the --footer-* tokens). Dark adds a divider so it separates from the
near-identical page background. Logo and GNA badge ship in both colorways,
toggled with .theme-icon-light / .theme-icon-dark. --- */
.site-footer { background: var(--footer-bg); color: var(--footer-ink); }
.site-footer { border-top: 1px solid var(--footer-edge); }
.site-footer a { color: var(--footer-ink); text-decoration: none; }
.site-footer a:not([class]) { color: var(--footer-link); }
.site-footer a:hover:not(.brand-lockup) { text-decoration: underline; }
/* The page's Sky-tint selection vanishes on the Sky ground and barely shows on
Midnight, so the footer selects in its accent pair: Navy with Paper in light,
Sky with Navy in dark. */
.site-footer::selection,
.site-footer ::selection { background: var(--footer-accent); color: var(--footer-accent-ink); }
/* The builder credit is the one footer link underlined at rest. */
.site-footer .footer-credit a {
text-decoration: underline;
text-decoration-color: color-mix(in srgb, currentColor 45%, transparent);
text-underline-offset: 0.18em;
}
.site-footer .footer-credit a:hover { text-decoration-color: currentColor; }
.footer-col-title {
font-family: theme('fontFamily.label');
font-size: 0.8125rem;
line-height: inherit;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--footer-title);
margin-bottom: 0.75rem;
}
.footer-dim { color: var(--footer-dim); }
.footer-rule { border-color: color-mix(in srgb, var(--footer-ink) 15%, transparent); }
.footer-social { border-color: color-mix(in srgb, var(--footer-ink) 20%, transparent); }
.footer-social:hover { border-color: color-mix(in srgb, var(--footer-ink) 60%, transparent); }
/* QR code to the site — paper only (see @media print). A display:none <img>
still downloads, so it's ready when the print dialog opens. */
.footer-qr { display: none; }
/* GNA member badge. Uses single-color lockups derived from
assets/gna-logo-w-tagline.svg — gna-logo-navy.svg on the light Sky footer,
gna-logo-white.svg on the dark Midnight one. */
.gna-badge {
display: inline-flex;
align-items: center;
max-width: 100%;
}
/* Print swaps in the primary mark and the full-color GNA badge. Browsers
don't wait for images first requested by print styles, so warm both here. */
body::after {
content: url('../logomarks/marks/advocate-beacon-primary.svg') url('../img/gna-logo-w-tagline.svg');
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
opacity: 0;
pointer-events: none;
}
/* The footer's Site preferences gear (prefs.js) sits beside the theme switch
in the social row, drawn like the social circles but with the switch
track's tint so the two controls read as one pair. Needs JS, like the dialog. */
.footer-gear {
color: var(--footer-ink);
background: color-mix(in srgb, var(--footer-ink) 14%, transparent);
cursor: pointer;
}
html:not(.js) .footer-gear { display: none; }
@media (prefers-reduced-motion: no-preference) {
.footer-gear svg { transition: transform 420ms cubic-bezier(0.16, 1, 0.3, 1); }
.footer-gear:hover svg { transform: rotate(60deg); }
}
/* Light/dark switch — pill track + sliding knob, in the footer and the mobile
menu. Sun (left) and moon (right) both stay on the track; the knob slides
under the active one. State is the button's aria-pressed (set by theme.js),
so the switch stays hidden until JS is on. Colors and sizes are local --sw-*
props: the footer's (--footer-* tokens) here, the menu panel's in
.theme-switch--menu, and the footer's 44px size in .theme-switch--footer. */
.theme-switch {
--sw-w: 3.5rem;
--sw-h: 1.95rem;
--sw-knob-size: 1.5rem;
--sw-inset: 0.22rem;
--sw-ico: 0.95rem;
--sw-track: color-mix(in srgb, var(--footer-ink) 14%, transparent);
--sw-track-on: color-mix(in srgb, var(--footer-accent) 26%, transparent);
--sw-border: color-mix(in srgb, var(--footer-ink) 22%, transparent);
--sw-border-hover: color-mix(in srgb, var(--footer-ink) 45%, transparent);
--sw-knob: var(--footer-accent);
--sw-on: var(--footer-accent-ink);
--sw-off: var(--footer-sw-off);
display: inline-flex;
align-items: center;
padding: 0;
border: 0;
background: none;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
html:not(.js) :is(.theme-switch, .mobile-nav-settings, .theme-orb) { display: none; }
.theme-switch-track {
position: relative;
display: block;
flex-shrink: 0;
width: var(--sw-w);
height: var(--sw-h);
border-radius: 999px;
background: var(--sw-track);
border: 1px solid var(--sw-border);
transition: background-color 0.2s ease, border-color 0.2s ease;
}
.theme-switch[aria-pressed="true"] .theme-switch-track { background: var(--sw-track-on); }
.theme-switch:hover .theme-switch-track { border-color: var(--sw-border-hover); }
.theme-switch-knob {
position: absolute;
top: 50%;
left: var(--sw-inset);
transform: translateY(-50%);
width: var(--sw-knob-size);
height: var(--sw-knob-size);
border-radius: 999px;
background: var(--sw-knob);
box-shadow: 0 1px 4px rgba(10, 15, 36, 0.45);
transition: left 0.24s cubic-bezier(0.16, 1, 0.3, 1), background 0.2s ease;
}
.theme-switch[aria-pressed="true"] .theme-switch-knob { left: calc(100% - var(--sw-knob-size) - var(--sw-inset)); }
.theme-switch-ico {
position: absolute;
top: 50%;
width: var(--sw-ico);
height: var(--sw-ico);
margin-top: calc(var(--sw-ico) / -2);
pointer-events: none;
transition: color 0.2s ease;
}
/* Centered on the knob's two resting spots. */
.theme-switch-ico--sun { left: calc(var(--sw-inset) + (var(--sw-knob-size) - var(--sw-ico)) / 2); color: var(--sw-on); }
.theme-switch-ico--moon { right: calc(var(--sw-inset) + (var(--sw-knob-size) - var(--sw-ico)) / 2); color: var(--sw-off); }
/* Footer: as tall as the 44px social circles and gear beside it, with the
same hairline border color. */
.theme-switch--footer {
--sw-w: 4.9rem;
--sw-h: 2.75rem;
--sw-knob-size: 2.15rem;
--sw-inset: 0.26rem;
--sw-ico: 1.15rem;
--sw-border: color-mix(in srgb, var(--footer-ink) 20%, transparent);
--sw-border-hover: color-mix(in srgb, var(--footer-ink) 60%, transparent);
}
.theme-switch[aria-pressed="true"] .theme-switch-ico--sun { color: var(--sw-off); }
.theme-switch[aria-pressed="true"] .theme-switch-ico--moon { color: var(--sw-on); }
@media (prefers-reduced-motion: reduce) {
.theme-switch-knob { transition-duration: 120ms; }
}
/* In the mobile menu: a full-width settings block under the links — the
theme switch, then the Site preferences row (the header's gear is
desktop-only; mobile gets this instead). Built from the panel's own
tokens, so it reads on both panel grounds (page ground in light, Sky in
dark). */
.mobile-nav-settings {
margin-top: 0.4rem;
padding-top: 0.4rem;
border-top: 1px solid var(--line-soft);
}
.theme-switch--menu {
--sw-track: color-mix(in srgb, var(--ink) 8%, transparent);
--sw-track-on: var(--sw-track);
--sw-border: var(--line);
--sw-border-hover: color-mix(in srgb, var(--ink) 45%, transparent);
--sw-knob: var(--ink);
--sw-on: var(--bg);
--sw-off: var(--ink-soft);
width: 100%;
justify-content: space-between;
gap: 1.25rem;
padding: 0.4rem 0.45rem 0.4rem 0.85rem;
border-radius: 8px; /* rounded.field */
font-family: theme('fontFamily.label');
font-size: 1rem;
letter-spacing: 0.02em;
color: var(--ink);
}
.theme-switch--menu:hover,
.theme-switch--menu:focus-visible { background: var(--tag-bg); }
/* Site preferences row: gear + label, the same row treatment as the theme
switch above it (full width, 44px target, hover/focus background). */
.mobile-nav-prefs {
display: flex;
align-items: center;
gap: 0.6rem;
width: 100%;
min-height: 44px;
margin-top: 0.15rem;
padding: 0.4rem 0.45rem 0.4rem 0.85rem;
border: 0;
border-radius: 8px; /* rounded.field */
font: inherit;
font-family: theme('fontFamily.label');
font-size: 1rem;
letter-spacing: 0.02em;
color: var(--ink);
background: none;
cursor: pointer;
text-align: left;
}
.mobile-nav-prefs svg { width: 1.15rem; height: 1.15rem; flex-shrink: 0; }
.mobile-nav-prefs:hover,
.mobile-nav-prefs:focus-visible { background: var(--tag-bg); }
html:not(.js) .mobile-nav-prefs { display: none; }
@media (prefers-reduced-motion: no-preference) {
.mobile-nav-prefs svg { transition: transform 420ms cubic-bezier(0.16, 1, 0.3, 1); }
.mobile-nav-prefs:hover svg { transform: rotate(60deg); }
}
/* Desktop header: the same toggle as a single circle (36px, a notch under the
44px header buttons, so the six links still clear the logo at the nav screen). Sun and
moon share the circle; on toggle the sun swipes out left as the moon comes in
from the right, and back. Mobile gets the menu row instead. */
.theme-orb {
position: relative;
overflow: hidden;
flex-shrink: 0;
width: 2.25rem;
height: 2.25rem;
-webkit-tap-highlight-color: transparent;
}
.theme-orb .theme-orb-ico {
position: absolute;
top: 50%;
left: 50%;
width: 1.15rem;
height: 1.15rem;
margin: -0.575rem 0 0 -0.575rem;
transition: transform 320ms cubic-bezier(0.16, 1, 0.3, 1);
}
.theme-orb .theme-orb-ico--moon { transform: translateX(170%); }
.theme-orb[aria-pressed="true"] .theme-orb-ico--sun { transform: translateX(-170%); }
.theme-orb[aria-pressed="true"] .theme-orb-ico--moon { transform: none; }
@media (prefers-reduced-motion: reduce) {
.theme-orb .theme-orb-ico { transition: none; }
}
/* Gear: opens Site preferences (prefs.js). Desktop only, orb-sized beside
the theme orb (the `hidden nav:inline-flex` classes on the button do the
breakpoint switch); mobile gets a row in the nav panel instead
(.mobile-nav-prefs). Needs JS, like the dialog. */
.prefs-gear { flex-shrink: 0; width: 2.25rem; height: 2.25rem; }
.prefs-gear svg { width: 1.2rem; height: 1.2rem; }
html:not(.js) .prefs-gear { display: none; }
@media (prefers-reduced-motion: no-preference) {
.prefs-gear svg { transition: transform 420ms cubic-bezier(0.16, 1, 0.3, 1); }
.prefs-gear:hover svg { transform: rotate(60deg); }
}
/* --- Hero actions: one primary button + the phone number, on the first screen --- */
.hero-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.9rem 1.5rem;
}
/* Tagline: a quieter second lede under the main one. */
.hero-tagline { font-size: 0.9375rem; letter-spacing: 0.01em; }
/* CTA note: sits under the button, not beside it, inside the wrapping flex row. */
.hero-cta-note {
flex-basis: 100%;
margin-top: -0.4rem;
font-size: 0.875rem;
color: var(--ink-soft);
}
/* Credentials beside the CTA: plain text, not a link. */
.hero-creds {
margin-top: 0.75rem;
font-family: theme('fontFamily.label');
font-size: 1.0625rem;
color: var(--ink);
}
/* Trust stack: five reasons in a row under the hero; two columns on
tablets, one list on phones. Each item gets a brand accent. No rule above
it: the hero maze runs underneath and a full-width line cut across it.
The maze is knocked back instead of ruled off — a soft wash of the page
ground fades it under the strip, and every glyph carries a small halo of
the same ground, so walls stop at the letterforms rather than crossing
them. Both are page-ground colored, so they follow the theme. */
.trust-stack {
position: relative;
margin-top: 3rem;
padding-top: 1.5rem;
}
/* Paints over .maze-hero: both sit at z-index -1 in the hero's stacking
context (.hero-atmos isolates), and this comes later in tree order. */
.trust-stack::before {
content: "";
position: absolute;
inset: -0.5rem -2.5rem -1.25rem;
z-index: -1;
pointer-events: none;
background: radial-gradient(110% 80% at 50% 66%,
color-mix(in srgb, var(--bg) 60%, transparent) 35%, transparent 100%);
}
.trust-stack :is(.trust-stack-title, .trust-item strong, .trust-item p) {
text-shadow: 0 0 5px var(--bg), 0 0 10px var(--bg);
}
.trust-stack-title {
font-family: theme('fontFamily.label');
font-size: 0.875rem;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--ink-soft);
}
.trust-list {
display: grid;
gap: 1.25rem 1.5rem;
margin-top: 1.25rem;
list-style: none;
padding: 0;
}
@media (min-width: 640px) { .trust-list { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1024px) { .trust-list { grid-template-columns: repeat(5, minmax(0, 1fr)); } }
.trust-item {
--trust-accent: var(--navy);
display: flex;
align-items: flex-start;
gap: 0.8rem;
color: var(--ink-soft);
font-size: 0.9375rem;
line-height: 1.45;
}
.trust-item strong {
display: block;
margin-bottom: 0.15rem;
font-family: theme('fontFamily.label');
font-size: 1.0625rem;
color: var(--ink);
}
.trust-item--years { --trust-accent: var(--coral-ink); }
.trust-item--sky { --trust-accent: var(--sky-ink); }
.trust-item--seagrass { --trust-accent: var(--seagrass-ink); }
.trust-item--lantern { --trust-accent: var(--lantern-ink); }
.trust-item--navy { --trust-accent: var(--ink); }
.trust-ico {
flex-shrink: 0;
width: 2.25rem;
height: 2.25rem;
color: var(--trust-accent);
}
/* Byline: Lindsey's face on the first screen, linking to her full bio. At
every width, so the text-only phone hero still has a person in it. */
.hero-byline {
display: inline-flex;
align-items: center;
gap: 0.9rem;
color: var(--ink);
text-decoration: none;
max-width: 33.75rem;
}
/* Circular mask framed on her face: the <picture> is the circle and the
portrait inside is scaled up and shifted so hair-to-chin fills it. */
.hero-byline-frame {
position: relative;
display: block;
width: 7.5rem;
height: 7.5rem;
flex: none;
overflow: hidden;
border-radius: 999px;
border: 2px solid var(--surface);
box-shadow: 0 0 0 1px var(--line), 0 4px 12px rgba(10, 15, 36, 0.10);
}
.hero-byline-photo {
position: absolute;
left: -27%;
top: -1.5%;
width: 155%;
max-width: none;
height: auto;
}
.hero-byline-text { display: flex; flex-direction: column; gap: 0.1rem; }
.hero-byline-name {
font-family: theme('fontFamily.label');
font-size: 1rem;
text-decoration: underline;
text-decoration-color: var(--line);
text-underline-offset: 0.22em;
}
.hero-byline:hover .hero-byline-name { text-decoration-color: currentColor; }
.hero-byline-meta { font-size: 0.9375rem; line-height: 1.4; color: var(--ink-soft); }
/* Plain-sentence reassurance inside the crisis path card. */
.path-note {
font-family: theme('fontFamily.label');
font-size: 1.0625rem;
color: var(--ink);
}
/* --- Services as rows, not a tile grid: icon in its own column, two
columns of rows from md up. Each row takes a palette accent (Seagrass,
Lantern, Sky, repeating): a tinted icon tile, a soft glow behind the
icon and matching bullet markers. --svc tints marks and washes; --svc-ink
is the contrast-safe step. --- */
.service-rows { display: grid; column-gap: 3rem; }
@media (min-width: 768px) { .service-rows { grid-template-columns: 1fr 1fr; } }
.service-rows > :nth-child(3n+1) { --svc: var(--seagrass); --svc-ink: var(--seagrass-ink); }
.service-rows > :nth-child(3n+2) { --svc: var(--lantern); --svc-ink: var(--lantern-ink); }
.service-rows > :nth-child(3n) { --svc: var(--sky); --svc-ink: var(--sky-ink); }
.service-row {
position: relative;
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
column-gap: 1.1rem;
align-content: start;
padding-block: 1.75rem;
background: radial-gradient(22rem 13rem at 0 0, color-mix(in srgb, var(--svc) 13%, transparent), transparent 70%);
}
.service-row > .service-icon {
grid-row: 1 / span 3;
display: grid;
place-items: center;
width: 3.5rem;
height: 3.5rem;
color: var(--svc-ink);
}
.service-row > .service-icon .icon-lg { width: 2.25rem; height: 2.25rem; }
.service-row > :not(.service-icon) { grid-column: 2; }
.service-row p { margin-top: 0.4rem; }
/* --- Who we help: a photo card per audience on a Sky-tinted band. One
column on phones, two from 640px, four in a row from 1024px (below).
Photos: Unsplash, credited on about-this-site.html; responsive
derivatives from scripts/fetch-photos.sh. --- */
.who-band {
background: color-mix(in srgb, var(--sky) 11%, var(--bg));
border-block: 1px solid color-mix(in srgb, var(--sky) 24%, var(--bg));
}
/* Spacing matches the For Professionals "Who refers" list (gap-y-10 gap-x-6, h3 mt-4, p mt-2). */
.who-list { display: grid; gap: 2.5rem 1.5rem; }
@media (min-width: 640px) { .who-list { grid-template-columns: 1fr 1fr; } }
.who-item { display: flex; flex-direction: column; }
.who-photo { margin-bottom: 1rem; }
.who-photo img { display: block; width: 100%; height: auto; aspect-ratio: 3 / 2; object-fit: cover; }
.who-item p { margin-top: 0.5rem; }
/* --- About: the page's showcase. Large portrait, editorial rail. --- */
.about-portrait { width: 100%; max-width: 16.5rem; }
.about-portrait img { aspect-ratio: 4 / 5; object-fit: cover; }
/* Fee agreement promise, placed beside the numbers it reassures. */
.fee-promise {
display: flex;
align-items: flex-start;
gap: 0.6rem;
margin-top: 1.25rem;
color: var(--ink);
}
.fee-promise .icon { color: var(--seagrass-ink); margin-top: 0.1rem; }
/* --- What is a healthcare advocate: the four value items as a quiet list --- */
.value-list { display: flex; flex-direction: column; gap: 1.5rem; }
.value-list > li { display: flex; align-items: flex-start; gap: 1rem; }
.value-list > li > .icon { width: 2.28rem; height: 2.28rem; margin-top: 0.1rem; color: var(--sky); }
.value-list > li:nth-child(1) > .icon { color: var(--sky); }
.value-list > li:nth-child(2) > .icon { color: var(--seagrass-ink); }
.value-list > li:nth-child(3) > .icon { color: var(--lantern-ink); }
.value-list > li:nth-child(4) > .icon { color: var(--coral-ink); }
.value-list p { margin-top: 0.35rem; }
@media (min-width: 480px) {
.value-list > li > .icon { width: 2.6rem; height: 2.6rem; }
}
/* Before/after: two framed panels, a header band over a white list, stacked
on phones and side by side from 640px. Side by side, the panels share row
tracks (subgrid) so each "before" row lines up with its "after" row. */
.ba-grid {
display: grid;
grid-template-columns: minmax(0, 1fr);
row-gap: 1rem;
}
.ba-panel {
--ba-frame: color-mix(in srgb, var(--ink) 6%, var(--surface));
--ba-head-ink: var(--ink);
display: grid;
align-content: start;
background: var(--ba-frame);
border-radius: 18px;
padding: 0 0.3rem 0.3rem;
}
.ba-panel--after { --ba-frame: var(--ba-after-bg); --ba-head-ink: var(--ba-after-ink); }
.ba-head {
display: flex;
flex-wrap: wrap;
align-items: baseline;
justify-content: space-between;
gap: 0.1rem 1rem;
padding: 0.85rem 0.7rem 0.75rem;
color: var(--ba-head-ink);
}
.ba-head h3 { font-size: 1.0625rem; line-height: 1.3; letter-spacing: 0.01em; color: inherit; font-weight: 700; text-transform: uppercase; }
.ba-head p { font-size: 1.0625rem; line-height: 1.35; }
.ba-list {
display: grid;
background: var(--surface);
border-radius: 13px;
padding-inline: 0.75rem;
overflow-wrap: break-word;
}
/* The one-colour Beacon as a faint green watermark filling the After list,
painted under the rows (isolation keeps z-index -1
above the white fill). */
.ba-panel--after .ba-list { position: relative; isolation: isolate; }
.ba-panel--after .ba-list::before {
content: "";
position: absolute;
inset: 1.25rem 0.5rem;
z-index: -1;
background: var(--seagrass);
-webkit-mask: url('../logomarks/marks/advocate-beacon-one-colour.svg') center / contain no-repeat;
mask: url('../logomarks/marks/advocate-beacon-one-colour.svg') center / contain no-repeat;
opacity: 0.08;
pointer-events: none;
}
/* Narrow: the marker leads the title and the description takes the full
width. From 640px the marker gets its own column beside both. */
.ba-list > li {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
align-content: start;
column-gap: 0.45rem;
padding-block: 0.9rem 1rem;
border-top: 1px solid var(--line);
}
.ba-list > li:first-child { border-top: 0; }
.ba-desc { grid-column: 1 / -1; }
/* Red cross / green tick discs; the palette hues clear 3:1 behind white. */
.ba-mark {
display: grid;
place-items: center;
width: 1.2rem;
height: 1.2rem;
margin-top: 0.2rem;
border-radius: 50%;
background: var(--coral);
}
.ba-mark--yes { background: var(--seagrass); }
.ba-mark svg {
width: 100%;
height: 100%;
fill: none;
stroke: #FFFFFF;
stroke-width: 3.2;
stroke-linecap: round;
stroke-linejoin: round;
}
.ba-title { font-weight: 600; line-height: 1.35; color: var(--ink); text-wrap: wrap; }
.ba-desc { margin-top: 0.2rem; color: var(--ink-soft); }
/* Phones (and Larger / Largest text): Before shows its titles only, so the
stacked panels reach the answer sooner. */
@media screen and (max-width: 639.98px) {
.ba-panel--before .ba-desc { display: none; }
}
@media (min-width: 640px) {
.ba-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-rows: auto repeat(6, auto);
column-gap: 1rem;
}
.ba-panel { grid-row: span 7; grid-template-rows: subgrid; padding: 0 0.375rem 0.375rem; }
/* Before stays on top so After can slide out from beneath it. */
.ba-panel--before { position: relative; z-index: 1; }
.ba-list { grid-row: span 6; grid-template-rows: subgrid; }
.ba-head { gap: 0.15rem 1rem; padding: 1rem 1.1rem 0.9rem; }
.ba-list { padding-inline: 1.25rem; }
.ba-list > li { column-gap: 0.7rem; padding-block: 1.05rem 1.15rem; }
.ba-list > li > .ba-desc { grid-column: 2; }
.ba-mark { grid-row: 1 / span 2; }
.ba-mark { width: 1.4rem; height: 1.4rem; margin-top: 0.1rem; }
}
/* About / How it works: an action where the reading ends */
/* Message field character count */
/* Sand from 550 characters; once a keystroke or paste runs past the limit it takes
the .error-summary treatment (Coral edge, 10% Coral wash, ink text). Sand sits
behind the count as a highlight because it can't carry text; Midnight on Sand ≈12:1.
The edge is an inset shadow so the 2px doesn't nudge the surrounding text. */
/* Non-breaking spaces (contact-form.js) keep "n of 600" together; the count can still
wrap before "characters" in a very narrow column. */
.field-count { padding-inline: 0.3em; border-radius: 0.25rem; }
.field-count.is-near { background: var(--sand); color: var(--midnight); font-weight: 600; }
.field-count.is-over {
background: color-mix(in srgb, var(--coral) 10%, var(--surface));
color: var(--ink);
box-shadow: inset 0 0 0 2px var(--coral);
}
/* Contact dialog: book or call first, message form as the alternative */
.modal-quickbook { margin-top: 1rem; }
.modal-quickbook[hidden] { display: none; }
.modal-quickbook-lede { color: var(--ink-soft); }
.modal-quickbook-actions { display: flex; flex-wrap: wrap; gap: 0.75rem; margin-top: 1rem; }
/* Labels stay on one line (so the Calendly mark sits level with the text);
when both don't fit side by side, the buttons stack instead. */
.modal-quickbook-actions .btn { flex: 1 1 auto; }
.modal-divider {
display: flex;
align-items: center;
gap: 0.9rem;
margin-top: 1.5rem;
font-size: 0.875rem;
color: var(--ink-soft);
}
.modal-divider::before,
.modal-divider::after { content: ""; flex: 1; height: 1px; background: var(--line); }
/* Discard check: dims the dialog's own content (made inert by modal.js) and
centers a small card over it while the visitor decides. */
.modal-confirm {
position: absolute;
inset: 0;
z-index: 2;
display: grid;
place-items: center;
padding: 1.25rem;
background: rgba(10, 15, 36, 0.45);
}
.modal-confirm[hidden] { display: none; }
.modal-confirm-card {
width: 100%;
max-width: 24rem;
padding: 1.5rem;
background: var(--bg);
color: var(--ink);
border: 1px solid var(--line);
border-radius: theme('borderRadius.card');
animation: backdrop-in 160ms ease both;
}
.modal-confirm-actions { display: flex; flex-wrap: wrap; gap: 0.75rem; margin-top: 1.25rem; }
.modal-confirm-actions .btn { flex: 1 1 10rem; }
/* --- "The light comes on": the one delight moment ---
When a message reaches Lindsey, the Advocate Beacon mark appears in the
success panel and its light comes up behind it: a soft Beacon Sky glow
blooms, one slow beam sweeps round, and it settles to a steady glow. The
mark itself is never altered (brand geometry); the light is drawn behind it.
Runs when the panel is unhidden (display:none -> shown restarts the
keyframes), once per send, never loops. A consultation booked in the inline
Calendly embed is the same moment and gets the same light ([data-calendly-booked],
with the Reversed mark where it sits on the navy booking band). Reduced motion: the glow fades in,
no sweep, no scale. */
.beacon-lit {
position: relative;
isolation: isolate;
display: grid;
place-items: center;
width: 5.5rem;
height: 5.5rem;
margin-bottom: 1rem;
}
.beacon-lit-mark {
position: relative;
z-index: 2;
width: 3.5rem;
height: auto;
}
/* steady glow */
.beacon-lit::before {
content: "";
position: absolute;
inset: -0.75rem;
z-index: 0;
border-radius: 999px;
background: radial-gradient(closest-side,
color-mix(in srgb, var(--sky) 42%, transparent),
color-mix(in srgb, var(--sky) 14%, transparent) 60%,
transparent);
opacity: 0.7;
}
/* the sweeping beam: a soft wedge of light, feathered by a radial mask */
.beacon-lit::after {
content: "";
position: absolute;
inset: -1.75rem;
z-index: 1;
border-radius: 999px;
background: conic-gradient(from 0deg,
transparent 0deg,
color-mix(in srgb, var(--sky) 55%, transparent) 18deg,
transparent 42deg,
transparent 360deg);
-webkit-mask-image: radial-gradient(closest-side, #000 35%, transparent 100%);
mask-image: radial-gradient(closest-side, #000 35%, transparent 100%);
opacity: 0;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
:is([data-contact-success], [data-calendly-booked]):not([hidden]) .beacon-lit::before {
animation: beacon-bloom 900ms cubic-bezier(0.16, 1, 0.3, 1) both;
}
:is([data-contact-success], [data-calendly-booked]):not([hidden]) .beacon-lit::after {
animation: beacon-sweep 1600ms cubic-bezier(0.45, 0, 0.25, 1) 240ms both;
}
:is([data-contact-success], [data-calendly-booked]):not([hidden]) .beacon-lit-mark {
animation: beacon-arrive 520ms cubic-bezier(0.16, 1, 0.3, 1) both;
}
}
@media (prefers-reduced-motion: reduce) {
:is([data-contact-success], [data-calendly-booked]):not([hidden]) .beacon-lit::before {
animation: beacon-glow-fade 400ms ease both;
}
}
@keyframes beacon-bloom {
0% { opacity: 0; transform: scale(0.55); }
55% { opacity: 1; transform: scale(1.08); }
100% { opacity: 0.7; transform: scale(1); }
}
@keyframes beacon-sweep {
0% { opacity: 0; transform: rotate(-40deg); }
15% { opacity: 1; }
80% { opacity: 0.85; }
100% { opacity: 0; transform: rotate(320deg); }
}
@keyframes beacon-arrive {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: none; }
}
@keyframes beacon-glow-fade {
from { opacity: 0; }
to { opacity: 0.7; }
}
/* --- Hero maze: a generated maze (#maze-hero) behind the whole hero, anchored to
its bottom-right corner. It fades in toward the right, so the copy column stays
clean and the maze runs around and under the illustration. Phones get the same
right-hand fade. Walls only, static; non-scaling strokes hold 1.75px. --- */
.maze-hero {
position: absolute;
inset: 0;
z-index: -1;
width: 100%;
height: 100%;
pointer-events: none;
fill: none;
stroke: var(--ink);
stroke-width: 1.75;
stroke-linecap: round;
stroke-linejoin: round;
-webkit-mask-image: linear-gradient(to right, transparent 34%, #000 88%);
mask-image: linear-gradient(to right, transparent 34%, #000 88%);
}
.maze-hero use { opacity: 0.16; }
#top > .canvas.section { padding-bottom: clamp(4rem, 2rem + 5vw, 7rem); }
/* --- Fine print, read: a sample statement beside three plain notes. The
statement is a paper document (Surface, hairline, Lift); flagged lines take
a Sand highlighter wash, and the reviewer's marks are Sky Ink pen strokes
(an underline, a ring) drawn in the sprite's round caps. Numbered pins tie
each mark to its note. Everything is visible without JS or motion. --- */
.fineprint-head p { max-width: theme('maxWidth.measure'); color: var(--ink-soft); }
.fineprint-body {
display: grid;
gap: 2rem;
margin-top: 1.75rem;
align-items: start;
container: fineprint / inline-size;
}
@media (min-width: 820px) {
.fineprint-body { grid-template-columns: minmax(0, 1.45fr) minmax(0, 1fr); gap: 2.75rem; }
}
.fineprint-notes p.fineprint-note {
grid-column: 1 / -1;
margin-top: 1.5rem;
text-align: left;
color: var(--ink-soft);
}
.fineprint-doc {
position: relative;
background: var(--surface);
border: 1px solid var(--line);
border-radius: theme('borderRadius.tile');
box-shadow: theme('boxShadow.lift');
padding: 1.25rem 0.85rem 1.25rem 2.35rem;
}
@media (min-width: 480px) {
.fineprint-doc { padding: 1.6rem 1.5rem 1.5rem 2.85rem; }
}
.fineprint-doc-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
}
.fineprint-doc-title {
font-family: theme('fontFamily.label');
font-size: 0.8125rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--ink);
}
.fineprint-stamp {
flex: none;
margin-top: -0.2rem;
padding: 0.15rem 0.55rem;
border: 1.5px solid var(--ink-soft);
border-radius: 6px;
font-family: theme('fontFamily.label');
font-size: 0.8125rem;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--ink-soft);
rotate: -6deg;
}
.fineprint-doc-meta {
display: flex;
flex-wrap: wrap;
gap: 0.1rem 1.25rem;
margin-top: 0.3rem;
padding-bottom: 0.8rem;
border-bottom: 1px solid var(--line);
font-size: 0.875rem;
color: var(--ink-soft);
}
.fineprint-table {
width: 100%;
border-collapse: collapse;
font-size: 0.9375rem;
line-height: 1.4;
}
.fineprint-table thead th {
padding: 0.7rem 0.45rem 0.4rem;
font-family: theme('fontFamily.label');
font-size: 0.75rem;
font-weight: 400;
letter-spacing: 0.1em;
text-transform: uppercase;
text-align: left;
color: var(--ink-soft);
}
.fineprint-table td {
padding: 0.62rem 0.45rem;
border-top: 1px solid var(--line-soft);
vertical-align: top;
color: var(--ink);
}
.fineprint-table td:first-child {
position: relative;
width: 3.6rem;
font-variant-numeric: tabular-nums;
letter-spacing: 0.02em;
}
@media (min-width: 480px) { .fineprint-table td:first-child { width: 4.4rem; } }
.fineprint-table .fp-amt {
text-align: right;
white-space: nowrap;
font-family: theme('fontFamily.mono');
font-feature-settings: 'tnum' 1;
font-size: 0.875rem;
}
.fineprint-table thead .fp-amt { font-family: theme('fontFamily.label'); font-size: 0.75rem; }
.fineprint-table tfoot th,
.fineprint-table tfoot td {
padding: 0.8rem 0.45rem 0.1rem;
border-top: 3px double var(--line);
color: var(--ink);
}
.fineprint-table tfoot th {
text-align: left;
font-family: theme('fontFamily.label');
font-weight: 400;
font-size: 0.9375rem;
}
/* The narrowest phones: with 320px or less to work in (a 320–360px screen, or
wider ones at the larger text sizes), tighter tracking, padding and figures
keep all three columns inside the document, and at a larger text size it
borrows a little of the page gutter. A container query, so it follows the
space actually left at every text size (text-layout.js only rewrites @media). */
@container fineprint (max-width: 320px) {
.fineprint-doc { padding-left: 2.15rem; padding-right: 0.6rem; }
.fineprint-table thead th { letter-spacing: 0.04em; }
.fineprint-table :is(thead, tbody, tfoot) :is(th, td) { padding-inline: 0.3rem; }
.fineprint-table td:first-child { width: 3.3rem; }
.fineprint-table .fp-amt { font-size: 0.8125rem; }
:root[data-text] .fineprint-doc { margin-inline: -0.75rem; }
}
/* Sand highlighter behind a flagged line. Each cell paints its own slice of one
row-wide gradient (main.js sets --fp-row-w and each cell's --fp-x), so a single
--fp-sweep on the row moves one edge across all three cells in one stroke.
Without the measurements each cell just carries the full wash. */
.fineprint-table tr.is-flagged td {
--fp-wash: color-mix(in srgb, var(--sand) 28%, transparent);
background-image: linear-gradient(to right, var(--fp-wash) var(--fp-sweep), transparent var(--fp-sweep));
background-repeat: no-repeat;
background-size: var(--fp-row-w, 100%) 100%;
background-position: calc(-1 * var(--fp-x, 0px)) 0;
}
.fineprint [data-note="1"] { --fp-d: 450ms; }
.fineprint [data-note="2"] { --fp-d: 850ms; }
.fineprint [data-note="3"] { --fp-d: 1250ms; }
.fp-pin {
display: grid;
place-items: center;
width: 1.4rem;
height: 1.4rem;
border: 1.5px solid var(--sky-ink);
border-radius: 999px;
background: var(--surface);
color: var(--sky-ink);
font-family: theme('fontFamily.label');
font-size: 0.8125rem;
line-height: 1;
transition: background-color 0.2s ease, color 0.2s ease;
}
.fineprint-table .fp-pin { position: absolute; left: -1.95rem; top: 0.6rem; }
@media (min-width: 480px) { .fineprint-table .fp-pin { left: -2.2rem; } }
.fp-mark { position: relative; display: inline-block; }
.fp-ink { position: absolute; overflow: visible; pointer-events: none; }
.fp-ink path {
fill: none;
stroke: var(--sky-ink);
stroke-width: 2.25;
stroke-linecap: round;
stroke-linejoin: round;
stroke-dasharray: 1;
}
.fp-ink--under { left: -3px; bottom: -0.3rem; width: calc(100% + 6px); height: 0.5rem; }
.fp-ink--ring { left: -0.6rem; top: -0.5rem; width: calc(100% + 1.2rem); height: calc(100% + 1rem); }
.fineprint-notes { display: flex; flex-direction: column; gap: 1.6rem; }
.fineprint-notes > li {
display: grid;
grid-template-columns: 2rem minmax(0, 1fr);
column-gap: 0.9rem;
}
.fineprint-notes .fp-pin { width: 2rem; height: 2rem; font-size: 0.9375rem; }
.fineprint-notes h3 { transition: color 0.2s ease; }
.fineprint-notes p { margin-top: 0.3rem; }
@media (min-width: 820px) { .fineprint-notes { padding-top: 0.5rem; } }
/* Pointing at a note (or its line) lights the pair. */
.fineprint[data-active="1"] [data-note="1"] .fp-pin,
.fineprint[data-active="2"] [data-note="2"] .fp-pin,
.fineprint[data-active="3"] [data-note="3"] .fp-pin { background: var(--sky-ink); color: var(--surface); }
.fineprint[data-active="1"] li[data-note="1"] h3,
.fineprint[data-active="2"] li[data-note="2"] h3,
.fineprint[data-active="3"] li[data-note="3"] h3 { color: var(--link); }
@media (prefers-reduced-motion: no-preference) {
.js [data-reveal="marks"].reveal-pending { opacity: 0.01; }
.js [data-reveal="marks"].reveal-pending .fp-ink path { stroke-dashoffset: 1; }
.js [data-reveal="marks"].reveal-pending tr.is-flagged { --fp-sweep: 0%; }
.js [data-reveal="marks"].reveal-pending .fineprint-table .fp-pin { opacity: 0; }
.js [data-reveal="marks"].reveal-in {
animation: reveal-rise 720ms var(--reveal-ease) backwards;
}
/* One stroke per row: a quick start, an even pull across, a soft lift at the end. */
.js [data-reveal="marks"].reveal-in tr.is-flagged {
animation: fp-highlight 780ms cubic-bezier(0.4, 0, 0.25, 1) backwards;
animation-delay: var(--fp-d);
}
.js [data-reveal="marks"].reveal-in .fineprint-table .fp-pin {
animation: reveal-settle 420ms var(--reveal-ease) backwards;
animation-delay: var(--fp-d);
}
.js [data-reveal="marks"].reveal-in .fp-ink path {
animation: fp-draw 560ms cubic-bezier(0.45, 0, 0.2, 1) backwards;
animation-delay: calc(var(--fp-d) + 300ms);
}
@keyframes fp-highlight {
from { --fp-sweep: 0%; }
to { --fp-sweep: 100%; }
}
@keyframes fp-draw {
from { stroke-dashoffset: 1; }
to { stroke-dashoffset: 0; }
}
}
/* --- Who we help: from 1024px the four audiences stand in a row. --- */
@media (min-width: 1024px) {
.who-list { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
/* --- Credentials: as the rows arrive, a Sky rule runs along each one, in
reading order, and fades back to the hairline. --- */
.dl-rows > div { position: relative; }
@media (prefers-reduced-motion: no-preference) {
.js .dl-rows.reveal-in > div::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: -1px;
height: 2px;
background: var(--sky);
transform-origin: left center;
opacity: 0;
animation: dl-read 1000ms var(--reveal-ease) backwards;
animation-delay: calc(var(--reveal-i, 0) * 150ms + 250ms);
}
@keyframes dl-read {
0% { opacity: 1; transform: scaleX(0); }
60% { opacity: 1; transform: none; }
100% { opacity: 0; transform: none; }
}
}
/* --- Booking band: the maze, solved, full width along the bottom of the navy
ground. On the index it has a strip of its own under the buttons (rule below),
so no copy crosses a wall; the style guide's masthead still uses it as a
background, anchored to the band's bottom edge and fading out upward, so it
takes no space of its own. It
scales with the page, so both walls always show; a smaller maze stands in on
phones so the cells stay readable. A Beacon Sky ring waits outside the entrance
in the left wall; the route crosses to the exit in the right wall and ends at a
dot. main.js adds .route-pending, then .is-routed once the footer comes into
view: ring, route, dot. No JS: all shown. Reduced motion: shown without the
draw (global rule). --- */
.schedule-band { position: relative; isolation: isolate; overflow: hidden; }
.maze-cross {
display: block;
position: absolute;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
width: 100%;
height: auto;
pointer-events: none;
fill: none;
stroke: var(--paper);
stroke-width: 1.75;
stroke-linecap: round;
stroke-linejoin: round;
-webkit-mask-image: linear-gradient(to bottom, transparent, #000 70%);
mask-image: linear-gradient(to bottom, transparent, #000 70%);
}
.maze-cross--lg { aspect-ratio: 1840 / 328; }
.maze-cross--sm { aspect-ratio: 720 / 248; display: none; }
@media (max-width: 640px) {
.maze-cross--lg { display: none; }
.maze-cross--sm { display: block; }
}
/* On the index the maze is the last thing in the band, in the flow: it takes the
room its aspect ratio asks for, so the copy and buttons end above it instead of
running across the walls. The negative bottom margin lands it flush with the
band's edge, in the padding .section leaves under the buttons (the clamp repeats
.section's), and goes with the maze where the maze is hidden (reader mode,
print), leaving that padding as the band's ordinary bottom. */
#schedule .maze-cross {
position: static;
margin-block: 1.5rem calc(-1 * clamp(2.25rem, 1.5rem + 3.25vw, 4rem));
}
.maze-cross use { opacity: 0.15; }
.maze-route { stroke: var(--sky); stroke-width: 3; opacity: 0.6; stroke-dasharray: 1; }
.maze-cross--sm .maze-route { stroke-width: 4; }
.maze-route-start { fill: none; stroke: var(--sky); stroke-width: 2.5; opacity: 0.8; }
.maze-route-end { fill: var(--sky); stroke: none; opacity: 0.9; }
/* Between phones and desktops the wide maze scales down to about half. */
@media (max-width: 1023px) {
.maze-cross--lg .maze-route { stroke-width: 5; }
.maze-cross--lg .maze-route-start { stroke-width: 4; }
}
.maze-cross.route-pending .maze-route { stroke-dashoffset: 1; }
.maze-cross.route-pending :is(.maze-route-start, .maze-route-end) { opacity: 0; }
.maze-cross.route-pending.is-routed .maze-route {
stroke-dashoffset: 0;
transition: stroke-dashoffset 4200ms cubic-bezier(0.45, 0, 0.25, 1) 500ms;
}
.maze-cross.route-pending.is-routed .maze-route-start {
opacity: 0.8;
transition: opacity 500ms ease;
}
.maze-cross.route-pending.is-routed .maze-route-end {
opacity: 0.9;
transition: opacity 500ms ease 4600ms;
}
/* --- Calendly, inline: the scheduler framed in the page by calendly.js, in the
booking band and in a panel of the contact dialog. Until the frame is in, a
mount holds only its authored fallback, a plain link out; .is-live swaps that
for the frame plus a one-line way out for a visitor whose browser refuses it.
Calendly's page is transparent around its own white card, so the mount adds no
card of its own. Its colors need a paid plan: the card is white in both
themes.
The mount's width picks Calendly's layout, and with it the size (its breakpoints
are px widths of the frame, measured 2026-09-18 with the event details hidden):
- under 650px, the phone layout: the page fills the frame, 602px tall, and a
picked day opens its times as a second view;
- 650 to 999px, a stacked card: 711px, growing past 1300px when a day is picked.
Avoided: it is the largest of the three;
- 1000px and up, the desktop layout: a 680px card in a transparent page, 660px
tall and fixed, because the times scroll inside the card.
So the mount is capped at 640px, and the booking band widens it to 1040px where
the viewport has room (clear of the 1000px edge; the band clips any overflow). --- */
.calendly-embed { position: relative; width: 100%; max-width: 640px; margin: 1.75rem auto 0; }
/* 1040px + a 1rem gutter each side. The Larger / Largest text sizes keep the phone
layout (text-layout.js rewrites this query), and so the 640px mount. */
@media (min-width: 1072px) {
#schedule .calendly-embed {
width: 1040px;
max-width: none;
margin-inline: calc((100% - 1040px) / 2);
}
/* The desktop layout brings a 66px top margin of its own. */
#schedule .calendly-embed.is-live { margin-top: 0.5rem; }
#schedule .calendly-frame { height: 660px; }
}
/* The height is only a start, matched to the layout's first report so nothing
jumps: calendly.js sets the real one from the frame's own reports, step by step.
color-scheme matches Calendly's page; without it the dark theme makes the
browser paint an opaque backdrop behind the frame. */
.calendly-frame {
display: block;
position: relative;
z-index: 1;
width: 100%;
height: 602px;
border: 0;
border-radius: 12px;
color-scheme: light;
}
/* Sits where Calendly's card will land and is covered by it; calendly.js removes
it when the frame has loaded. */
.calendly-loading {
position: absolute;
inset: 7rem 0 auto;
text-align: center;
opacity: 0.82;
}
.calendly-alt { display: none; margin-top: 0.75rem; font-size: 0.9375rem; color: var(--ink-soft); }
.on-dark .calendly-alt { color: rgba(245, 251, 255, 0.82); }
.calendly-embed.is-live .calendly-alt { display: block; }
.calendly-embed.is-live .calendly-fallback { display: none; }
/* Booked: calendly.js has taken the frame out and unhidden the authored thanks
(.calendly-booked), which is all the mount shows. It is the same moment as a sent
message, so the beacon's light comes on the same way (rules below, "The light
comes on"). In the dialog, modal.js marks the booking block too, and everything
in it but the mount steps aside. */
.calendly-embed.is-booked .calendly-fallback { display: none; }
.calendly-booked[hidden] { display: none; }
.calendly-booked:focus { outline: none; }
.modal-quickbook.is-booked > :not(.calendly-embed) { display: none; }
/* Once booked, not asked again: calendly.js remembers a booking (localStorage) and
sets data-booked on <html>, and this takes the invitations to book away: every
mount but the one showing its thanks, every toggle, and whatever is marked
data-calendly-offer (the success panel's "Book time with us"). Copy marked
data-calendly-after is authored hidden and stands in where a sentence needs
rewording. The footer's Calendly icon is left alone, and following it brings
all of this back. A rule and not script, so a dialog modal.js imports later
needs no setup. Screen only: paper keeps the band's fallback link. */
@media screen {
:root[data-booked] :is([data-calendly]:not(.is-booked), [data-calendly-toggle], [data-calendly-offer]) { display: none; }
:root[data-booked] [data-calendly-after] { display: inline; }
}
/* In the dialog: a gap under the buttons. The drawer is 30rem wide, so this is
always Calendly's phone layout. */
.calendly-embed.calendly-embed--panel { margin-top: 1rem; }
.calendly-embed--panel .calendly-loading { top: 3rem; }
/* --- "Get Started" tab: the first time it comes out it leans a little
further than its resting place and settles back, once per page view. --- */
.drawer-tab {
--tab-rest: translateY(-50%);
--tab-away: translateY(-50%) translateX(110%);
--tab-lean: translateY(-50%) translateX(-0.45rem);
}
@media (max-width: 479px) {
.drawer-tab {
--tab-rest: translateY(0);
--tab-away: translateY(110%);
--tab-lean: translateY(-0.2rem);
}
}
@media (prefers-reduced-motion: no-preference) {
.drawer-tab.is-arriving { animation: tab-arrive 900ms cubic-bezier(0.16, 1, 0.3, 1) both; }
@keyframes tab-arrive {
0% { transform: var(--tab-away); }
55% { transform: var(--tab-lean); }
100% { transform: var(--tab-rest); }
}
}
/* --- 404: a small maze with its route drawn through to the way out --- */
.lost-maze { display: block; width: 100%; max-width: 22rem; margin-top: 2rem; overflow: visible; }
.lost-maze path { fill: none; stroke-linecap: round; stroke-linejoin: round; }
.lost-maze-walls { stroke: var(--ink-soft); stroke-width: 1.5; opacity: 0.45; }
.lost-maze-route { stroke: var(--sky-ink); stroke-width: 2.5; stroke-dasharray: 1; }
.lost-maze-end { fill: var(--sky-ink); }
@media (prefers-reduced-motion: no-preference) {
.lost-maze-route { animation: maze-route 2000ms cubic-bezier(0.45, 0, 0.2, 1) 400ms both; }
.lost-maze-end { animation: reveal-fade 400ms ease 2300ms both; }
@keyframes maze-route {
from { stroke-dashoffset: 1; }
to { stroke-dashoffset: 0; }
}
}
@media print {
.maze-hero, .maze-cross { display: none; }
.trust-stack::before { content: none; }
.trust-stack :is(.trust-stack-title, .trust-item strong, .trust-item p) { text-shadow: none; }
}
/* --- Legal pages --- */
.legal-prose { max-width: theme('maxWidth.prose'); }
.legal-prose h2 { font-size: theme('fontSize.h3'); margin-top: 2.5rem; margin-bottom: 0.75rem; }
.legal-prose h3 { font-size: theme('fontSize.h4'); margin-top: 1.75rem; margin-bottom: 0.5rem; }
.legal-prose p, .legal-prose li { color: var(--ink-soft); }
.legal-prose > p + p { margin-top: 1rem; }
.legal-prose ul, .legal-prose ol { margin: 1rem 0; padding-left: 1.4rem; }
.legal-prose ul { list-style: disc; }
.legal-prose ol { list-style: decimal; }
.legal-prose li { margin-top: 0.4rem; }
.legal-prose li::marker { color: var(--sky); }
.legal-prose a { color: var(--ink); text-decoration: underline; text-decoration-color: var(--sky); }
.legal-prose :where(h1, h2, h3)[id] { scroll-margin-top: 6rem; }
.legal-prose address {
margin: 1.25rem 0;
padding: 0.1rem 0 0.1rem 1.1rem;
border-left: 3px solid var(--sky);
color: var(--ink-soft);
line-height: 1.7;
}
.legal-toc { background: var(--surface); border: 1px solid var(--line); border-radius: theme('borderRadius.card'); padding: 1.25rem 1.5rem; }
.legal-toc ol { margin: 0.5rem 0 0; }
.legal-prose code {
font-family: theme('fontFamily.mono');
font-size: 0.85em;
background: var(--tag-bg);
color: var(--ink);
padding: 0.1em 0.4em;
border-radius: 0.25rem;
}
.legal-prose .table-wrap {
overflow-x: auto;
margin: 1.5rem 0;
border: 1px solid var(--line);
border-radius: theme('borderRadius.card');
}
.legal-prose table { width: 100%; min-width: 30rem; border-collapse: collapse; font-size: 0.9375rem; }
.legal-prose th, .legal-prose td { padding: 0.7rem 0.95rem; text-align: left; vertical-align: top; border-top: 1px solid var(--line-soft); }
.legal-prose thead th {
background: var(--tag-bg);
color: var(--ink);
font-family: theme('fontFamily.label');
font-size: 0.75rem;
letter-spacing: 0.06em;
text-transform: uppercase;
border-top: 0;
}
.legal-prose tbody tr:first-child td, .legal-prose tbody tr:first-child th { border-top: 0; }
.legal-prose td:last-child, .legal-prose th:last-child { text-align: right; }
.legal-prose .sig-grid {
display: grid;
gap: 1rem;
margin: 1.25rem 0;
}
@media (min-width: 480px) { .legal-prose .sig-grid { grid-template-columns: 1fr 1fr; } }
.legal-prose .sig-card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: theme('borderRadius.card');
padding: 1rem 1.15rem;
}
.legal-prose .sig-card p { margin-top: 0.5rem; }
.legal-prose .sig-card .role {
display: block;
margin-bottom: 0.25rem;
font-family: theme('fontFamily.label');
font-size: 0.9375rem;
font-weight: 500;
letter-spacing: 0.01em;
color: var(--ink);
}
.legal-prose .sig-card .role em { color: var(--ink-soft); font-weight: 400; }
.legal-prose .fill { display: inline-block; min-width: 9rem; border-bottom: 1px solid var(--line); }
.legal-prose .checklist { list-style: none; padding-left: 0; }
.legal-prose .checklist li { padding-left: 1.6rem; text-indent: -1.6rem; }
.legal-prose .checklist li::before { content: "\2610"; color: var(--sky); margin-right: 0.6rem; }
.todo-flag {
border: 1px dashed var(--warn-line);
background: var(--warn-bg);
color: var(--warn-ink);
border-radius: 0.5rem;
padding: 0.9rem 1rem;
font-size: 0.9375rem;
margin: 1rem 0;
}
.todo-flag strong { color: var(--ink); }
.todo-flag ul, .todo-flag ol { margin: 0.5rem 0 0; padding-left: 1.2rem; }
.legal-toc .footer-col-title { color: var(--ink-soft); }
/* --- Style guide: the bar. The internal pages' own bar: the mark, the page's
name, links between the guide and the two code references, and the theme
and Site preferences buttons. Deliberately not the site header, so nobody
takes these pages for public ones. Navy in both themes, like the booking
band, so it borrows the dark palette (.sg-theme-dark on the element) and
the header's round buttons work on it unchanged. Not sticky: a reference
page is scrolled, not navigated from the top. --- */
.sg-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem 1.75rem;
padding: 0.85rem 1.5rem;
background: var(--navy);
color: var(--paper);
border-bottom: 1px solid color-mix(in srgb, var(--paper) 18%, transparent);
}
/* The bar's content keeps the shell's edges: 2rem of padding, or whatever
centers it on the same 1116px once the window is wider than the shell. */
@media (min-width: 480px) { .sg-bar { padding-inline: max(2rem, calc((100% - 1116px) / 2)); } }
.sg-bar-home { display: flex; flex: none; border-radius: 6px; }
.sg-bar-home img { height: 2.5rem; width: auto; }
.sg-bar-title {
margin-right: auto;
font-family: theme('fontFamily.label');
font-size: theme('fontSize.h4[0]');
line-height: 1;
}
.sg-bar-nav { display: flex; flex-wrap: wrap; gap: 0.25rem 1.6rem; }
/* Label-voice links. The current page holds the nav's 2px Sky underline.
Two classes, to outrank .on-dark's Sky for classless links. */
.sg-bar .sg-bar-nav a {
position: relative;
padding: 0.7rem 0;
font-family: theme('fontFamily.label');
font-size: 0.9375rem;
color: color-mix(in srgb, var(--paper) 86%, transparent);
text-decoration: none;
}
.sg-bar .sg-bar-nav a:hover, .sg-bar .sg-bar-nav a[aria-current] { color: var(--paper); }
.sg-bar-nav a[aria-current]::after {
content: "";
position: absolute;
inset: auto 0 0.4rem;
height: 2px;
border-radius: 1px;
background: var(--sky);
}
.sg-bar-tools { display: flex; gap: 0.5rem; }
/* On a phone the links and the two buttons share the second row. */
@media (max-width: 479px) {
.sg-bar { column-gap: 0.75rem; }
.sg-bar-nav { margin-right: auto; column-gap: 1.1rem; }
}
/* The header sets its theme and gear buttons at 2.25rem; here there is room
for the full 44px. A class of its own rather than a descendant selector:
Tailwind keeps a rule when any class in it is used, and .header-icon-btn
is, on every page. */
.sg-bar-btn {
width: 44px;
height: 44px;
border-color: color-mix(in srgb, var(--paper) 38%, transparent);
}
/* At the larger text sizes the bar holds a fixed-geometry mark and round
buttons, so it is zoomed back like the site header, and its buttons up again. */
@media screen {
:root[data-text] .sg-bar { zoom: var(--text-unscale); }
:root[data-text] .sg-bar-btn { zoom: var(--text-scale); }
}
/* --- Style guide: the masthead. The guide opens on a real component: the
booking band's navy ground with its maze, solved, along the bottom edge
(.schedule-band and .maze-cross, unchanged), and the title block above it.
main.js isn't loaded here, so the route is simply shown drawn. --- */
.sg-mast {
background: var(--navy);
color: var(--paper);
padding: clamp(2.5rem, 1.5rem + 4vw, 4.5rem) 1.5rem clamp(7rem, 3rem + 15vw, 15rem);
}
@media (min-width: 480px) { .sg-mast { padding-inline: 2rem; } }
.sg-mast-copy { max-width: 1116px; margin-inline: auto; }
.sg-mast h1 { color: var(--paper); }
.sg-mast-lede { margin-top: 1.25rem; color: color-mix(in srgb, var(--paper) 86%, transparent); }
.sg-mast-key {
margin-top: 1.5rem;
font-family: theme('fontFamily.label');
font-size: 0.8125rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: color-mix(in srgb, var(--paper) 74%, transparent);
}
/* --- Style guide: shell and contents. A sticky contents list beside one
reading column. The list is numbered because the numbers are how sections
get referred to ("see 9, Buttons"); the headings carry no numeral. Below
1024px, and at the Larger and Largest text sizes, it is one column and the
contents becomes a two-column block at the top. --- */
.sg-shell {
display: grid;
grid-template-columns: minmax(0, 1fr);
row-gap: 2.5rem;
max-width: 1180px;
margin-inline: auto;
padding: clamp(2.25rem, 1.5rem + 3.25vw, 4rem) 1.5rem clamp(3rem, 2rem + 4vw, 6rem);
align-items: start;
}
@media (min-width: 480px) { .sg-shell { padding-inline: 2rem; } }
@media (min-width: 1024px) {
.sg-shell { grid-template-columns: 14rem minmax(0, 1fr); column-gap: 4rem; }
}
.sg-toc-title {
display: block;
margin-bottom: 0.85rem;
font-family: theme('fontFamily.label');
font-size: 0.8125rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--ink-soft);
}
.sg-toc ol { counter-reset: toc; }
.sg-toc li { counter-increment: toc; border-top: 1px solid var(--line); break-inside: avoid; }
.sg-toc li:last-child { border-bottom: 1px solid var(--line); }
.sg-toc a {
position: relative;
display: block;
padding: 0.55rem 0 0.55rem 1.75rem;
font-size: 1rem;
line-height: 1.35;
color: var(--ink-soft);
text-decoration: none;
}
/* Out of flow, so the current link's underline stops at its words. */
.sg-toc a::before {
content: counter(toc);
position: absolute;
left: 0;
top: 0.7rem;
font-family: theme('fontFamily.label');
font-size: 0.8125rem;
font-variant-numeric: tabular-nums;
color: var(--ink-soft);
}
.sg-toc a:hover { color: var(--ink); }
/* The section being read, set by style-guide.js: full-strength ink and the
2px Sky underline the site nav gives its current link. */
.sg-toc a[aria-current] {
color: var(--ink);
text-decoration: underline;
text-decoration-color: var(--sky);
text-decoration-thickness: 2px;
text-underline-offset: 0.3em;
}
@media (min-width: 560px) { .sg-toc ol { columns: 2; column-gap: 2rem; } }
@media (min-width: 1024px) {
.sg-toc {
position: sticky;
top: 1.5rem;
max-height: calc(100vh - 3rem);
overflow-y: auto;
scrollbar-width: thin;
padding-right: 0.5rem;
}
.sg-toc ol { columns: auto; }
}
/* The bar isn't sticky, so an in-page jump needs none of the site header's
scroll padding. */
:root:has(.sg-shell) { scroll-padding-top: 1.25rem; }
/* --- Style guide: sections and prose. One hairline and a generous gap above
each section, more space above a heading than below it. The prose rules
carry no specificity and skip any element with a class, so a real
component shown as a specimen is never restyled by the page around it. --- */
.sg-main { min-width: 0; }
.sg-main > section {
margin-top: clamp(3rem, 2rem + 4vw, 4.5rem);
padding-top: clamp(3rem, 2rem + 4vw, 4.5rem);
border-top: 1px solid var(--line);
}
/* Every section heading carries the site's 8rem Sky rule. */
.sg-main > section > h2::after {
content: "";
display: block;
width: 8rem;
height: 2px;
margin-top: 1.5rem;
background: var(--sky);
border-radius: 1px;
}
:where(.sg-main) h3:not([class]) { margin-top: 3.25rem; }
:where(.sg-main) h4:not([class]) { margin-top: 2.25rem; }
:where(.sg-main) p:not([class]) { margin-top: 1rem; max-width: theme('maxWidth.prose'); color: var(--ink-soft); }
/* A section's one opening sentence: .lede's size and measure, in full-strength ink. */
.sg-lede { margin-top: 1.25rem; color: var(--ink); }
:where(.sg-main) :is(ul, ol):not([class]) {
margin-top: 1rem;
padding-left: 1.25rem;
max-width: theme('maxWidth.prose');
list-style: disc;
color: var(--ink-soft);
}
:where(.sg-main) :is(ul, ol):not([class]) > li + li { margin-top: 0.5rem; }
/* Code is code: the mono face, on a quiet chip. A chip moves to the next line
whole, and only breaks inside when it is longer than the line. */
.sg-main code {
display: inline-block;
max-width: 100%;
padding: 0.05em 0.35em;
border-radius: 4px;
background: var(--tag-bg);
font-family: theme('fontFamily.mono');
font-size: 0.84em;
line-height: 1.35;
color: var(--ink);
vertical-align: baseline;
}
.sg-main pre {
margin-top: 1.25rem;
padding: 1rem 1.25rem;
overflow-x: auto;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--surface);
font-family: theme('fontFamily.mono');
font-size: 0.875rem;
line-height: 1.6;
color: var(--ink-soft);
}
.sg-main pre code { display: inline; padding: 0; background: none; font-size: inherit; }
.sg-note { font-size: 0.9375rem; }
.sg-foot {
margin-top: clamp(4rem, 3rem + 4vw, 6rem);
padding-top: 1.35rem;
border-top: 1px solid var(--line);
font-size: 0.9375rem;
color: var(--ink-soft);
}
/* --- Style guide: named rules. A short list that closes a section: each
rule's name in the heading face over its sentence. Names are sentence case,
not tracked capitals, because several run to a full clause. --- */
.sg-rules { display: grid; gap: 1.35rem; margin-top: 2.25rem; max-width: theme('maxWidth.prose'); }
.sg-rules > li { color: var(--ink-soft); }
.sg-rules > li > b {
display: block;
margin-bottom: 0.2rem;
font-family: theme('fontFamily.label');
font-weight: 400;
font-size: 1.0625rem;
line-height: 1.35;
color: var(--ink);
}
/* --- Style guide: ledgers. Reference tables are ledgers, not grids:
label-voice column heads over hairline rows, no cell borders, no zebra.
.sg-scroll wraps any ledger that may be wider than a phone, so the page
itself never scrolls sideways. --- */
.sg-scroll { margin-top: 1.5rem; max-width: 100%; overflow-x: auto; scrollbar-width: thin; }
.sg-table { width: 100%; border-collapse: collapse; font-size: 1rem; line-height: 1.5; }
.sg-scroll > .sg-table { min-width: 36rem; }
.sg-table th, .sg-table td {
padding: 0.7rem 1.15rem 0.7rem 0;
text-align: left;
vertical-align: top;
border-top: 1px solid var(--line);
}
.sg-table :is(th, td):last-child { padding-right: 0; }
.sg-table td { color: var(--ink-soft); }
.sg-table tbody th { font-weight: 400; color: var(--ink); }
.sg-table thead th {
padding-top: 0;
padding-bottom: 0.6rem;
border-top: 0;
vertical-align: bottom;
font-family: theme('fontFamily.label');
font-weight: 400;
font-size: 0.8125rem;
letter-spacing: 0.08em;
text-transform: uppercase;
white-space: nowrap;
color: var(--ink-soft);
}
.sg-table tbody tr:last-child > * { border-bottom: 1px solid var(--line); }
/* A token or selector in a ledger's narrow first column never breaks at its hyphens. */
.sg-table tbody th > code { white-space: nowrap; }
/* A group heading inside a long ledger (the token table's families). */
.sg-table tr.sg-group th {
padding: 2rem 0 0.6rem;
border-top: 0;
font-family: theme('fontFamily.label');
font-size: theme('fontSize.h4[0]');
letter-spacing: 0;
text-transform: none;
color: var(--ink);
}
.sg-table--tokens { font-size: 0.9375rem; }
.sg-table--tokens td:last-child { min-width: 16rem; }
/* A value cell keeps its chip and its hex on one line. */
.sg-table--tokens td:has(> .sg-chip) { white-space: nowrap; }
/* A small square of a color beside its name or value. */
.sg-chip {
display: inline-block;
width: 0.9rem;
height: 0.9rem;
margin-right: 0.5rem;
vertical-align: -0.1em;
border: 1px solid color-mix(in srgb, var(--ink) 22%, transparent);
border-radius: 3px;
}
/* Marks a palette color that appears in the mark itself. */
.sg-tag {
display: inline-block;
margin-left: 0.4rem;
padding: 0.05rem 0.4rem;
border: 1px solid var(--line);
border-radius: 4px;
font-family: theme('fontFamily.label');
font-size: 0.75rem;
letter-spacing: 0.08em;
text-transform: uppercase;
white-space: nowrap;
color: var(--ink-soft);
}
/* A measured contrast ratio, with the level it clears printed after it in
words, never by color. */
.sg-ratio { white-space: nowrap; font-variant-numeric: tabular-nums; color: var(--ink); }
.sg-ratio > span {
display: block;
font-family: theme('fontFamily.label');
font-size: 0.75rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--ink-soft);
}
/* scripts/build-docs.js writes the grade onto the cell as data-grade. */
.sg-ratio[data-grade^="Below"] > span { color: var(--ink); text-decoration: underline; text-underline-offset: 0.2em; }
/* A contrast sample: real ink on a real ground. Each one sits in a
.sg-theme-light or .sg-theme-dark scope and paints with var(), inline because
every row is a different pair, so a sample can't drift from its tokens. */
.sg-pair {
display: inline-flex;
align-items: baseline;
gap: 0.6rem;
min-width: 11.5rem;
padding: 0.45rem 0.85rem;
border-radius: 8px;
font-size: 1rem;
line-height: 1.3;
}
.sg-pair b { font-family: theme('fontFamily.label'); font-weight: 400; font-size: 1.5rem; line-height: 1; }
.sg-pair--edge { box-shadow: inset 0 0 0 1px var(--line); }
/* A drawn sample of the 3px focus ring, for the contrast ledger. */
.sg-ringbox {
display: inline-block;
width: 1rem;
height: 1rem;
margin: 0 0.35rem 0 0.25rem;
border-radius: 3px;
outline: 3px solid currentColor;
vertical-align: -0.1em;
}
/* --- Style guide: palette strips. Each color family is a contiguous strip
of steps over a ledger that names every one. The name and value sit under
the color in ordinary ink, so no caption depends on the color above it.
The grounds are the brand's fixed hex values, theme-independent. --- */
.sg-tints { display: flex; flex-wrap: wrap; row-gap: 1.1rem; margin-top: 1.25rem; }
.sg-tint { flex: 0 0 50%; }
@media (min-width: 640px) { .sg-tints { flex-wrap: nowrap; } .sg-tint { flex: 1 1 0; } }
.sg-tint > i { display: block; height: 4.5rem; }
@media (min-width: 640px) { .sg-tint > i { height: 6.5rem; } }
.sg-tint:first-child > i { border-radius: 12px 0 0 12px; }
.sg-tint:last-child > i { border-radius: 0 12px 12px 0; }
.sg-tint:only-child > i { border-radius: 12px; }
/* A step too close to the page ground to show its own edge. */
.sg-tint--edge > i { box-shadow: inset 0 0 0 1px var(--line); }
.sg-tint > b {
display: block;
margin-top: 0.6rem;
padding-right: 0.6rem;
font-family: theme('fontFamily.label');
font-weight: 400;
font-size: 0.9375rem;
line-height: 1.3;
color: var(--ink);
}
.sg-main .sg-tint > code { margin-top: 0.25rem; font-size: 0.8125rem; }
/* The palette tokens never change with the theme, so these point at them
rather than repeating the hex. Deep-tone·alt has no token: its value is the
README's. */
.sg-chip-navy { background: var(--navy); }
.sg-chip-sky { background: var(--sky); }
.sg-chip-cyan { background: var(--cyan); }
.sg-chip-coral { background: var(--coral); }
.sg-chip-midnight { background: var(--midnight); }
.sg-chip-paper { background: var(--paper); }
.sg-chip-seagrass { background: var(--seagrass); }
.sg-chip-lantern { background: var(--lantern); }
.sg-chip-sand { background: var(--sand); }
.sg-chip-deep-tone { background: #2E3F72; }
.sg-ground-paper { background: var(--paper); color: var(--navy); }
.sg-ground-midnight { background: var(--midnight); color: var(--paper); }
.sg-ground-navy { background: var(--navy); color: var(--paper); }
.sg-favicon-dark { background: var(--midnight); border-radius: 4px; padding: 2px; }
/* --- Style guide: specimen plates. A specimen sits on the ground it ships
on. The base plate is Surface with a hairline; the navy plate is the
booking band's ground and takes .on-dark beside it for the Sky focus ring.
.sg-theme-light and .sg-theme-dark (defined with the tokens) re-point a
plate at one palette, whatever theme the page is in. --- */
.sg-plate {
margin-top: 1.5rem;
padding: 1.75rem 1.5rem;
border: 1px solid var(--line);
border-radius: theme('borderRadius.card');
background: var(--surface);
color: var(--ink);
}
.sg-plate--navy { background: var(--navy); border-color: var(--navy); color: var(--paper); }
.sg-plate--navy :is(h3, h4) { color: var(--paper); }
.sg-plate-cap { margin-top: 0.6rem; font-size: 0.9375rem; line-height: 1.5; color: var(--ink-soft); }
.sg-row { display: flex; flex-wrap: wrap; align-items: center; gap: 1rem 1.25rem; }
/* A forced focus ring, for showing the ring without asking the reader to tab. */
.sg-ring-demo { outline: 3px solid var(--ring); outline-offset: 2px; }
.sg-plate--navy .sg-ring-demo { outline-color: var(--focus-on-dark); }
/* A specimen that must not act: the button keeps its look and loses its pointer. */
.sg-inert { pointer-events: none; }
/* Mark and lockup tiles are fixed to their documented ground per
assets/logomarks/README.txt, so they stay correct in either theme. */
.sg-tile {
border: 1px solid var(--line);
border-radius: theme('borderRadius.card');
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.85rem;
padding: 2rem 1.25rem 1.4rem;
text-align: center;
}
.sg-tile img { width: auto; }
.sg-tile p {
margin: 0;
max-width: none;
font-family: theme('fontFamily.label');
font-size: 0.8125rem;
letter-spacing: 0.02em;
color: inherit;
}
.sg-tile .sg-tile-hex { font-family: theme('fontFamily.body'); font-size: 0.8125rem; opacity: 0.8; }
.sg-tiles { display: grid; gap: theme('gap.gutter'); grid-template-columns: repeat(2, minmax(0, 1fr)); margin-top: 1.5rem; }
@media (min-width: 768px) { .sg-tiles--5 { grid-template-columns: repeat(5, minmax(0, 1fr)); } }
@media (max-width: 479px) { .sg-tiles--2 { grid-template-columns: minmax(0, 1fr); } }
/* --- Style guide: type specimens. The faces at poster size, then each role
set live from its own token beside its name and numbers. --- */
.sg-faces { display: grid; gap: 1.5rem theme('gap.gutter'); margin-top: 1.75rem; }
@media (min-width: 640px) { .sg-faces { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.sg-face { border-top: 1px solid var(--line); padding-top: 1rem; }
.sg-face-big { font-size: clamp(3.25rem, 2rem + 5vw, 5.25rem); line-height: 1; color: var(--ink); }
.sg-face-name { margin-top: 0.75rem; font-family: theme('fontFamily.label'); font-size: theme('fontSize.h4[0]'); line-height: 1.2; color: var(--ink); }
.sg-face-note { margin-top: 0.35rem; color: var(--ink-soft); }
.sg-face-alpha { margin-top: 0.5rem; font-size: 1.25rem; line-height: 1.4; color: var(--ink); overflow-wrap: anywhere; }
.sg-face--logo :is(.sg-face-big, .sg-face-alpha) { font-family: theme('fontFamily.logo'); letter-spacing: 0.04em; text-transform: uppercase; }
.sg-face--label :is(.sg-face-big, .sg-face-alpha) { font-family: theme('fontFamily.label'); }
.sg-face--mono :is(.sg-face-big, .sg-face-alpha) { font-family: theme('fontFamily.mono'); }
.sg-specimens { margin-top: 1.75rem; }
.sg-specimen {
display: grid;
gap: 0.6rem;
padding-block: 1.4rem;
border-top: 1px solid var(--line);
align-items: baseline;
}
.sg-specimen:last-child { border-bottom: 1px solid var(--line); }
@media (min-width: 768px) {
.sg-specimen { grid-template-columns: 12.5rem minmax(0, 1fr); column-gap: 2rem; }
}
.sg-specimen-meta { font-size: 0.9375rem; line-height: 1.5; color: var(--ink-soft); }
.sg-specimen-meta > b {
display: block;
margin-bottom: 0.3rem;
font-family: theme('fontFamily.label');
font-weight: 400;
font-size: 0.8125rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--ink);
}
.sg-sample { color: var(--ink); overflow-wrap: anywhere; }
.sg-spec-h1 { font-family: theme('fontFamily.label'); font-size: theme('fontSize.h1'); line-height: 1.08; letter-spacing: -0.005em; }
.sg-spec-h2 { font-family: theme('fontFamily.label'); font-size: theme('fontSize.h2'); line-height: 1.18; }
.sg-spec-h3 { font-family: theme('fontFamily.label'); font-size: theme('fontSize.h3'); line-height: 1.25; }
.sg-spec-h4 { font-family: theme('fontFamily.label'); font-size: theme('fontSize.h4[0]'); line-height: 1.3; }
.sg-spec-row { font-family: theme('fontFamily.label'); font-size: 1.0625rem; }
.sg-spec-caption { font-size: 0.875rem; }
.sg-spec-label-sm { font-family: theme('fontFamily.label'); font-size: 0.8125rem; letter-spacing: 0.08em; text-transform: uppercase; }
.sg-spec-overline { font-family: theme('fontFamily.label'); font-size: 0.75rem; letter-spacing: 0.12em; text-transform: uppercase; }
/* --- Style guide: depth and shape specimens. One card per shadow or radius,
drawn with the real value. --- */
.sg-depths { display: grid; gap: 1.75rem theme('gap.gutter'); margin-top: 1.75rem; grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (min-width: 768px) { .sg-depths { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
.sg-depth {
display: flex;
align-items: flex-end;
min-height: 5.5rem;
padding: 0.75rem 0.9rem;
border: 1px solid var(--line);
border-radius: theme('borderRadius.card');
background: var(--surface);
font-family: theme('fontFamily.label');
font-size: 0.9375rem;
color: var(--ink);
}
.sg-depth--rest { box-shadow: theme('boxShadow.card'); }
.sg-depth--lift { box-shadow: theme('boxShadow.lift'); }
.sg-depth--float { box-shadow: 0 18px 44px -14px rgba(10, 15, 36, 0.32); }
.sg-depth--tab { box-shadow: -2px 2px 16px rgba(10, 15, 36, 0.22); }
.sg-depth--r12 { border-radius: 12px; }
.sg-depth--r10 { border-radius: 10px; }
.sg-depth--r8 { border-radius: 8px; }
.sg-depth--round { border-radius: 999px; justify-content: center; align-items: center; width: 5.5rem; }
/* --- Grid demo: the real .grid-9 tracks at the real gutter, so the strip is
the thing it documents rather than a picture of it. It sits in a .table-wrap
with a min-width, which is how the guide handles anything too wide to fold. --- */
.sg-grid-demo {
display: grid;
grid-template-columns: repeat(9, minmax(0, 1fr));
gap: theme('gap.gutter');
}
.sg-grid-demo + .sg-grid-demo { margin-top: theme('gap.gutter'); }
.sg-grid-demo > * {
display: flex;
align-items: center;
justify-content: center;
height: 2.5rem;
border-radius: 6px;
background: var(--tag-bg);
font-family: theme('fontFamily.label');
font-size: 0.75rem;
color: var(--ink-soft);
white-space: nowrap;
}
/* The two filled spans borrow token pairs that are already contrast-checked in
both themes. The *-ink accents point back at the raw palette in dark, so
Paper on them would land near 2.3:1 — these pairs put dark ink on the bright
ground instead, the way the primary button and the Before/After panel do. */
.sg-grid-demo > .sg-grid-fill {
background: var(--cta-bg);
color: var(--cta-ink);
}
.sg-grid-demo > .sg-grid-fill--alt {
background: var(--ba-after-bg);
color: var(--ba-after-ink);
}
/* The demo documents the >=1024px behavior, so it keeps its nine tracks at
every width — .grid-9's own collapse rule would otherwise flatten it here
and the spec would illustrate nothing. */
.sg-grid-demo > * { grid-column: span 1; }
.sg-grid-demo > .sg-span-3 { grid-column: span 3; }
.sg-grid-demo > .sg-span-4 { grid-column: span 4; }
.sg-grid-demo > .sg-span-5 { grid-column: span 5; }
.sg-grid-demo > .sg-span-6 { grid-column: span 6; }
.sg-grid-demo > .sg-span-9 { grid-column: span 9; }
/* Unused columns: outlined, not filled, so white space reads as space. */
.sg-grid-demo > .sg-grid-ghost {
background: none;
border: 1px dashed var(--line);
color: var(--ink-soft);
}
/* Annotations over a diagram, in the label voice. The wireframes caption every
frame the same way, so their captions take this by position rather than
carrying the class on 42 elements. */
.sg-grid-label,
.sg-wire-frames > div > p {
margin-top: 1.25rem;
margin-bottom: 0.45rem;
font-family: theme('fontFamily.label');
font-size: 0.75rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--ink-soft);
}
.sg-grid-label:first-child,
.sg-wire-frames > div > p { margin-top: 0; }
/* --- Theme panels: the same card rendered in each palette, side by side, so
light and dark can be compared without toggling the page. --- */
.sg-theme-pair { display: grid; gap: theme('gap.gutter'); margin-top: 1.5rem; }
@media (min-width: 640px) { .sg-theme-pair { grid-template-columns: 1fr 1fr; } }
.sg-theme-panel {
background: var(--bg);
color: var(--ink);
border: 1px solid var(--line);
border-radius: theme('borderRadius.card');
padding: 1.5rem;
}
.sg-theme-panel .sg-theme-name {
font-family: theme('fontFamily.label');
font-size: 0.8125rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--ink-soft);
}
.sg-theme-swatches { display: flex; gap: 0.4rem; margin-top: 1rem; }
.sg-theme-swatches > span {
width: 1.75rem;
height: 1.75rem;
border-radius: 5px;
border: 1px solid var(--line);
}
/* --- Layout wireframes: one schematic per layout the site actually uses.
Plain boxes on the real tokens — a picture of the tracks, not a copy of
the section, so a diagram stays true when the copy inside it changes.
Each frame sets its own tracks through the .sg-wf-row* classes rather than
Tailwind's `grid`, which keeps reader mode's flatten-everything rule off
the diagrams: a wireframe collapsed to one column illustrates nothing.
The vocabulary is flat, not BEM: every block and every row variant carries
the shared base through the :is() lists below, so markup writes
`class="sg-wf-card sg-wf-tall"` rather than repeating a base class on 230
elements. Order matters — each base rule comes before the variants that
override it. --- */
.sg-wire { border-top: 1px solid var(--line); padding-block: 2rem; }
.sg-wire:first-child { border-top: 0; padding-top: 0.5rem; }
.sg-wire-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.15rem 0.85rem; }
/* The layout's name and, beside it, where it lives. */
.sg-wire-head h4 { margin-top: 0; font-size: theme('fontSize.h4[0]'); line-height: 1.3; }
.sg-wire-where {
margin: 0;
font-family: theme('fontFamily.mono');
font-size: 0.75rem;
color: var(--ink-soft);
}
/* The wide frame with the phone frame beside it. Both stack on a narrow
screen, and the phone frame keeps its width there — it reads as a phone
because it is narrow, so letting it fill the column would lose the point.
Widths are in rem, so the whole diagram grows with the text-size setting
instead of being zoomed back down. */
.sg-wire-frames { display: grid; gap: 1.1rem; margin-top: 1.25rem; align-items: start; }
@media (min-width: 640px) {
.sg-wire-frames { grid-template-columns: minmax(0, 1fr) 9rem; gap: 1.75rem; }
.sg-wire-frames--trio { grid-template-columns: minmax(0, 1fr) 9rem 9rem; }
.sg-wire-frames--duo { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.sg-wire-frame {
border: 1px solid var(--line);
border-radius: 10px;
background: var(--bg);
padding: 0.5rem;
display: grid;
gap: 0.4rem;
align-content: start;
}
.sg-wire-frame--phone { max-width: 9rem; }
/* The key: each block style beside what it stands for. */
.sg-wf-key-list { display: grid; gap: 0.7rem 1.5rem; margin-top: 0.8rem; }
@media (min-width: 560px) { .sg-wf-key-list { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 900px) { .sg-wf-key-list { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.sg-wf-key-list > li {
display: grid;
grid-template-columns: 3.75rem minmax(0, 1fr);
align-items: center;
gap: 0.7rem;
font-size: 0.9375rem;
color: var(--ink-soft);
}
/* The reading column drawn inside the viewport: dashed, because the canvas
is a width limit, not a thing you can see. The inset stands in for the cap
plus the side padding — a proportion, not a measurement. */
.sg-wf-canvas {
margin-inline: 9%;
padding: 0.35rem;
border: 1px dashed var(--wf-line, var(--line));
border-radius: 7px;
display: grid;
gap: 0.4rem;
align-content: start;
}
.sg-wf-canvas--wide { margin-inline: 4%; }
/* A band whose ground runs the full width with only its content in the
canvas — the closing booking panel. It re-points the block tokens rather
than reaching in with descendant selectors, so a CTA on the navy ground
still reads as a CTA, the way .btn-primary does on dark. */
.sg-wf-band {
margin: -0.5rem;
padding: 0.5rem 0;
border-radius: 9px;
background: var(--tag-bg);
}
.sg-wf-band--dark {
background: var(--navy);
--wf-bg: rgba(245, 251, 255, 0.16);
--wf-ink: var(--paper);
--wf-key-bg: var(--sky);
--wf-key-ink: var(--navy);
--wf-line: rgba(245, 251, 255, 0.35);
}
/* Rows and stacks inside a frame. A row that has real proportions — the
hero's 4:5, the fine print's 1.45:1 — states them; the rest split evenly. */
.sg-wf-stack { display: grid; gap: 0.4rem; align-content: start; }
:is(.sg-wf-row, .sg-wf-row-4, .sg-wf-row-5, .sg-wf-row-hero,
.sg-wf-row-rail, .sg-wf-row-aside, .sg-wf-row-doc, .sg-wf-row-icon,
.sg-wf-row-bar) {
display: grid;
gap: 0.4rem;
grid-template-columns: repeat(2, minmax(0, 1fr));
align-items: stretch;
}
.sg-wf-row-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.sg-wf-row-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
.sg-wf-row-hero { grid-template-columns: 4fr 5fr; }
.sg-wf-row-rail { grid-template-columns: 22fr 56fr; }
.sg-wf-row-aside { grid-template-columns: 16fr 54fr; }
.sg-wf-row-doc { grid-template-columns: 1.45fr 1fr; }
.sg-wf-row-icon { grid-template-columns: 1.5rem minmax(0, 1fr); gap: 0.3rem; }
.sg-wf-row-bar { grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; }
/* Closes a stack or a row up; reads the same on either. */
.sg-wf-tight { gap: 0.25rem; }
/* The empty middle track of a bar row: the space between the two ends, with
nothing in it to draw. */
.sg-wf-gap { min-width: 1rem; }
/* A block held narrower than the column it sits in — the intake card. */
.sg-wf-measure { width: 78%; margin-inline: auto; }
/* How it works: the same four grid areas the real steps take, cards pinned to
the outer edges of a two-column block with the thread weaving the gap. */
.sg-wf-zig {
position: relative;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.4rem 1.5rem;
}
.sg-wf-zig::before {
content: "";
position: absolute;
top: 0.75rem;
bottom: 0.75rem;
left: 50%;
border-left: 1px dashed var(--line);
}
/* width, not max-width: these are flex containers, and justify-self would
otherwise shrink them to their label. */
.sg-wf-zig > * { width: 88%; }
.sg-wf-zig > :nth-child(1) { grid-area: 1 / 1; justify-self: start; }
.sg-wf-zig > :nth-child(2) { grid-area: 2 / 2; justify-self: end; }
.sg-wf-zig > :nth-child(3) { grid-area: 3 / 1; justify-self: start; }
.sg-wf-zig > :nth-child(4) { grid-area: 4 / 2; justify-self: end; }
/* Float demos: the vignette photo, and About between 450 and 699px. */
.sg-wf-float::after { content: ""; display: block; clear: both; }
.sg-wf-float > .sg-wf-floated { float: right; width: 40%; margin: 0 0 0.4rem 0.4rem; }
.sg-wf-float > :not(.sg-wf-floated) { margin-bottom: 0.4rem; }
/* The blocks themselves. Labels are set small and centered; a block that runs
out of room clips rather than pushing the diagram out of shape. The two
tokens let a band re-point every block inside it in one place. */
:is(.sg-wf-b, .sg-wf-xs, .sg-wf-tall, .sg-wf-xl, .sg-wf-card, .sg-wf-photo,
.sg-wf-key, .sg-wf-alt, .sg-wf-ghost, .sg-wf-bleed, .sg-wf-floated) {
display: flex;
align-items: center;
justify-content: center;
min-height: 1.5rem;
padding: 0.2rem 0.3rem;
border-radius: 5px;
background: var(--wf-bg, var(--tag-bg));
font-family: theme('fontFamily.label');
font-size: 0.6875rem;
line-height: 1.2;
letter-spacing: 0.01em;
text-align: center;
color: var(--wf-ink, var(--ink-soft));
overflow: hidden;
}
.sg-wf-xs { min-height: 0.7rem; }
.sg-wf-tall { min-height: 3rem; }
.sg-wf-xl { min-height: 5rem; }
/* The filled blocks borrow token pairs already contrast-checked in both
themes, same as the grid demo's spans. */
.sg-wf-key { background: var(--wf-key-bg, var(--cta-bg)); color: var(--wf-key-ink, var(--cta-ink)); }
.sg-wf-alt { background: var(--ba-after-bg); color: var(--ba-after-ink); }
.sg-wf-card { background: none; border: 1px solid var(--wf-line, var(--line)); }
/* Dashed = space the layout leaves empty, or a block that isn't there at
this width. Never a thing you can see on the page. */
.sg-wf-ghost { background: none; border: 1px dashed var(--wf-line, var(--line)); }
/* Photography, hatched so it doesn't read as another panel. */
.sg-wf-photo {
background-image: repeating-linear-gradient(-45deg,
transparent 0 0.3rem, var(--line) 0.3rem 0.36rem);
}
/* A block that bleeds off the right edge of the frame — the hero photo. */
.sg-wf-bleed { margin-right: -0.5rem; border-radius: 5px 0 0 5px; }
/* Overlay frame: the contact drawer over a dimmed page, plus the tab that
opens it. The scrim and panel are absolutely placed the way the real ones
are fixed, so the diagram keeps the drawer's proportion (min(30rem, 100vw)
against the viewport). */
.sg-wf-overlay { position: relative; }
.sg-wf-scrim {
position: absolute;
inset: 0;
border-radius: 9px;
background: rgba(10, 15, 36, 0.55);
}
.sg-wf-drawer {
position: absolute;
inset: 0 0 0 auto;
width: 46%;
padding: 0.4rem;
border-left: 1px solid var(--line);
border-radius: 0 9px 9px 0;
background: var(--bg);
display: grid;
gap: 0.3rem;
align-content: start;
}
.sg-wf-tab {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
width: 0.85rem;
height: 3.25rem;
border-radius: 5px 0 0 5px;
background: var(--sky);
}
/* --- Style guide on paper: kept here, inside the layer, rather than in the
PRINT section at the end of the file. That section is outside @layer and so
is never purged; these rules name only the guide's classes, and in here
they land in style-guide.css alone. They follow the furniture above, so
they still win on source order. Every selector must carry a class: one
rule without one (an attribute selector, say) and Tailwind keeps the whole
@media block in both stylesheets. --- */
@media print {
/* The bar is navigation, so it goes; the masthead's navy becomes plain
type, and the contents folds to two columns. */
.sg-bar { display: none; }
.sg-toc a[aria-current] { color: var(--ink-soft); text-decoration: none; }
.sg-mast { padding: 1rem 0 0; background: none; color: var(--ink); }
.sg-mast h1, .sg-mast-lede { color: var(--ink); }
.sg-mast-maze, .sg-mast-key { display: none; }
.sg-shell { display: block; max-width: none; padding: 1rem 0 0; }
.sg-toc { position: static; max-height: none; overflow: visible; margin-bottom: 1.5rem; }
.sg-toc ol { columns: 2; column-gap: 2rem; }
.sg-main > section { margin-top: 1.75rem; padding-top: 1.5rem; }
.sg-main :is(h2, h3, h4) { break-after: avoid; }
.sg-scroll { overflow: visible; }
.sg-scroll > .sg-table { min-width: 0; }
.sg-table tr, .sg-specimen, .sg-rules > li, .sg-plate, .sg-tile, .sg-theme-panel { break-inside: avoid; }
/* Color is the content of these, so they print. */
.sg-tint > i,
.sg-chip,
.sg-pair,
.sg-plate--navy,
.sg-theme-panel,
.sg-theme-swatches > span,
.sg-grid-demo > *,
.sg-tile,
.sg-favicon-dark {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
/* So are the wireframes. Their blocks are filled shapes, and a printer
that drops the fills leaves a page of empty outlines. */
.sg-wf-b, .sg-wf-xs, .sg-wf-tall, .sg-wf-xl, .sg-wf-photo, .sg-wf-key,
.sg-wf-alt, .sg-wf-bleed, .sg-wf-floated, .sg-wf-band,
.sg-wf-tab,
.sg-wf-scrim {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
/* The wide frame and the phone frame side by side, so a layout and its
phone form stay on one line of the printed page. */
.sg-wire { break-inside: avoid; }
.sg-wire-frames { grid-template-columns: minmax(0, 1fr) 9rem; gap: 1.5rem; }
.sg-wire-frames--trio { grid-template-columns: minmax(0, 1fr) 9rem 9rem; }
.sg-wire-frames--duo { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
}
/* ============================================================================
UTILITIES
========================================================================== */
@layer utilities {
.on-dark { color: var(--paper); }
.grid-9 { display: grid; grid-template-columns: repeat(9, minmax(0, 1fr)); gap: theme('gap.gutter'); }
@media (max-width: 1023px) {
.grid-9 { grid-template-columns: 1fr; }
.grid-9 > * { grid-column: auto !important; }
}
/* Hero: keep the framed photo beside the heading below the lg breakpoint too,
mirroring the desktop split, instead of dropping it full-width under the
copy. Tablet range keeps the whole illustration at its natural ratio. */
@media (max-width: 1023px) {
#top .grid-9 { grid-template-columns: minmax(0, 1fr) 38%; }
#top .grid-9 > div:nth-of-type(2) { margin-top: 0; }
}
/* Below 500px the hero image itself is hidden (index.html), so collapse
back to a single column instead of reserving the 38% track for nothing. */
@media (max-width: 499px) {
#top .grid-9 { grid-template-columns: 1fr; }
}
/* Below the md breakpoint the column is too narrow for the full 4:3
illustration, so the figure stretches to the copy's height and the photo
covers it. The focus sits just left of the figures (~78%): they stay in
frame at every width, and the maze on their left fills more of the strip
as the screen widens, less as it narrows. */
@media (max-width: 767px) {
#top .grid-9 > div:nth-of-type(2) { align-self: start; margin-top: 0.5rem; }
#top .grid-9 > div:nth-of-type(2) .framed img {
width: 100%;
aspect-ratio: 1 / 2;
max-height: 498px;
object-fit: cover;
object-position: 85% center;
}
}
/* Wherever the hero photo shows, run it flush to the browser's right edge:
pull it across .canvas's 2rem padding plus the gutter that opens once
.canvas hits its 1000px cap. Square off the right side so it reads as
bleeding off-screen. The people sit on the right, so they stay in view. */
@media (min-width: 768px) {
#top .grid-9 > div:nth-of-type(2) { align-self: stretch; }
#top .grid-9 > div:nth-of-type(2) .framed { height: 100%; max-height: 500px; }
#top .grid-9 > div:nth-of-type(2) .framed img {
height: 100%;
object-fit: cover;
object-position: 82% 80%;
}
}
@media (min-width: 500px) {
#top .grid-9 > div:nth-of-type(2) {
margin-right: calc(min(0px, (1000px - 100vw) / 2) - 2rem);
}
#top .grid-9 > div:nth-of-type(2) .framed {
border-radius: theme('borderRadius.card') 0 0 theme('borderRadius.card');
border-right: 0;
}
}
/* About: at >=700px the left column (heading + portrait) is one sticky rail beside
the scrolling bio. The rail only needs room for the ~200px portrait, so it gets a
fixed track rather than a 4/9 share that would strand a gap before the bio.
Between 450 and 699 the rail goes full-width and the portrait floats right so the
bio wraps around it. Below 450 everything stacks. */
@media (min-width: 700px) {
#about .grid-9 { grid-template-columns: 18rem minmax(0, 1fr); column-gap: 3rem; }
#about .about-rail { position: sticky; top: calc(var(--header-h) + 1.5rem); align-self: start; }
}
@media (min-width: 450px) and (max-width: 699px) {
#about .grid-9 { display: block; }
#about .grid-9::after { content: ""; display: block; clear: both; }
#about .about-rail > .meta { margin-bottom: 1.5rem; }
#about .about-rail > figure { float: right; width: 45%; margin: 0 0 1rem 1.75rem; }
}
@media (min-width: 1024px) {
#about .grid-9 { grid-template-columns: 22rem minmax(0, 1fr); column-gap: 4rem; }
}
/* Paper text at reduced strength, for copy on the navy/midnight grounds. */
.on-dark-soft { color: rgba(245, 251, 255, 0.82); }
/* Stagger index for [data-rise] entrance animations. */
.rise-0 { --i: 0; }
.rise-1 { --i: 1; }
.rise-3 { --i: 3; }
}
/* ============================================================================
PRINT
Unlayered, so it follows Tailwind's utilities and wins on source order.
Written for the browser default of "background graphics" off: anything that
has to reach paper is carried by text color or a border, never a fill.
Margin boxes are Chromium-only; other engines fall back to the footer URL.
========================================================================== */
@page {
margin: 0.6in 0.65in 0.7in;
@bottom-left {
/* The number is split across strings so it never sits whole in site.css. */
content: "Coastal Healthcare Advocates \00B7 (757) " "574-" "0771 (Monday\2013 Friday, 8:00 AM\2013 5:00 PM ET) \00B7 coastalhealthcareadvocates.org";
font-family: Questrial, "Century Gothic", system-ui, sans-serif;
font-size: 8pt;
color: #3C4A73;
}
@bottom-right {
content: counter(page) " of " counter(pages);
font-family: "IBM Plex Mono", ui-monospace, monospace;
font-size: 8pt;
color: #3C4A73;
}
}
@media print {
/* Paper is always the light theme, whatever the screen was set to. The triple
:root outranks both dark blocks ((0,2,0) each). */
:root:root:root {
--bg: #FFFFFF;
--surface: #FFFFFF;
--ink: #0F1E4A;
--ink-soft: #3C4A73;
--line: #C9D3E6;
--line-soft: #DDE4F2;
--tag-bg: #FFFFFF;
--warn-bg: #FFFFFF;
--warn-line: #A9B8D4;
--warn-ink: #2F4468;
--link: #0B5FA5;
--ring: #0F1E4A;
--sky-ink: #1F74C4;
--seagrass-ink: #087A58;
--lantern-ink: #A85A00;
--coral-ink: #C4140F;
--ba-after-bg: var(--seagrass-ink);
--ba-after-ink: #FFFFFF;
--brand-mark: url('../logomarks/marks/advocate-beacon-primary.svg');
--brand-ink: var(--navy);
--brand-sub: var(--sky-ink);
color-scheme: light;
}
/* Every rem shrinks together: 18px body copy lands at ~11pt. */
html { font-size: 13px; scroll-padding-top: 0; }
body { background: #FFFFFF; color: var(--ink); line-height: 1.5; }
*, *::before, *::after {
animation: none !important;
transition: none !important;
box-shadow: none !important;
text-shadow: none !important;
}
/* Entrance start-states hold content invisible until it scrolls into view. */
.reveal-pending,
.reveal-pending > *,
.reveal-pending tr,
[data-rise],
.how-flow .how-step { opacity: 1 !important; transform: none !important; }
.reveal-pending::after { transform: none !important; }
/* Scroll containers don't fragment across pages; let them flow. */
.table-wrap,
[data-turnstile-field] { overflow: visible !important; }
/* --- Screen-only chrome and interactions --- */
body::after,
.skip-link,
.site-header nav,
#nav-toggle,
.drawer-tab,
#contact-backdrop,
#contact-modal,
#intake,
.path-card ul,
p:has(> button[data-open-contact]),
button[data-open-contact],
p:has(> a[href$=".pdf"]),
p:has(> a.link-quiet[href="index.html"]),
.hero-atmos::before,
.path-card::before,
.how-flow::before,
.how-spine,
.how-pin,
.ba-list::before,
.faq-sign,
.theme-switch,
.theme-orb,
.site-footer nav,
.site-footer .brand-lockup,
.site-footer div:has(> a > .icon) { display: none !important; }
/* --- Type and rhythm --- */
h1, h2, h3, h4 { break-after: avoid; }
p, li, dd { orphans: 3; widows: 3; }
img, svg, figure, tr, .card, .callout, .ba-list > li, .dl-rows > div { break-inside: avoid; }
a[href^="#"],
a[href^="tel:"],
a[href^="mailto:"] { text-decoration: none; color: inherit; }
main a[href^="http"]:not(.btn):not([href^="https://coastalhealthcareadvocates.org"])::after {
content: " (" attr(href) ")";
font-size: 0.85em;
color: var(--ink-soft);
overflow-wrap: anywhere;
}
.canvas,
.canvas-wide { max-width: none; padding-inline: 0; }
main > section { background: none !important; border: 0 !important; }
.section,
.section--tight { padding-block: 1.1rem; }
/* The before/after marks are a colored disc with a white glyph; print drops
background fills, so redraw them as an ink glyph inside a thin ring. */
.ba-mark {
background: none !important;
border: 1pt solid var(--coral-ink);
color: var(--coral-ink);
}
.ba-mark--yes { border-color: var(--seagrass-ink); color: var(--seagrass-ink); }
.ba-mark svg { stroke: currentColor; }
/* The Sky accent rule under section headings is a fill; redraw it as a border. */
.section h2[id$="-h"]::after,
.section--tight h2[id$="-h"]::after {
height: 0;
width: 4.5rem;
margin-block: 0.55rem 1rem;
background: none;
border-top: 2px solid var(--sky);
}
/* --- Header: the logo alone, once, at the top of page one --- */
.site-header {
position: static;
background: none;
backdrop-filter: none;
border-bottom: 1px solid var(--line);
transform: none !important;
opacity: 1 !important;
visibility: visible !important;
}
.site-header > .canvas-wide { padding-block: 0 0.9rem; }
.brand-lockup {
--logo-h: 0.6in;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
/* --- Hero: the photo is atmosphere, not content — drop it and let the
heading column take the full width. --- */
#top .grid-9 { display: block; }
#top .grid-9 > div:nth-of-type(2) { display: none; }
.hero-actions,
.hero-byline { margin-top: 1.25rem; }
/* --- Two entry paths --- */
.path-card { padding: 1rem 1.2rem; gap: 0.5rem; }
/* --- What is an advocate: the vignette photo is atmosphere, not content --- */
.vignette-figure { display: none; }
.callout.p-7 { padding: 1.1rem 1.3rem; }
/* --- Services: two columns of rows --- */
.service-rows { grid-template-columns: 1fr 1fr; column-gap: 2rem; }
.service-rows.mt-10 { margin-top: 0.5rem; }
.service-row {
grid-template-columns: 2.25rem minmax(0, 1fr);
column-gap: 0.8rem;
padding-block: 0.85rem;
break-inside: avoid;
background: none;
}
.service-row > .service-icon { width: 2.25rem; height: 2.25rem; background: none; }
.service-row > .service-icon .icon-lg { width: 2rem; height: 2rem; }
.who-photo { display: none; }
/* --- Who we help --- */
.who-list { grid-template-columns: 1fr 1fr; gap: 1.1rem 2rem; }
.who-list.mt-10 { margin-top: 0.5rem; }
.who-item { grid-template-columns: 2.25rem minmax(0, 1fr); column-gap: 0.8rem; break-inside: avoid; }
/* --- About: the portrait is a people photo, not content --- */
#about .grid-9 { display: block; }
#about .about-rail { position: static; }
#about .about-rail > figure { display: none; }
.about-bio .mt-12 { margin-top: 1.75rem; }
.dl-rows.mt-8 { margin-top: 0.75rem; }
.dl-rows > div { grid-template-columns: 8.5rem 1fr; padding-block: 0.55rem; }
/* --- How it works: a flat 2x2 of cards, each keyed by its step color --- */
.how-flow { margin-top: 0.5rem; }
.how-steps {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem 1.25rem;
align-items: stretch;
}
.how-step {
grid-area: auto !important;
justify-self: stretch !important;
max-width: none;
break-inside: avoid;
}
.how-card {
height: 100%;
padding: 0;
rotate: none !important;
scale: none !important;
border: 1px solid color-mix(in srgb, var(--how-accent, var(--sky)) 55%, var(--line));
border-radius: 12px;
}
.how-card-inner { height: 100%; background: none; border: 0; padding: 0.8rem 1rem 1rem; }
.step-num { font-size: 2rem; }
/* --- Pricing --- */
.price-choices { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.5rem; margin-top: 1rem; }
.price-choice { padding: 0.6rem 0.75rem; break-inside: avoid; }
.price-choice-fee { font-size: 1.1rem; }
.price-choice-cta { display: none; }
.price-choice--start { background: none; }
.price-table { min-width: 0; }
.price-table thead { display: table-header-group; }
.price-table th,
.price-table td { padding: 0.5rem 0.85rem; }
.price-group { background: none; padding-top: 0.75rem; padding-bottom: 0.3rem; }
/* --- FAQs: every answer open --- */
.faq-layout { display: block; }
.faq-aside { position: static; margin-bottom: 0.75rem; }
.faq-more { max-width: none; }
.faq-item { break-inside: avoid; }
.faq-trigger { padding: 0.8rem 0 0.35rem; font-size: 1.2rem; }
.faq-panel > * { padding-bottom: 0.85rem; }
/* A closed <details> keeps its answer in ::details-content, which the browser
hides with content-visibility. Kept in a rule of its own: a browser that
doesn't know the pseudo-element (before Chrome 131, Safari 18.4 and
Firefox 143) drops the whole rule, and prints only the questions. */
.faq-item::details-content { content-visibility: visible; }
/* --- Closing booking panel: navy fill becomes a navy outline --- */
main > #schedule {
margin-top: 1rem;
padding-block: 1.25rem;
border: 1.5px solid var(--navy) !important;
border-radius: 12px;
color: var(--ink);
break-inside: avoid;
}
#schedule :is(h2, p, .text-paper, .on-dark-soft) { color: var(--ink); }
#schedule .schedule-actions { flex-direction: column; align-items: center; gap: 0.2rem; margin-top: 0.9rem; }
#schedule .btn {
display: inline;
min-height: 0;
padding: 0;
border: 0;
background: none;
color: var(--ink);
font-family: theme('fontFamily.body');
font-size: 1.05rem;
letter-spacing: 0;
text-transform: none;
white-space: normal;
}
#schedule .btn .icon { display: none; }
#schedule a.btn[href*="calendly.com"]::after {
content: "\2014 calendly.com/coastalhealthcareadvocates/30min";
}
/* The inline scheduler doesn't print: the fallback link and its address do. */
.calendly-frame,
.calendly-loading,
.calendly-embed.is-live .calendly-alt { display: none; }
.calendly-embed.is-live .calendly-fallback { display: block; }
/* --- Footer: tagline, one contact line, badge + disclaimers. Kept short so
break-inside: avoid never strands it alone on a page. --- */
.site-footer {
background: none !important;
border-top: 1px solid var(--line);
margin-top: 1.25rem;
break-inside: avoid;
}
.site-footer,
.site-footer * { color: var(--ink) !important; }
.site-footer .meta,
.site-footer .meta *,
.site-footer address span { color: var(--ink-soft) !important; }
.site-footer .canvas-wide { padding-block: 0.8rem 0; }
/* Tagline and contact line stack on the left; the QR code sits to their right. */
.site-footer .canvas-wide > div:first-child {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-content: center;
gap: 0.35rem 1.5rem;
}
.site-footer .canvas-wide > div:first-child > :not(.footer-qr) { grid-column: 1; }
.site-footer .canvas-wide > div:first-child .meta { margin-top: 0; }
.site-footer .footer-qr {
display: flex;
flex-direction: column;
align-items: center;
/* The SVG has no quiet zone of its own; keep the caption 4+ modules clear. */
gap: 0.12in;
grid-column: 2;
grid-row: 1 / span 2;
margin: 0;
break-inside: avoid;
}
.site-footer .footer-qr img { width: 0.8in; height: 0.8in; }
.site-footer .footer-qr figcaption { font-size: 0.75rem; }
.site-footer .footer-col-title { display: none; }
.site-footer address { flex-direction: row; flex-wrap: wrap; gap: 0.1rem 1.5rem; }
.site-footer address::after { content: "coastalhealthcareadvocates.org"; }
.site-footer .canvas-wide > div:last-child {
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-top: 0.7rem;
padding-top: 0.7rem;
border-color: var(--line-soft);
}
.site-footer .canvas-wide > div:last-child .meta { gap: 0; font-size: 0.85rem; text-align: right; }
/* The white badge would vanish on paper; swap in the full-color lockup. */
.site-footer .gna-badge img {
content: url('../img/gna-logo-w-tagline.svg');
height: 2.1rem;
width: auto;
}
/* --- Legal pages --- */
.legal-prose { max-width: none; }
.legal-prose h2 { margin-top: 1.6rem; }
.legal-prose li::marker { color: var(--ink-soft); }
.legal-toc { padding: 0.8rem 1.1rem; break-inside: avoid; }
.legal-toc ol { columns: 2; column-gap: 2rem; }
.legal-toc li { break-inside: avoid; margin-top: 0; padding-block: 0.15rem; }
.legal-prose table { min-width: 0; }
.legal-prose thead { display: table-header-group; }
.legal-prose thead th { background: none; border-bottom: 1px solid var(--line); }
.todo-flag { break-inside: avoid; }
.legal-prose .checklist li::before { color: var(--ink); }
/* Blanks meant for a pen: dark enough to see, long enough to write on. */
.legal-prose .fill { min-width: 11rem; border-bottom-color: var(--ink-soft); }
.legal-prose .sig-grid { grid-template-columns: 1fr 1fr; }
.legal-prose .sig-card { break-inside: avoid; }
.legal-prose .sig-card p { margin-top: 1.1rem; }
.legal-prose .sig-card .fill { min-width: 60%; }
/* The style guide's own print rules sit with the rest of its furniture, in
the components layer, so they stay out of site.css. */
}An internal page: generated by scripts/build-docs.js, not in
the site’s navigation, and marked noindex.
Back to the style guide.