/* Handled — design system. Static CSS, no build step, no external requests.
   Palette per decisions/13 §3: Handled green; RBP is navy + orange.

   THE MARKETING SITE IS LIGHT (T-572). Matthew, 2026-08-31: "I think white
   background is better" — so the ground is white, the ink is dark, and the
   greens are the darker cuts that clear WCAG AA on white. The dark palette
   did not go away: the tenant app is exempt from the theme change ("not the
   account stuff") and opts back in via `body.app-body` below, which
   redefines the SAME tokens — no rule anywhere else in this file needs to
   know which theme it is painting.

   Orange appears exactly once, in the "by RBP" lockup. System fonts only:
   a webfont is a third-party request. */

:root{
  color-scheme:light;

  /* ground — white. Cards are white too; borders and shadow do the
     separating, surface-2 is the recessed grey for code, table heads and
     inset panels. */
  --bg:#f7f9fa;             /* brand Off-white. Cards stay white, so the ground reads */
  --surface:#ffffff;
  --surface-2:#f3f5f9;
  --line:#dce4e9;
  --line-soft:#eaeef4;

  /* ink */
  --ink:#263238;            /* brand Charcoal - 12.46:1 on off-white */
  --ink-strong:#0e1424;
  --muted:#536573;          /* 6.0:1 on off-white */
  --nav-hover:#376782;      /* nav link HOVER, lighter than the resting navy */

  /* Handled emerald, per the brand guide. --green is the BRAND value and is
     used for artwork: fills, bars, dots, borders, focus rings. It is 3.33:1 on
     white, which clears the 3:1 bar for graphics and UI components but FAILS
     the 4.5:1 ordinary-text bar - the brand guide says so itself, and adds that
     "a logo exemption does not extend to interface copy". So text, links and
     button fills take --green-bright (4.80:1) and hover takes --green-deep
     (5.84:1). Same hue family; only the lightness moves. */
  --navy:#183153;            /* brand Deep Navy - 12.40:1 on off-white */
  --green:#22A06B;
  --green-bright:#1B8257;
  --green-deep:#18734C;
  --green-dim:#DDF4EE;
  --on-green:#ffffff;       /* text painted ON a --green fill */

  /* RBP */
  --orange:#c9441c;         /* used ONCE, in the by-RBP lockup */

  --warn:#8a5a00;
  --warn-bg:#fff4dd;

  /* elevation */
  --shadow-1:0 1px 2px rgb(15 23 42 / .06);
  --shadow-2:0 8px 24px rgb(15 23 42 / .10);

  /* rhythm */
  --u:.5rem;
  --max:78rem;              /* 1248px, matching the marketing preview */
  --measure:38rem;          /* reading width for prose */
  --radius:12px;
  --radius-sm:8px;
}

/* The tenant app keeps the dark ground (soft navy-charcoal, NOT black —
   ~#0a0a0a was the harshness the first pass removed). Only tokens are
   redefined here; every rule below paints from the same names. */
body.app-body{
  color-scheme:dark;
  --bg:#141821;
  --surface:#1a1f2b;
  --surface-2:#212838;
  --line:#2b3345;
  --line-soft:#232a3a;
  --ink:#e9edf3;
  --ink-strong:#ffffff;
  --muted:#8a9dad;
  --green:#2FB87C;
  --green-bright:#4FD39A;
  --green-deep:#6FE0B8;
  --green-dim:#13322A;
  --on-green:#06231a;
  --orange:#ff6633;
  --warn:#e0b055;
  --warn-bg:#2a2113;
  --shadow-1:0 1px 2px rgb(0 0 0 / .18);
  --shadow-2:0 8px 24px rgb(0 0 0 / .22);
}

/* THE PORTAL NAMES ITS OWN TEXT COLOURS, because --navy is not one of the
   tokens above. a2fc7c5 moved headings, the eyebrow and the top nav onto
   --navy for the light marketing pages, and on this dark ground that measured
   1.26-1.36:1, the switcher's "Connect HubSpot" link 1.12:1 (PR #103, R1 in
   the 2026-10-07 re-check). Redefining --navy here is not the fix: the
   portal's .btn-primary buttons take their fill from it. And the nav rule is
   scoped to the header's own links, because the left rail's carry colours of
   their own. */
body.app-body :is(h1,h2,h3,h4,.eyebrow){color:var(--ink-strong)}
body.app-body header.site nav > a,
body.app-body header.site nav > a[aria-current]{color:var(--ink)}
body.app-body header.site nav > a:hover{color:var(--green-deep)}
body.app-body .switcher__menu p a{color:var(--green-bright)}

*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%;scroll-behavior:smooth}
body{
  margin:0;background:var(--bg);color:var(--ink);
  font:16px/1.6 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  -webkit-font-smoothing:antialiased;
}

/* ---------- type scale ---------- */
/* Headings are NAVY, not near-black - the marketing preview's single biggest
   typographic move, and the brand guide's role for the colour. 12.40:1. */
h1,h2,h3,h4{color:var(--navy);line-height:1.15;letter-spacing:-.035em;margin:0;text-wrap:balance}
h1{font-size:clamp(2.375rem,1.9rem + 1.9vw,3rem);line-height:1.08;font-weight:700}
h2{font-size:clamp(1.75rem,1.3rem + 1.9vw,2.5rem);font-weight:650;
  margin-top:calc(var(--u)*8);margin-bottom:calc(var(--u)*2)}
h3{font-size:1.3125rem;line-height:1.3;font-weight:650;
  margin-top:calc(var(--u)*5);margin-bottom:var(--u)}
p,ul,ol,table{margin:0 0 calc(var(--u)*2.5)}
.lede{font-size:1.125rem;color:var(--muted);line-height:1.65;max-width:var(--measure)}
.eyebrow{font-size:.6875rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--navy);margin:0 0 var(--u)}
main > p, main > ul, main > ol, main > h2 + p{max-width:var(--measure)}
a{color:var(--green-bright);text-underline-offset:.18em;text-decoration-thickness:.06em}
a:hover{color:var(--green-deep)}
strong{color:var(--ink-strong);font-weight:650}
code{background:var(--surface-2);border:1px solid var(--line-soft);padding:.1em .4em;border-radius:5px;
  font:.88em/1.4 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:var(--ink)}

/* ---------- shell ---------- */
.wrap{max-width:var(--max);margin:0 auto;padding-inline:clamp(1.25rem,4vw,2rem)}
/* Header per the marketing preview: solid white against the off-white ground,
   one fixed 86px bar, no translucency or blur. */
header.site{border-bottom:1px solid var(--line);background:var(--surface);
  position:sticky;top:0;z-index:30}
header.site .wrap{display:flex;align-items:center;justify-content:space-between;
  gap:calc(var(--u)*3);min-height:86px}
.brand{display:flex;align-items:center;gap:calc(var(--u)*1.25);text-decoration:none;flex-shrink:0}
/* The logo is fixed artwork: the brand guide forbids recolouring it, so it is an
   <img> rather than inline SVG and takes no tokens from this file. width drives
   the size - the guide's 200px starting point - and height:auto preserves the
   1633x397 ratio it protects. The img also carries width/height attributes, so
   without this rule it renders at intrinsic size. */
.brand-logo{width:200px;height:auto;display:block}
@media (max-width:640px){.brand-logo{width:150px}}
.byrbp{font-size:.625rem;font-weight:650;letter-spacing:.08em;text-transform:uppercase;
  color:var(--muted);white-space:nowrap}
.byrbp b{color:var(--orange);font-weight:700}   /* the single deliberate orange */
nav{display:flex;align-items:center;gap:calc(var(--u)*3)}
nav a{text-decoration:none;color:var(--navy);font-size:.875rem;font-weight:500}
nav a:hover{color:var(--nav-hover)}
nav a[aria-current]{color:var(--navy);text-decoration:underline;text-underline-offset:.3em}
nav .signin{color:var(--muted)}

/* Resources is a native <details>, so it opens without script and closes on Esc. */
.resource-menu{position:relative}
.resource-menu summary{cursor:pointer;min-height:44px;display:flex;align-items:center;gap:6px;
  list-style:none;font-size:.875rem;font-weight:500;color:var(--navy)}
.resource-menu summary::-webkit-details-marker{display:none}
.resource-menu summary::after{content:'\2304';font-size:.875rem;line-height:1}
.resource-menu summary:hover{color:var(--nav-hover)}
.resource-menu>div{position:absolute;top:calc(100% + 6px);right:0;min-width:13rem;
  background:var(--surface);border:1px solid var(--line);border-radius:10px;
  box-shadow:0 12px 28px #18315318;padding:6px;display:grid;z-index:2}
.resource-menu a{padding:10px;border-radius:6px}
.resource-menu a:hover{background:var(--bg)}

.btn.small{min-height:44px;padding:11px 16px;font-size:.8125rem}

.menu-toggle{display:none;align-items:center;gap:calc(var(--u)*1.5);border:1px solid var(--line);
  border-radius:8px;min-height:44px;padding:8px 14px;background:var(--surface);color:var(--navy);
  font:inherit;font-size:.875rem;cursor:pointer}

/* The Menu button shows only under html.nav-js, which assets/nav.js sets as
   it takes charge of the button. Without the script there is no button and
   the navigation simply stays open - and the header stops being sticky, so
   that open navigation does not sit over a quarter of a phone screen. */
@media (max-width:900px){
  header.site .wrap{flex-wrap:wrap;min-height:76px;padding-block:calc(var(--u)*1.5)}
  .nav-js .menu-toggle{display:flex}
  html:not(.nav-js) header.site{position:static}
  .byrbp{display:none}
  /* #navigation, the marketing header's menu, and not a bare `nav`: /app has
     three navs of its own (the top bar, the rail and the footer), and a bare
     selector put /app's top bar on its own full-width row, with the tenant
     switcher at its left end and the switcher's menu thrown off the screen.
     Top-aligned, so opening Resources grows its own item downwards instead of
     re-centring everything beside it. */
  #navigation{width:100%;flex-wrap:wrap;align-items:flex-start;gap:calc(var(--u)*2);padding-block:calc(var(--u)*1.25)}
  #navigation[hidden]{display:none}
  #navigation>a{min-height:44px;display:flex;align-items:center}
  .resource-menu>div{position:static;box-shadow:none;border:0;padding:0 0 0 calc(var(--u)*2)}
}
main{padding-block:calc(var(--u)*7) calc(var(--u)*10)}

/* ---------- hero ---------- */
.hero{padding-block:calc(var(--u)*4) calc(var(--u)*6);border-bottom:1px solid var(--line-soft);
  margin-bottom:calc(var(--u)*6)}
.hero h1{max-width:20ch}
.hero .lede{margin-top:calc(var(--u)*3)}
/* A rule immediately above a full-bleed colour band is saying the same thing
   twice, and the band says it better. Degrades to the rule if :has() is absent,
   which is harmless. */
.hero:has(+ .act--bleed){border-bottom:0;margin-bottom:calc(var(--u)*2)}
.actions{display:flex;flex-wrap:wrap;gap:calc(var(--u)*2);margin-top:calc(var(--u)*4)}
/* `font:inherit` and `cursor:pointer` are inert on the <a> elements that have
   always used this class, and are what let a real <button> wear it — which the
   tenant page's Connect QuickBooks control needs, because it POSTs rather than
   navigates. No new colours: the palette is unchanged. */
.btn{display:inline-block;padding:.7rem 1.25rem;border-radius:var(--radius-sm);font-weight:600;font-size:.95rem;
  text-decoration:none;border:1px solid transparent;transition:transform .12s ease;
  font-family:inherit;line-height:inherit;cursor:pointer}
.btn:hover{transform:translateY(-1px)}
/* An in-flight action must LOOK unavailable, or a customer clicks twice and
   starts two OAuth flows. The APPEARANCE is defined once, further down, in the
   .btn:disabled rule beside the destructive controls — two opacity rules on one
   element is how a control ends up unreadable and nobody can say which line did
   it. This one only carries the cursor. */
.btn:disabled{cursor:progress;transform:none}
/* Navy, not emerald: the marketing preview's primary, and the brand guide's own
   instruction - "prefer white text on navy for primary buttons". 13.10:1, where
   white on emerald is 3.33:1 and fails. */
.btn-primary{background:var(--navy);color:#fff;border-color:var(--navy)}
.btn-primary:hover{background:#294a70;border-color:#294a70;color:#fff}
.btn-ghost{border-color:var(--line);color:var(--ink)}
.btn-ghost:hover{border-color:var(--green);color:var(--ink-strong)}

/* ---------- surfaces ---------- */
.grid{display:grid;gap:calc(var(--u)*2.5);grid-template-columns:repeat(auto-fit,minmax(16rem,1fr));
  margin-bottom:calc(var(--u)*3)}
.card{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);
  padding:calc(var(--u)*3);box-shadow:var(--shadow-1)}
.card h3{margin-top:0}
.card p:last-child,.card ul:last-child{margin-bottom:0}
.card.raised{background:var(--surface-2)}
.note{background:var(--surface);border:1px solid var(--line);border-left:3px solid var(--green);
  border-radius:0 var(--radius-sm) var(--radius-sm) 0;padding:calc(var(--u)*2.5) calc(var(--u)*3);
  margin:calc(var(--u)*4) 0;max-width:var(--measure)}
.note :last-child{margin-bottom:0}
.warn{background:var(--warn-bg);border:1px solid color-mix(in srgb,var(--warn) 45%,transparent);
  border-left:3px solid var(--warn);border-radius:0 var(--radius-sm) var(--radius-sm) 0;
  padding:calc(var(--u)*2.5) calc(var(--u)*3);margin:calc(var(--u)*4) 0;max-width:var(--measure)}
.warn :last-child{margin-bottom:0}

/* ---------- tables ---------- */
table{border-collapse:separate;border-spacing:0;width:100%;font-size:.94rem;
  border:1px solid var(--line);border-radius:var(--radius-sm);overflow:hidden}
th,td{padding:.62rem .8rem;text-align:left;vertical-align:top;border-bottom:1px solid var(--line-soft)}
thead th{background:var(--surface-2);font-weight:650;color:var(--ink-strong);font-size:.86rem;
  letter-spacing:.02em;text-transform:uppercase}
tbody tr:last-child td{border-bottom:0}
.num{text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap}
.scroll{overflow-x:auto;margin-bottom:calc(var(--u)*2.5)}

/* ---------- bits ---------- */
.tag{display:inline-block;font-size:.68rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;
  padding:.18rem .55rem;border-radius:999px;vertical-align:.1em}
.tag-live{background:var(--green-dim);color:var(--green-deep);border:1px solid color-mix(in srgb,var(--green) 40%,transparent)}
.tag-test{background:var(--warn-bg);color:var(--warn);border:1px solid color-mix(in srgb,var(--warn) 40%,transparent)}
.tag-next{background:var(--surface-2);color:var(--muted);border:1px solid var(--line)}
ul.tight{padding-left:1.15rem}
ul.tight li{margin:.35rem 0}
ul.check{list-style:none;padding:0}
ul.check li{position:relative;padding-left:1.5rem;margin:.4rem 0}
ul.check li::before{content:"";position:absolute;left:0;top:.62em;width:.44rem;height:.44rem;
  border-radius:50%;background:var(--green)}

/* ---------- pricing ---------- */
.tiers{display:grid;gap:calc(var(--u)*2.5);grid-template-columns:repeat(auto-fit,minmax(15rem,1fr));
  margin-bottom:calc(var(--u)*4)}
.tier{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);
  padding:calc(var(--u)*3);display:flex;flex-direction:column}
.tier.featured{border-color:var(--green);box-shadow:0 0 0 1px var(--green),var(--shadow-2)}
.tier h3{margin-top:0}
.price{font-size:2.15rem;font-weight:700;letter-spacing:-.03em;color:var(--ink-strong);margin:.2rem 0 0}
.price small{font-size:.82rem;font-weight:450;color:var(--muted);letter-spacing:0}
.included{margin:calc(var(--u)*2) 0;padding:calc(var(--u)*1.5) calc(var(--u)*2);background:var(--surface-2);
  border-radius:var(--radius-sm);font-size:.9rem}
.included b{color:var(--green-bright);font-size:1.05rem}
.tier ul{margin-bottom:0;flex:1}

/* ---------- figures ---------- */
figure{margin:calc(var(--u)*5) 0;max-width:100%}
figure svg{display:block;width:100%;height:auto;border:1px solid var(--line);border-radius:var(--radius);
  background:var(--surface)}
figcaption{color:var(--muted);font-size:.88rem;margin-top:calc(var(--u)*1.5);max-width:var(--measure)}

/* ---------- footer ---------- */
footer.site{border-top:1px solid var(--line);margin-top:calc(var(--u)*10);background:var(--surface)}
footer.site .wrap{padding-block:calc(var(--u)*4);display:flex;flex-wrap:wrap;gap:calc(var(--u)*2) calc(var(--u)*4);
  justify-content:space-between;align-items:center;color:var(--muted);font-size:.9rem}
footer.site a{color:var(--muted);text-decoration:none}
footer.site a:hover{color:var(--ink)}
footer nav{gap:calc(var(--u)*2.5)}

@media (prefers-reduced-motion:reduce){*{transition:none!important;scroll-behavior:auto!important}}

/* ---------- narrative acts ----------
   The homepage is a sequence of acts rather than a stack of headings, so a
   scroll engine can later take a section wholesale without the copy being
   re-cut. data-act names the beat; nothing reads it yet. Order and reasoning
   are in research/website/handled-rewrite.md (Schwartz stage 3: name the
   mechanism, prove it, then earn the right to the story). */
.act{padding-block:calc(var(--u)*9)}
.act + .act{border-top:1px solid var(--line-soft)}
.act > h2:first-child,.act > .eyebrow:first-child{margin-top:0}
.act-lede{font-size:1.0625rem;color:var(--muted);line-height:1.65;max-width:var(--measure)}

/* A four-card scope grid, not four mini-essays. The detail lives on /product;
   these exist to be scanned in about two seconds and to prove the product is
   wider than the one mechanism the section above it just demonstrated. */
.grid--tight{margin-top:calc(var(--u)*4)}
.grid--tight .card p{font-size:.88rem;margin-bottom:0}
.more{margin-top:calc(var(--u)*4)}
.more a{font-weight:650;text-underline-offset:.25em}

/* ---------- the comparison page's table ----------
   A real <table> with a caption and row headers, because that is what this is.
   Screen readers and search engines both read a comparison table better than a
   div grid pretending to be one. */
.vs-table{margin:calc(var(--u)*5) 0 0;overflow-x:auto;border:1px solid var(--line);
  border-radius:12px;background:var(--surface)}
.vs-table table{border-collapse:collapse;width:100%;min-width:46rem;font-size:.9rem}
.vs-table th,.vs-table td{text-align:left;vertical-align:top;padding:calc(var(--u)*2.25);
  border-bottom:1px solid var(--line-soft);line-height:1.5}
.vs-table thead th{background:var(--bg);font-size:.72rem;letter-spacing:.08em;
  text-transform:uppercase;color:var(--muted);font-weight:700;white-space:nowrap}
.vs-table thead th:nth-child(2){color:var(--green-deep)}
.vs-table tbody th{font-weight:650;color:var(--ink);width:15rem}
.vs-table tbody td:first-of-type{color:var(--muted)}
.vs-table tr:last-child th,.vs-table tr:last-child td{border-bottom:0}
.vs-table tbody tr:hover th,.vs-table tbody tr:hover td{background:var(--bg)}
/* On a phone the third column sat off-screen with nothing to say it was
   there (PR #103, R6). So each row stacks, the way .ctl-table does below, and
   every cell names its own column from data-label; the header row stays in
   the markup for screen readers but leaves the layout.
   The breakpoint is the table's own width, not a phone's: 46rem of table, two
   2rem gutters, its 1px borders and a 17px classic scrollbar need an 819px
   viewport, and at 640px the table scrolled its third column out of sight
   again from 641px to about 800px (an iPad in portrait lost 31px of it). In
   rem, so it moves with the table when a reader enlarges the default font.
   scripts/_public-layout-selftest.mjs checks the arithmetic. */
@media (max-width:51.25rem){
  .vs-table{overflow-x:visible}
  .vs-table table{min-width:0}
  .vs-table thead{position:absolute;width:1px;height:1px;margin:-1px;padding:0;
    overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
  .vs-table tbody tr{display:block;border-bottom:1px solid var(--line-soft)}
  .vs-table tbody tr:last-child{border-bottom:0}
  .vs-table th,.vs-table td{display:block;width:auto;border-bottom:0}
  /* width:auto again, here: `.vs-table tbody th{width:15rem}` above outranks
     the line before this one, and kept every stacked heading at 240px. */
  .vs-table tbody th{padding-bottom:0;width:auto}
  .vs-table td{padding-top:calc(var(--u)*1.25)}
  .vs-table td[data-label]::before{content:attr(data-label);display:block;font-size:.66rem;
    font-weight:700;letter-spacing:.09em;text-transform:uppercase;color:var(--muted);
    margin-bottom:.35em}
  .vs-table td[data-label="Handled"]::before{color:var(--green-deep)}
}

/* Status chips carry the same vocabulary as the docs page, deliberately: a
   claim that reads Live here and In testing there would be the exact failure
   the published status list exists to prevent. */
.st{display:inline-block;margin-left:.35em;font-size:.66rem;font-weight:700;
  letter-spacing:.05em;text-transform:uppercase;padding:2px 7px;border-radius:4px;
  white-space:nowrap;vertical-align:1px}
.st--live{color:var(--green-deep);background:var(--green-dim);
  border:1px solid color-mix(in srgb,var(--green) 34%,transparent)}
.st--testing{color:var(--warn);background:var(--warn-bg);
  border:1px solid color-mix(in srgb,var(--warn) 30%,transparent)}

/* ---------- control tables ----------
   /security is a checklist that was written as prose. Its content is a control
   and what that control does, which is two columns, and a reader scanning for
   one answer should not have to read four bullets to find it. Same bordered
   treatment as the comparison table so the site keeps one table vocabulary. */
.ctl-table{margin:calc(var(--u)*2.5) 0 calc(var(--u)*5);border:1px solid var(--line);
  border-radius:12px;background:var(--surface);overflow:hidden}
.ctl-table table{border-collapse:collapse;width:100%;font-size:.9rem}
.ctl-table th,.ctl-table td{text-align:left;vertical-align:top;
  padding:calc(var(--u)*2.25);border-bottom:1px solid var(--line-soft);line-height:1.55}
.ctl-table tbody th{font-weight:650;color:var(--ink);width:15rem}
.ctl-table td{color:var(--muted)}
.ctl-table tr:last-child th,.ctl-table tr:last-child td{border-bottom:0}
.ctl-table tr:hover th,.ctl-table tr:hover td{background:var(--bg)}
@media (max-width:640px){
  .ctl-table th,.ctl-table td{display:block;width:auto}
  .ctl-table th{padding-bottom:0;border-bottom:0}
  .ctl-table td{padding-top:calc(var(--u)*0.75)}
}

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

/* ---- full-bleed section grounds ----
   A long page of one colour reads as one thing. These break it into bands.

   The palette decides which colours can host a band, not taste: on EMERALD
   nothing clears 4.5:1 - not white (3.33), not navy, not charcoal - so emerald
   stays an accent inside a band and never becomes one. Navy takes white at
   13.10:1 and aqua takes navy at 11.38:1, so the grounds are off-white, aqua
   and navy, and that is the whole vocabulary.

   Breaking out of the centred .wrap: margin-inline pulls to the viewport edge
   and padding-inline puts the content back. overflow-x:clip on <html> absorbs
   the scrollbar's worth of 100vw - clip rather than hidden, because hidden on
   an ancestor kills position:sticky and the bill-split rail depends on it. */
html{overflow-x:clip}
.act--bleed{margin-inline:calc(50% - 50vw);padding-inline:calc(50vw - 50%);
  border-top:0 !important}
.act--bleed + .act{border-top:0}
/* No per-child max-width here: padding-inline already puts content back inside
   the original content box, and auto margins on top of it centre any child with
   a narrower max-width - which silently centred the story heading. */

.act--aqua{background:var(--green-dim)}
.act--white{background:var(--surface)}
/* The page ground, declared rather than inherited. A bleed act with no colour
   class already shows --bg through, but naming it means the hairline rule
   below can see it: a selector cannot match "transparent happens to equal the
   body". */
.act--bg{background:var(--bg)}

/* Two white bands in a row have NO boundary - measured at 1.00, which is not
   a low contrast step, it is the same colour. The page's own header has the
   same problem and solves it the same way: white, with a hairline under it.
   The !important is answering .act--bleed's own !important above, which exists
   to stop stray borders on bleed sections; this is the one place a border is
   the point. */
.act--white + .act--white,
.act--white + .act--bg,
.act--bg + .act--white,
.act--ledger + .act--bg,
.act--bg + .act--ledger,
.act--ledger + .act--white,
.act--white + .act--ledger{border-top:1px solid var(--line) !important}

/* The dark anchor. One per page, about two thirds down, where the story is. */
.act--navy{background:var(--navy);color:#dbe5f0}
.act--navy h2,.act--navy h3{color:#fff}
.act--navy .eyebrow{color:#7fd9b4}
.act--navy strong,.act--navy em{color:#fff}
.act--navy .act-lede{color:#b9c9de}
.act--navy .card{background:#21406b;border-color:#3d6499;color:#dbe5f0}
.act--navy .card h3{color:#fff}
.act--navy a{color:#7fd9b4}
.act--navy a:hover{color:#a5e6cb}

/* A primary action sitting on the navy band. The standing primary is navy on
   off-white, which disappears on navy, so it inverts: the band's own accent as
   the fill, the band's ground as the ink. 6.99:1 the one way, 6.55:1 the other. */
.btn-onnavy{background:#2FB87C;color:#06231a;border:1px solid #2FB87C;font-weight:650;
  max-width:100%;overflow-wrap:anywhere}
.btn-onnavy:hover{background:#4FD39A;border-color:#4FD39A;color:#06231a}
/* `.act--navy a` paints every link on the band mint, and it outranks the two
   rules above, so the walkthrough's closing button came out mint on emerald,
   1.51:1 (PR #103, R4). Restated with the band in the selector. The wrap
   above is for the same button: an email address is one unbreakable word,
   and at 320px it ran 13px past the screen (R3). */
.act--navy a.btn-onnavy,.act--navy a.btn-onnavy:hover{color:#06231a}
.act--navy .actions{margin-top:calc(var(--u)*3)}
.trial-terms{font-size:.88rem;color:#b9c9de;max-width:var(--measure)}
.act--navy .trial-terms + .actions{margin-top:calc(var(--u)*3)}

.act--quiet h2{max-width:20ch}

/* ---------- the comparison, set BESIDE the setup rather than six sections
   later. Read on its own further down the page, a list of three limits has
   no visible owner and reads as if it describes THIS product. Paired in two
   columns under one heading, each limit has an answer opposite it and the
   attribution is unambiguous.

   The left column is deliberately not styled as a loser: the framing says the
   native connector is fine for simple cases, and handled-rewrite.md is explicit
   that it is the incumbent belief to displace, not a competitor to insult. */
/* One container with two tinted halves cannot work on this band: the limit
   cell and the answer cell measured 1.056:1 apart, and both sat about 1.1:1
   from the aqua behind them, so nothing read as raised or recessed. Colour
   alone has nowhere to go here. So the container is gone. The limit sits
   directly on the band with no card at all, and the answer is a white card
   with a real shadow, which is elevation the eye can actually see. */
.versus{margin:calc(var(--u)*4) 0 0;display:grid;gap:calc(var(--u)*2)}

.versus-head{display:grid;grid-template-columns:1fr 1fr;gap:calc(var(--u)*4);
  padding-inline:calc(var(--u)*0.5)}
.versus-head span{font-size:.72rem;font-weight:700;letter-spacing:.09em;
  text-transform:uppercase;color:var(--muted)}
.versus-head span{padding-left:calc(var(--u)*2.5)}
.versus-head span:first-child{color:var(--green-deep)}

.versus-row{display:grid;grid-template-columns:1fr 1fr;gap:calc(var(--u)*4);
  align-items:center;position:relative}

/* Recessed, not absent. Flat on the band it had no edges and the structure
   went soft, so it takes a tint a step DEEPER than the band while the answer
   card sits a step lighter: 1.10:1 down and 1.15:1 up, with the band itself
   between them. Both halves carry a left edge so they read as a pair, the
   limit's muted and the answer's emerald. No shadow here, which is what keeps
   it reading as ground rather than as a second card. */
.v-limit{padding:calc(var(--u)*2.5);font-size:.9rem;line-height:1.55;
  color:var(--muted);background:#CEEBE2;border-radius:10px;
  border-left:3px solid #9CC4B7}
.v-limit strong{display:block;color:var(--ink);margin-bottom:.35em}

/* This half floats above it. */
.v-answer{padding:calc(var(--u)*2.5);font-size:.9rem;line-height:1.55;
  background:var(--surface);border:1px solid var(--line);border-radius:10px;
  box-shadow:0 6px 18px rgb(24 49 83 / .09);border-left:3px solid var(--green)}
.v-answer strong{display:block;color:var(--green-deep);margin-bottom:.35em}

/* Handled reads first, so this is no longer an arrow. A chevron pointing right
   used to carry the eye from the gap to the answer; with the answer on the left
   it would point from the answer back at the gap, which is the wrong claim. A
   direction-neutral marker says "compare these" without asserting an order.
   White sits on --green-deep, not --green: two letters are TEXT and need 4.5:1
   (5.84:1 here), where the old glyph could pass as a graphic at 3:1. */
.versus-row::after{content:"vs";position:absolute;left:50%;top:50%;
  transform:translate(-50%,-50%);z-index:2;width:26px;height:26px;
  display:grid;place-items:center;border-radius:50%;
  background:var(--green-deep);color:#fff;font-size:.58rem;font-weight:700;
  letter-spacing:.04em;text-transform:uppercase;
  line-height:1;pointer-events:none}

@media (max-width:760px){
  .versus-head{display:none}
  .versus-row{grid-template-columns:1fr;gap:var(--u)}
  .versus-row::after{display:none}
  .v-limit{border-left:0;border-top:3px solid #9CC4B7}
  .v-limit::before{content:"Native connector";display:block;font-size:.66rem;
    font-weight:700;letter-spacing:.09em;text-transform:uppercase;
    color:var(--muted);margin-bottom:.5em}
  .v-answer::before{content:"Handled";display:block;font-size:.66rem;
    font-weight:700;letter-spacing:.09em;text-transform:uppercase;
    color:var(--green-deep);margin-bottom:.5em}
}

.act--story h2{max-width:24ch}

/* A real person on the origin band. The page argues by specificity everywhere
   else; the one place it asks to be trusted rather than checked is the story,
   so the face there has to be an actual named one. No stock, per FIN-11. */
.story-grid{display:grid;grid-template-columns:1fr 15rem;gap:calc(var(--u)*6);
  align-items:center}
.story-copy > *:first-child{margin-top:0}
.who{margin:0}
.who > img{display:block;width:100%;height:auto;border-radius:12px;
  border:1px solid #3d6499}
.who figcaption{display:grid;gap:2px;margin-top:calc(var(--u)*2);font-size:.8125rem;
  line-height:1.5;color:#b9c9de}
.who figcaption strong{color:#fff;font-size:.875rem}
/* balance: in a ~15rem column this wraps, and the default wrap drops
   "September 2026." onto a line of its own under a full one. */
.who-award{margin-top:calc(var(--u)*1.25);color:#7fd9b4;font-weight:600;
  text-wrap:balance}

/* ---------- the partner tier, stated ----------
   The badge alone names the programme and the tier but never says WHOSE, and
   stacking it under the RBP mark left the reader to infer the relationship from
   proximity. A sentence states it and the badge evidences it, which is the way
   round that actually carries the claim. Full width, so it also fills the gap
   the shortened teaser copy left beside the portrait. */
.partner-strip{display:flex;align-items:center;gap:calc(var(--u)*3);
  margin:calc(var(--u)*5) 0 0;padding-top:calc(var(--u)*4);
  border-top:1px solid #2e5183;max-width:none}
.partner-strip img{flex:0 0 auto;width:14rem;height:auto;display:block}
.partner-strip span{font-size:.92rem;line-height:1.6;color:#b9c9de;max-width:58ch}
.partner-strip strong{color:#fff}

.who-col{display:grid;gap:0}

/* No badge on the homepage. At portrait-column width it renders about 52px,
   where it reads as a coloured shape rather than a recognisable mark, and the
   sentence beside it wraps to three lines. One caption line says the same thing
   and costs the column nothing; the badge gets room on /about instead. */
/* OUTSIDE the figcaption on purpose. A figcaption describes its figure, so a
   tier line inside it reads as Matthew's credential rather than the company's,
   which is the same ownership ambiguity the stacked logos had. */
.who-tier{margin:calc(var(--u)*2.5) 0 0;padding-top:calc(var(--u)*2);
  border-top:1px solid #2e5183;font-size:.78rem;line-height:1.5;color:#9fb0c6}
.who-tier strong{color:#dbe5f0;font-weight:650}

.partner-strip--light{border-top-color:var(--line);margin-top:calc(var(--u)*3);
  padding-top:calc(var(--u)*3);flex-direction:column;align-items:flex-start;
  gap:calc(var(--u)*2.5)}
.partner-strip--light span{max-width:46ch}
.partner-strip--light span{color:var(--muted)}
.partner-strip--light strong{color:var(--ink)}

@media (max-width:640px){
  .partner-strip{flex-direction:column;align-items:flex-start;gap:calc(var(--u)*2)}
}

/* The teaser's one exit, and the only thing in the left column with any weight
   once the copy was cut to a paragraph. Set larger than body so it reads as the
   next move rather than a footnote. Not a button: the page's actual CTAs are in
   the close, and a second button here would compete with them. On the navy band
   the standing link colour is emerald at too low a contrast, so it takes the
   band's own accent. */
.story-link{display:inline-flex;align-items:baseline;gap:.5rem;
  font-size:1.18rem;font-weight:650;letter-spacing:-.01em;color:#7fd9b4;
  text-decoration:none;border-bottom:1px solid #2e6b56;padding-bottom:4px}
.story-link:hover{color:#a5e6cb;border-bottom-color:#7fd9b4}
.story-link:focus-visible{outline:2px solid #7fd9b4;outline-offset:3px}
.story-copy .story-link{margin-top:calc(var(--u)*1.5)}

/* /about reuses the homepage's portrait and credential, on a light ground. */
.about-people{display:grid;grid-template-columns:15rem minmax(0,1fr);
  gap:clamp(1.5rem,4vw,3rem);align-items:start;margin-top:calc(var(--u)*3)}
.about-people .who img{border-radius:10px;border:1px solid var(--line)}
.about-people .who figcaption{display:grid;gap:2px;margin-top:calc(var(--u)*2);
  font-size:.8125rem;line-height:1.5;color:var(--muted)}
.about-people .who figcaption strong{color:var(--ink);font-size:.875rem}
.about-people .who-award{color:var(--green-deep)}
@media (max-width:760px){.about-people{grid-template-columns:1fr}}

/* The partner tier is about RBP, not about Matthew, so it sits outside his
   caption as its own credential rather than reading as a line of his bio.
   The cream variant is the one that works here: the orange would fight a
   palette that spends its one orange on the by-RBP lockup, and the dark variant
   disappears into the navy. It carries an alpha channel, so it needs no card -
   it sits on the ground directly. */
@media (max-width:860px){
  .story-grid{grid-template-columns:1fr;gap:calc(var(--u)*4)}
  .who{max-width:14rem}
}
.act--close{text-align:center;border-top:1px solid var(--line)}
.act--close h2{margin-inline:auto;max-width:18ch}
.act--close .act-lede{margin-inline:auto}
.act--close .actions{justify-content:center}
/* The honesty promise, folded out of its own boxed section and into the close.
   It was a bordered panel between the native-connector act and the offer, which
   put a caveat exactly where the page is asking for the click. As one quiet line
   UNDER the CTA it still commits to the same thing - the docs page carries the
   status of every capability - without interrupting the ask. */
.close-note{margin:calc(var(--u)*3) auto 0;max-width:58ch;font-size:.875rem;
  color:var(--muted);line-height:1.6}

/* Product screenshots. Real captures, so they carry a frame and a caption
   rather than floating: the frame says "this is a screen", the caption says
   what to look at. */
.shot{margin:calc(var(--u)*4) 0 0}
.shot img{display:block;width:100%;height:auto;border:1px solid var(--line);
  border-radius:12px;box-shadow:0 14px 34px #18315314;background:var(--surface)}
.shot figcaption{margin-top:calc(var(--u)*2.5);font-size:.875rem;color:var(--muted);
  line-height:1.6;max-width:var(--measure)}
.shot figcaption strong{color:var(--ink)}

.note--gate{margin-top:calc(var(--u)*3);border-left-color:var(--navy)}
.note--gate p{margin:0}

/* ---------- hero product demo ----------
   Ported from the marketing preview (handled-marketing-preview.vercel.app), rebuilt on
   this site's tokens rather than vendoring scroll-craft: the component needs no engine,
   and inheriting the preview's flat #22a06b would have put emerald text at 3.33:1.
   The three tabs ARE the three mechanisms in research/website/handled-rewrite.md - cost
   at line level, the split, the approval gate - as one object above the fold. */
.hero--demo{display:grid;grid-template-columns:.92fr 1.08fr;gap:calc(var(--u)*5);
  align-items:center;padding-block:calc(var(--u)*7) calc(var(--u)*6)}
.hero-copy h1{margin:calc(var(--u)*2) 0 calc(var(--u)*3)}
.hero-copy .lede{max-width:34rem}

.hero-scene{position:relative;padding:calc(var(--u)*3) 0 0;min-width:0;isolation:isolate}
.scene-ground{position:absolute;inset:0 0 calc(var(--u)*7) calc(var(--u)*3);
  background:var(--green-dim);border-radius:26px;z-index:-2}
.source-sheet{position:absolute;z-index:-1;inset:var(--u) calc(var(--u)*4.4) auto calc(var(--u)*7);
  height:160px;padding:calc(var(--u)*2) calc(var(--u)*2.5);background:#f0faf7;
  border:1px solid #bfded4;border-radius:12px;rotate:-4deg}
.source-sheet span{font-size:.56rem;font-weight:700;color:#5a7a73;letter-spacing:.1em}
.source-sheet i{display:block;height:1px;background:#bcd8cf;margin:calc(var(--u)*1.75) 0}
.window-top{display:flex;gap:var(--u);align-items:center;padding:calc(var(--u)*1.9) calc(var(--u)*2.25);
  border-bottom:1px solid var(--line);font-size:.69rem;color:var(--muted)}
.window-top .dot{width:8px;height:8px;border-radius:50%;background:var(--green);flex:none}
.app-shot{margin:0;background:#fff;border:1px solid #d1e0da;border-radius:16px;
  box-shadow:0 16px 35px #18315312;overflow:hidden;position:relative}
.app-shot img{display:block;width:100%;height:auto}
.window-top .demo-label{margin-left:auto;font-size:.62rem;letter-spacing:.06em;text-transform:uppercase}
.cost-rows>div{display:flex;justify-content:space-between;padding:6px 0;font-size:.75rem}
.cost-rows>div>span{display:flex;gap:9px;align-items:center}
.cost-mark.sub{background:#4d7c86}
.cost-mark.disposal{background:#92bcb2}
.scene-caption{font-size:.62rem;color:var(--muted);margin:calc(var(--u)*2) var(--u) 0;line-height:1.5}

.value-line{display:flex;justify-content:space-between;gap:calc(var(--u)*3);
  border-block:1px solid var(--line);padding-block:calc(var(--u)*2.75);font-size:.82rem;
  color:var(--muted)}
.value-line span::before{content:'\2713';color:var(--navy);margin-right:10px}
.value-line a{text-decoration:none;color:var(--navy)}

@media (max-width:900px){
  .hero--demo{grid-template-columns:1fr;gap:calc(var(--u)*3.5);padding-block:calc(var(--u)*5)}
  .hero-copy h1 br:nth-of-type(2){display:none}
  .hero-scene{max-width:37rem;width:100%;margin-inline:auto}
  .value-line{display:grid;gap:calc(var(--u)*1.25)}
}
@media (max-width:560px){
  .hero-scene{padding:calc(var(--u)*3) 0 0}
  .scene-ground{inset:0 0 calc(var(--u)*5.5) calc(var(--u)*1.5)}
}

/* ---------- the reconciliation line ----------
   What is left of the bill-split move after it was made static. The rail, the
   sticky stage and the phase gating are gone; this is the figure they used to
   reveal, now simply present. Rebuilt from git if the motion comes back. */
.reconcile{margin:calc(var(--u)*3) 0 0;padding:calc(var(--u)*1.5) calc(var(--u)*2.5);
  background:var(--green-dim);border-radius:10px;color:var(--green-deep);
  font-weight:650;font-size:.95rem;display:inline-flex;gap:.6rem;align-items:baseline;
  font-variant-numeric:tabular-nums}
.reconcile .op{color:var(--ink-strong)}

/* Full-bleed bands still need the scrollbar's worth of 100vw absorbed.
   Kept as CLIP rather than hidden: it was chosen because hidden kills
   position:sticky, and although the sticky rail has gone, clip is the correct
   property for this job anyway and hidden would reintroduce the hazard the day
   anything sticky returns. */
html{overflow-x:clip}

/* ---------- auth (T-510) ----------
   The sign-in page and the tenant landing. Same tokens as everything else —
   no new colours, no new type scale, no webfont. */
.auth-shell{display:grid;gap:calc(var(--u)*6);grid-template-columns:minmax(0,26rem) minmax(0,1fr);
  align-items:start;padding-block:calc(var(--u)*8) calc(var(--u)*10)}
@media (max-width:60rem){.auth-shell{grid-template-columns:1fr;gap:calc(var(--u)*5)}}

.auth-card{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);
  padding:calc(var(--u)*4);box-shadow:var(--shadow-1)}
.auth-title{font-size:1.75rem;letter-spacing:-.028em;margin:0 0 var(--u)}
.auth-sub{color:var(--muted);font-size:.94rem;margin:0 0 calc(var(--u)*3)}

/* provider buttons */
.provider{display:flex;align-items:center;justify-content:center;gap:.6rem;width:100%;
  padding:.72rem 1rem;border-radius:var(--radius-sm);border:1px solid var(--line);
  background:var(--surface-2);color:var(--ink);font:600 .95rem/1 inherit;font-family:inherit;
  text-decoration:none;cursor:pointer;transition:border-color .12s ease,transform .12s ease}
.provider:hover{border-color:var(--green);color:var(--ink-strong);transform:translateY(-1px)}
.provider-hubspot{background:var(--green-bright);border-color:var(--green-bright);color:var(--on-green)}
.provider-hubspot:hover{background:var(--green-deep);border-color:var(--green-deep);color:var(--on-green)}
.provider-mark{display:inline-flex;align-items:center;justify-content:center;width:1.15rem;height:1.15rem;
  font-weight:700;font-size:.85rem}
.provider-row{display:grid;grid-template-columns:1fr 1fr;gap:var(--u);margin-top:var(--u)}
.provider-muted{opacity:.5;cursor:not-allowed}
.provider-muted:hover{border-color:var(--line);transform:none;color:var(--ink)}
.provider-note{color:var(--muted);font-size:.8rem;margin:calc(var(--u)*1.5) 0 0;text-align:center}

.auth-or{display:flex;align-items:center;gap:var(--u);color:var(--muted);font-size:.8rem;
  margin:calc(var(--u)*3.5) 0 calc(var(--u)*2.5)}
.auth-or::before,.auth-or::after{content:"";flex:1;height:1px;background:var(--line)}

/* form */
.field{display:block;margin-bottom:calc(var(--u)*2.5)}
/* `hidden` is the switch the sign-in script flips for each mode, and an author
   display rule out-ranks the browser's own [hidden]. Without these the reset
   form drew the name and Password fields and the provider buttons it had
   hidden, the same guard .switcher[hidden] and .trial-banner[hidden] carry. */
.field[hidden],.provider[hidden],.provider-row[hidden],.auth-or[hidden]{display:none}
.field > span{display:block;font-size:.85rem;font-weight:600;color:var(--ink);margin-bottom:.35rem}
.field > span em{font-style:normal;font-weight:450;color:var(--muted)}
.field input{width:100%;padding:.66rem .8rem;border-radius:var(--radius-sm);
  border:1px solid var(--line);background:var(--bg);color:var(--ink);font:inherit;font-size:.95rem}
.field input::placeholder{color:color-mix(in srgb,var(--muted) 70%,transparent)}
.field input:focus{outline:2px solid var(--green);outline-offset:1px;border-color:var(--green)}
.field-help{color:var(--muted);font-size:.82rem;margin:calc(var(--u)*-1) 0 calc(var(--u)*2.5)}
.auth-submit{width:100%;text-align:center;border:0;font-family:inherit;cursor:pointer}
.auth-submit:disabled{opacity:.6;cursor:progress;transform:none}
.auth-switch{margin:calc(var(--u)*3) 0 0;font-size:.9rem;color:var(--muted);text-align:center}

/* banners — the page only shows one when it has something true to say */
.auth-banner{border-radius:var(--radius-sm);padding:calc(var(--u)*2) calc(var(--u)*2.5);
  margin:0 0 calc(var(--u)*3);font-size:.9rem;line-height:1.5;border:1px solid var(--line);
  background:var(--surface-2)}
.auth-banner-info{border-left:3px solid var(--green);background:var(--green-dim);color:var(--ink)}
.auth-banner-ok{border-left:3px solid var(--green);background:var(--green-dim);color:var(--ink)}
.auth-banner-error{border-left:3px solid var(--warn);background:var(--warn-bg);color:var(--ink)}
.auth-banner a{color:var(--green-bright)}
.auth-banner-error a{color:var(--warn)}

/* the reassurance column */
.auth-aside{padding-top:calc(var(--u)*2)}
.auth-aside h2{margin-top:0;font-size:1.15rem}
.auth-aside-note{color:var(--muted);font-size:.88rem;max-width:34rem}

.auth-loading{color:var(--muted);padding-block:calc(var(--u)*10);text-align:center}

/* ---------------------------------------------------------------------------
   Per-portal onboarding progress (tenant page)

   Each portal is at its OWN stage, so the progress belongs to the portal card
   rather than sitting once at the bottom of the page. A tenant with two portals
   genuinely has two different answers to "what do I do next", and a single
   shared "Next" section could only ever be right about one of them.

   The steps are a chain because the order is real: nothing can read QuickBooks
   costs until Handled is installed in the portal, and the onboarding agent has
   nothing to configure until both ends are connected.
   --------------------------------------------------------------------------- */
/* ---------------------------------------------------------------------------
   THE TENANT LIST

   One row per portal, carrying only what somebody needs in order to CHOOSE
   between them: the id, their role, and one line of state. Everything else —
   the integrations, the people, the keys, the plan — is inside the tenant,
   because all of it is per tenant and rendering two tenants' detail at once was
   the thing that read as "we are in both tenants at once".

   The whole card is the link. A card where only the id is clickable trains
   people to aim at four characters.
   --------------------------------------------------------------------------- */
/* THE GRID DECLARES NO ROW THE CARD MIGHT NOT RENDER.

   It used to name four areas — "id go" "hub go" "state go" "role go" — while
   the `hub` line is only rendered when a tenant has a NAME to put above its id,
   which today is almost none of them. A named grid area with no item in it is
   still a row: every card without a name carried an empty track plus its
   row-gap, so the whole list sat looser than it was drawn to and the gap read
   as a rendering fault rather than as spacing.

   So the rows are not named at all. Every child except the arrow takes column
   1 and gets a row from the implicit flow, which means an unrendered line
   occupies nothing; the arrow spans whatever rows there turn out to be. */
.tenant-card {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: calc(var(--u) * 0.5) calc(var(--u) * 2);
  align-items: center;
  border: 1px solid var(--line);
  border-left: 3px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  padding: calc(var(--u) * 3);
  margin-bottom: calc(var(--u) * 2);
  text-decoration: none;
  color: var(--ink);
}
.tenant-card:hover { border-color: var(--green); color: var(--ink-strong); }
.tenant-card:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }
.tenant-card > span { grid-column: 1; }
.tenant-card__id { font-size: 1.05rem; font-weight: 650; color: var(--ink-strong); }
/* The portal id, once there is a NAME above it to be the title. It stays on the
   card rather than being replaced by the name: the id is what a person quotes
   to support and what appears in HubSpot's own URLs. When there is no name the
   id IS the title and this row is not rendered at all — an empty line under a
   heading reads as a rendering fault. */
.tenant-card__hub { color: var(--muted); font-size: 0.82rem; }
.tenant-card__state { color: var(--ink); font-size: 0.94rem; }
.tenant-card__role { color: var(--muted); font-size: 0.85rem; }
/* Column 2, spanning every row the card actually produced — `1 / -1` follows
   the implicit rows rather than a count somebody has to keep in step. */
.tenant-card__go { grid-column: 2; grid-row: 1 / -1; color: var(--muted); font-size: 1.4rem; }
/* The left edge carries the state, so a list of tenants can be read at a
   glance without reading every line. Dashed for "we could not check" — an
   unread value never borrows the look of a value we established. */
.tenant-card--done { border-left-color: var(--green); }
.tenant-card--todo { border-left-color: var(--muted); }
.tenant-card--warn { border-left-color: var(--warn); }
.tenant-card--unknown { border-left-style: dashed; border-left-color: var(--muted); }

/* Where you are, and the way back out. Rendered only inside a tenant. */
.tenant-crumb {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: calc(var(--u) * 1);
  margin-bottom: calc(var(--u) * 3);
  padding-bottom: calc(var(--u) * 1.5);
  border-bottom: 1px solid var(--line-soft);
  font-size: 0.9rem;
}
.tenant-crumb[hidden] { display: none; }

/* Wraps on narrow screens rather than scrolling sideways: this is the one
   thing on the page a person is meant to read at a glance. */
.steps {
  display: flex;
  align-items: stretch;
  flex-wrap: wrap;
  gap: calc(var(--u) * 1);
}
.step {
  flex: 1 1 11rem;
  min-width: 10rem;
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-sm);
  padding: calc(var(--u) * 1.75) calc(var(--u) * 2);
  background: var(--bg);
}
.step__label {
  display: block;
  font-weight: 600;
  color: var(--ink-strong);
  margin-bottom: calc(var(--u) * 0.5);
}
.step__state { font-size: 0.8125rem; color: var(--muted); }

.step--done { border-color: var(--green-dim); }
.step--done .step__label::before { content: "\2713\00a0"; color: var(--green-bright); }
.step--todo .step__label::before { content: "\25cb\00a0"; color: var(--muted); }
.step--warn { border-color: var(--warn); background: var(--warn-bg); }
.step--warn .step__label::before { content: "\26a0\00a0"; color: var(--warn); }
/* Unknown is its OWN state and never borrows the look of "not done". A read we
   could not make must not render as a fact we established. */
.step--unknown { border-style: dashed; }
.step--unknown .step__label::before { content: "?\00a0"; color: var(--muted); }

.step__arrow {
  align-self: center;
  color: var(--muted);
  padding: 0 calc(var(--u) * 0.5);
  flex: 0 0 auto;
}
@media (max-width: 640px) { .step__arrow { display: none; } }

/* THE SQUARES ARE LINKS NOW.
   They were a read-only progress chain; each one opens its own provider card,
   where the CRM or the ERP is chosen and set up. So they need to LOOK like
   somewhere to go — a square that opens something and a square that does not
   must not be indistinguishable. */
.step--link { text-decoration: none; color: inherit; display: block; }
.step--link:hover { border-color: var(--green); background: var(--surface); color: var(--ink-strong); }
.step--link:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }
.step--active { background: var(--surface); box-shadow: inset 0 0 0 1px var(--green); }

.card__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: calc(var(--u) * 1.5);
  margin-top: calc(var(--u) * 2.5);
}

/* The way in. The site had no route to /signin/ from any page -- it was
   reachable only by typing the URL -- so this is the one nav item that reads as
   an action rather than another destination. */
.nav-signin {
  border: 1px solid var(--green-dim);
  border-radius: var(--radius-sm);
  padding: calc(var(--u) * 0.75) calc(var(--u) * 1.75);
  color: var(--green-bright);
}
/* Hover fills with the green tint and DEEPENS the green rather than painting
   the page background over it — `color:var(--bg)` here was white-on-pale-green
   the moment the light theme became the default. */
.nav-signin:hover { background: var(--green-dim); color: var(--green-deep); }

/* ---------------------------------------------------------------------------
   Tenant app shell: the left-hand section nav (T-548, reshaped by T-572)

   Matthew, on the first cut: "the bar isn't along the very left hand side of
   the screen and the UI doesn't take up most of the screen other than the top
   bar and lower part … (funky looking UI)". The shell used to be a grid
   floating inside the centered .wrap — which IS that complaint: dead gutters
   both sides, a rail starting a gutter's width in from the edge.

   So `body.app-body` (set only by public/app/index.html) turns the page into
   a fixed-viewport app: the top bar spans the top, the rail sits FLUSH
   against the left edge and runs the full height under it, the content column
   takes every remaining pixel and scrolls by itself, and the footer is a slim
   strip across the bottom. Marketing pages never set the class and never see
   any of this — they also keep the light palette this file now defaults to,
   while app-body re-tokens itself dark up top.

   No new colours. The active item borrows the same --green-dim/--green
   pairing the "Viewing" tag and the done step already use, so "this is the
   live one" looks the same everywhere on the page.

   The section nav is a <nav>, so the global `nav{display:flex}` and
   `nav a[aria-current]{border-bottom-color:var(--green)}` rules further up this
   file apply to it. Every rule here is written to out-specify those rather than
   to fight them: `.app-nav a` is (0,1,1) against `nav a` (0,0,2), and
   `.app-nav a[aria-current]` is (0,2,1) against `nav a[aria-current]` (0,1,2).
   A class outranks any number of types, so both win on the b column and the c
   column never gets read — test S15 checks these numbers rather than leaving
   the next reader to trust them.
   --------------------------------------------------------------------------- */
body.app-body { height: 100vh; overflow: hidden; display: flex; flex-direction: column; }
body.app-body header.site { flex: 0 0 auto; position: static; }
body.app-body header.site .wrap { max-width: none; }
/* The footer stays — Matthew called the top bar and the lower strip the parts
   that already spanned — but it may not eat the app's height with
   marketing-page padding. */
body.app-body footer.site { flex: 0 0 auto; margin-top: 0; }
body.app-body footer.site .wrap { max-width: none; padding-block: calc(var(--u) * 1.5); font-size: .82rem; }
/* Intuit's attribution line (AEN-323) on a row of its own under the strip,
   so it reads as the footnote it is rather than squeezing the nav. */
body.app-body footer.site .foot-marks { flex-basis: 100%; font-size: .75rem; }

.app-main { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; padding: 0; }
/* `hidden` IS the switch, here as everywhere on this page: the flex rule below
   must never out-rank the attribute. (1,1,0) beats (1,0,0) in either order. */
#app[hidden] { display: none; }
#app { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }

/* THE RAIL COLLAPSES FROM ONE ATTRIBUTE, AND THE GRID DOES THE WORK.

   `data-rail="expanded|collapsed"` on .app-shell is the entire state. The first
   track is sized from it and every label in the rail is shown or hidden from
   it, so there is nothing to measure, nothing to recompute on resize, and no
   inline width for the media query below to fight. A default is given on the
   bare selector as well as on [data-rail="expanded"], so a shell whose script
   never ran still lays out correctly rather than collapsing to nothing. */
.app-shell {
  flex: 1 1 auto;
  min-height: 0;
  display: grid;
  grid-template-columns: minmax(0, 15rem) minmax(0, 1fr);
  align-items: stretch;
}
.app-shell[data-rail="expanded"] { grid-template-columns: minmax(0, 15rem) minmax(0, 1fr); }
.app-shell[data-rail="collapsed"] { grid-template-columns: minmax(0, 3.5rem) minmax(0, 1fr); }

/* Full height, flush left, and it scrolls its own overflow, so a long list of
   sections can never push the toggle off screen. */
.app-rail {
  min-width: 0;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  padding: calc(var(--u) * 2) calc(var(--u) * 1.5) calc(var(--u) * 3);
  background: var(--surface);
  border-right: 1px solid var(--line);
}
.app-shell[data-rail="collapsed"] .app-rail { padding-inline: calc(var(--u) * 0.75); }

/* The toggle. It carries aria-pressed, so its accessible name never changes;
   only the visible word does, and that word is hidden entirely when collapsed
   so the rail can narrow to an icon rail without the button overflowing it. */
.rail-toggle {
  display: flex;
  align-items: center;
  gap: calc(var(--u) * 1);
  width: 100%;
  margin-bottom: calc(var(--u) * 1.5);
  padding: calc(var(--u) * 1) calc(var(--u) * 1.25);
  background: none;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  color: var(--muted);
  font: inherit;
  font-size: .8rem;
  font-weight: 650;
  letter-spacing: .04em;
  text-transform: uppercase;
  cursor: pointer;
}
.rail-toggle:hover { border-color: var(--green); color: var(--ink-strong); }
.rail-toggle:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }
/* The chevron is inline SVG in the markup — it inherits colour through
   currentColor and cannot go missing behind a blocked asset (the CSP allows no
   external images). It points the way the press moves the rail; collapsed, it
   turns around. */
.rail-toggle__icon { display: inline-flex; flex: 0 0 auto; }
.rail-toggle__icon svg {
  width: 1rem;
  height: 1rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: transform .15s ease;
}
.app-shell[data-rail="collapsed"] .rail-toggle__icon svg { transform: rotate(180deg); }
.app-shell[data-rail="collapsed"] .rail-toggle { justify-content: center; padding-inline: 0; }
.app-shell[data-rail="collapsed"] .rail-toggle__text { display: none; }

/* EVERY LINK CARRIES AN ICON, AND COLLAPSED THE ICON IS THE LINK.

   Matthew: "for the pop-out side bar I feel like you can use icons when they
   are collapsed down". The icons are inline SVG chosen per section by
   SECTION_ICONS in public/app/index.html — the CSP forbids external assets,
   and an icon font is exactly that — stroke-drawn in currentColor so they take
   the link's own state colours. Test S14 asserts every destination's icon is
   DISTINCT as rendered: the same rule that once kept two collapsed sections
   from both being marked "A" in the letter-mark era.

   Collapsed: the section titles go, the labels and "Soon" tags go
   (display:none takes them out of the accessibility tree too — which is why
   every link carries its name in `title`, kept honest by test S17), and what
   remains is a column of centred icons that still route correctly. Hovering
   one names it: the same `title` is the tooltip. It is deliberately NOT
   display:none on the whole nav — a rail you collapsed should still be a
   rail, not an absence. */
.app-shell[data-rail="collapsed"] .app-nav__title { display: none; }
.app-shell[data-rail="collapsed"] .app-nav a { justify-content: center; padding-inline: 0; }
.app-shell[data-rail="collapsed"] .app-nav a > span { display: none; }
.app-shell[data-rail="collapsed"] .app-nav .tag { display: none; }

.nav-ico {
  flex: 0 0 auto;
  width: 1.1rem;
  height: 1.1rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.app-nav { display: block; }
.app-nav ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: calc(var(--u) * 0.5);
}
.app-nav li { margin: 0; }
.app-nav a {
  display: flex;
  align-items: center;
  gap: calc(var(--u) * 1.25);
  padding: calc(var(--u) * 1.25) calc(var(--u) * 1.5);
  border: 1px solid transparent;   /* also neutralises `nav a`'s bottom border */
  border-radius: var(--radius-sm);
  color: var(--muted);
  font-size: 0.94rem;
  font-weight: 600;
  text-decoration: none;
}
/* The label takes the middle, so the "Soon" tag sits at the far edge. */
.app-nav a > span { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.app-nav a:hover { background: var(--surface-2); color: var(--ink-strong); }
/* A visible focus ring is not optional here: the nav is the only way to reach
   four of the five sections, and :focus-visible is what a keyboard gets. */
.app-nav a:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }
.app-nav a[aria-current] {
  background: var(--green-dim);
  color: var(--green-bright);
  border-color: color-mix(in srgb, var(--green) 40%, transparent);
}
/* The "Soon" tag is part of the link's accessible name on purpose -- a screen
   reader announces "Subscription Soon", which is the whole point. */
.app-nav .tag { flex: 0 0 auto; }

/* THE NAV IS TWO GROUPS NOW, and the titles are what make the split readable:
   the tenant's own sections, then the things that are not a tenant's. Without
   them a person reading down the list has no way to tell that "Account" is not
   the account OF this tenant — which is exactly the conflation this whole
   restructure exists to undo. */
.app-nav__title {
  margin: calc(var(--u) * 2) 0 var(--u);
  padding: 0 calc(var(--u) * 1.5);
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--muted);
}
.app-nav__title:first-child { margin-top: 0; }
.app-nav__title code { background: none; border: 0; padding: 0; color: var(--ink-strong); font-size: .85rem; }

/* The content column: every remaining pixel, scrolling on its own. Prose
   inside panels still reads at --measure; cards, tables and grids stretch. */
.app-panels {
  min-width: 0;
  overflow-y: auto;
  padding: calc(var(--u) * 4) clamp(1rem, 3vw, 2.75rem) calc(var(--u) * 8);
}
/* The hero lives inside the content column now (the rail owns the full left
   edge), so it drops the landing-page scale and reads as a page header. */
body.app-body .hero { padding-block: 0 calc(var(--u) * 3); margin-bottom: calc(var(--u) * 4); }
body.app-body .hero h1 { font-size: clamp(1.45rem, 1.2rem + 1vw, 2.05rem); }
.app-panel { min-width: 0; }
/* Never let a display rule out-rank the `hidden` attribute: hidden IS the
   switch, and a panel that is hidden in the DOM but painted on screen would be
   two sections at once. */
.app-panel[hidden] { display: none; }
/* Focus lands here programmatically after a section change. A mouse user gets
   no ring (:focus-visible does not match), a keyboard user does. */
.app-panel:focus { outline: none; }
.app-panel:focus-visible { outline: 2px solid var(--green); outline-offset: 4px; }
.app-panel > h2:first-child { margin-top: 0; }
.app-panel > p { max-width: var(--measure); }

/* ONE COLUMN ON A PHONE, AND THE COLLAPSE STOPS APPLYING.

   48rem, not the 60rem the boxed layout used: the icon rail is narrow enough
   to earn its keep on a ~900px window now that it hugs the edge, so only true
   phone widths fold it into a row of chips. The attribute selectors above are
   `.app-shell[data-rail="…"]` — one class and one attribute and NO type, so
   (0,2,0). A bare `.app-shell` here is (0,1,0) and would LOSE to them, leaving
   a phone rendering a 3.5rem rail beside a squeezed panel. So both states are
   restated at the same specificity, and every collapsed rule is undone rather
   than left to fight the layout. The fixed-viewport body unwinds too: a phone
   page scrolls as one document again. */
@media (max-width: 48rem) {
  body.app-body { height: auto; overflow: auto; display: block; }
  .app-main { display: block; }
  #app { display: block; }
  .app-shell,
  .app-shell[data-rail="expanded"],
  .app-shell[data-rail="collapsed"] {
    grid-template-columns: 1fr;
  }
  .app-rail {
    overflow: visible;
    border-right: 0;
    border-bottom: 1px solid var(--line);
    padding: calc(var(--u) * 2) clamp(1rem, 4vw, 2rem);
  }
  .app-shell[data-rail="collapsed"] .app-rail { padding-inline: clamp(1rem, 4vw, 2rem); }
  /* Collapsing a rail that is already a row of chips buys nothing and could
     leave the only nav on the page as a strip of bare icons. */
  .rail-toggle { display: none; }
  .app-shell[data-rail="collapsed"] .app-nav__title { display: block; }
  .app-shell[data-rail="collapsed"] .app-nav a { justify-content: flex-start; padding-inline: calc(var(--u) * 1.5); }
  .app-shell[data-rail="collapsed"] .app-nav a > span { display: inline; }
  .app-shell[data-rail="collapsed"] .app-nav .tag { display: inline-block; }
  .app-nav ul {
    flex-direction: row;
    flex-wrap: wrap;
    gap: calc(var(--u) * 0.75);
  }
  .app-nav a {
    padding: calc(var(--u) * 1) calc(var(--u) * 1.5);
    border-color: var(--line);
    background: var(--surface);
  }
  .app-nav a > span { flex: 0 1 auto; }
  .app-panels {
    overflow: visible;
    padding: calc(var(--u) * 3) clamp(1rem, 4vw, 2rem) calc(var(--u) * 6);
  }
}

/* ---------------------------------------------------------------------------
   Destructive controls and their confirmations (T-551)

   No new colours and no new custom properties. Danger borrows --warn, which is
   already the page's "a human has to act" colour: it marks the reauth_required
   step and the unconfirmed-e-mail tag. Introducing a red here would have
   invented a fifth meaning for a palette that already has four, and the light
   theme would then need a second red nobody had checked for contrast.
   --------------------------------------------------------------------------- */

/* DISCONNECT LIVES INSIDE THE CARD IT ENDS.
   It used to sit in one "danger" strip under the whole portal, which put an ERP
   action and an account action side by side as though they were the same kind
   of thing. Now each provider card carries its own, separated by a rule rather
   than by colour: this part of the card ENDS things. */
.card__danger {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: calc(var(--u) * 1.5);
  margin-top: calc(var(--u) * 3);
  padding-top: calc(var(--u) * 2);
  border-top: 1px solid var(--line-soft);
}
/* Explanations take the full row so they read as sentences, not as labels
   squeezed beside a button. */
.card__danger .step__state { flex: 1 1 100%; margin: 0; }
.card--danger { border-top: 3px solid var(--warn); }

.btn-small { padding: .45rem .9rem; font-size: .85rem; }

/* --warn, not a new red. The border carries the weight and the fill only
   appears on hover, so a page with two of these is not a page of alarms. */
.btn-danger {
  border-color: var(--warn);
  color: var(--warn);
  background: transparent;
}
.btn-danger:hover { background: var(--warn-bg); color: var(--warn); }
/* A GREYED CONTROL MUST STILL BE LEGIBLE.
   ---------------------------------------
   This rule used to say `opacity:.55`, sitting under an earlier
   `.btn:disabled{opacity:.6}` — same specificity, so the later one won and every
   refused control rendered at 55% of --ink over --surface. In Matthew's
   screenshot "Leave this portal" was simply unreadable, which is the worst
   possible reading of a control that is carrying an explanation beside it: it
   looks like a rendering bug rather than like a decision somebody made.

   OPACITY IS THE WRONG TOOL for this and is gone. It fades the border and the
   text by the same amount and cannot be reasoned about for contrast, because
   the result depends on whatever happens to be painted behind. Explicit tokens
   can be: --muted on --surface-2 measures 5.2:1 on the dark theme and 4.7:1 on
   the light one, both above the 4.5:1 floor for body text. The control reads as
   inert because it is flat and grey, not because it is half-erased. */
.btn:disabled, .btn[disabled] {
  opacity: 1;
  color: var(--muted);
  background: var(--surface-2);
  border-color: var(--line);
  cursor: not-allowed;
  transform: none;
  box-shadow: none;
}
.btn:disabled:hover, .btn[disabled]:hover {
  color: var(--muted);
  background: var(--surface-2);
  border-color: var(--line);
  transform: none;
}

.identity-list li {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: calc(var(--u) * 1.25);
}
.identity-list .step__state { flex: 1 1 100%; }

/* The native <dialog>. ::backdrop is what makes it read as modal; without it
   the page behind stays fully lit and the confirmation looks like a card that
   happened to appear. */
dialog.confirm {
  max-width: 34rem;
  width: calc(100% - 2rem);
  border: 1px solid var(--line);
  border-top: 3px solid var(--warn);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
  padding: calc(var(--u) * 4);
  box-shadow: 0 18px 48px rgb(0 0 0 / .35);
}
dialog.confirm::backdrop { background: rgb(0 0 0 / .55); }
dialog.confirm h2 { margin-top: 0; font-size: 1.3rem; }
dialog.confirm ul { margin: calc(var(--u) * 2) 0; }
dialog.confirm li { margin-bottom: calc(var(--u) * 1.25); }
dialog.confirm code { word-break: break-all; }

.confirm__actions {
  display: flex;
  flex-wrap: wrap;
  gap: calc(var(--u) * 1.5);
  justify-content: flex-end;
  margin: calc(var(--u) * 3) 0 0;
}
/* The safe choice comes FIRST in the DOM, so it is what a keyboard reaches
   first and what a screen reader reads first. Visual order follows. */
.confirm__actions .btn { flex: 0 0 auto; }
@media (max-width: 30rem) {
  .confirm__actions { justify-content: stretch; }
  .confirm__actions .btn { flex: 1 1 100%; text-align: center; }
}

/* ---------------------------------------------------------------------------
   PROVIDER CARDS AND THEIR CHOOSERS

   Each of the four squares opens one of these. Two of them are a CHOICE — which
   CRM this tenant syncs with, which ERP it reads cost from — and the chooser is
   where the honesty lives: what is live is marked live, and what is not built is
   marked "Not built" in the same list rather than being left off it.

   Leaving unbuilt providers OFF the list would be the safer-looking mistake:
   the buyer asks "does it do NetSuite" and gets no answer at all. Listing them
   as logos with no marking is the worse one — a list of logos implying support
   we lack is a control that renders and does nothing, aimed at somebody with a
   budget. Marked, in one list, is the only version that is both useful and true.
   --------------------------------------------------------------------------- */
.provider-card { margin-top: calc(var(--u) * 3); }
.provider-card h3 { margin-top: 0; }

.chooser {
  margin-top: calc(var(--u) * 4);
  padding-top: calc(var(--u) * 2.5);
  border-top: 1px solid var(--line-soft);
}
.chooser h3 { margin-top: 0; font-size: .98rem; }
.chooser__list { list-style: none; margin: 0 0 calc(var(--u) * 2); padding: 0; }
.chooser__item {
  display: grid;
  grid-template-columns: auto auto;
  justify-content: start;
  align-items: center;
  gap: calc(var(--u) * 0.4) calc(var(--u) * 1.5);
  padding: calc(var(--u) * 1.5) 0;
  border-bottom: 1px solid var(--line-soft);
}
.chooser__item:last-child { border-bottom: 0; }
.chooser__name { font-weight: 600; color: var(--ink-strong); }
/* The note spans both columns so a provider's one line of truth sits under its
   own name rather than being squeezed beside a tag. */
.chooser__note { grid-column: 1 / -1; color: var(--muted); font-size: .85rem; }
/* Not built is DIMMER but never unreadable — same reason the disabled button
   stopped using opacity. --muted on --surface clears 4.5:1 in both themes. */
.chooser__item--soon .chooser__name { color: var(--muted); font-weight: 550; }

/* The invite form's role picker. The field styles above were written for
   <input>; a <select> needs the same treatment or it renders as an OS widget
   with a white background in the dark theme. */
.field select {
  width: 100%;
  padding: .66rem .8rem;
  border-radius: var(--radius-sm);
  border: 1px solid var(--line);
  background: var(--bg);
  color: var(--ink);
  font: inherit;
  font-size: .95rem;
}
.field select:focus { outline: 2px solid var(--green); outline-offset: 1px; border-color: var(--green); }
td select {
  padding: .3rem .5rem;
  border-radius: var(--radius-sm);
  border: 1px solid var(--line);
  background: var(--bg);
  color: var(--ink);
  font: inherit;
  font-size: .88rem;
}

/* ---------------------------------------------------------------------------
   THE TRIAL HEADER

   It sits inside the tenant, directly under the crumb that names which tenant,
   because an entitlement is per portal — a countdown printed above the wrong
   tenant is a wrong number on a finance screen.

   No new colours. Green is "running", --warn is "a human has to act", and a
   DASHED left edge is already this page's word for "we could not read it" (the
   unknown step, the unknown tenant card). Unknown therefore looks like the
   other unknowns rather than like a fifth thing, and it never borrows the
   colour of a state we established.
   --------------------------------------------------------------------------- */
.trial-banner {
  border: 1px solid var(--line);
  border-left: 3px solid var(--muted);
  border-radius: var(--radius-sm);
  background: var(--surface);
  padding: calc(var(--u) * 2) calc(var(--u) * 2.5);
  margin-bottom: calc(var(--u) * 3);
}
.trial-banner[hidden] { display: none; }
.trial-banner p { margin: 0 0 calc(var(--u) * 1); max-width: var(--measure); }
.trial-banner p:last-child { margin-bottom: 0; }
.trial-banner .card__actions { margin-top: calc(var(--u) * 2); }
.trial-banner--trial { border-left-color: var(--green); background: var(--green-dim); }
.trial-banner--expired { border-left-color: var(--warn); background: var(--warn-bg); }
.trial-banner--warn { border-left-color: var(--warn); background: var(--warn-bg); }
/* Pending is not a warning. Nothing is wrong with an account whose trial has
   not started; something is simply not finished, and the checklist is right
   there. So it borrows the neutral surface and a dashed edge, which is this
   page's word for "not settled yet" rather than "you are in trouble". */
.trial-banner--pending { border-left-style: dashed; border-left-color: var(--muted); }
/* A FAILURE IS NOT A STATE COLOUR. When the read broke, the banner must not
   look like any status: no green, no amber, no chip. It takes the same dashed
   neutral treatment the page already uses for "we could not check". */
.trial-banner--failure,
.trial-banner--unknown,
.trial-banner--loading { border-left-style: dashed; border-left-color: var(--muted); background: var(--surface); }

/* ---------------------------------------------------------------------------
   THE TENANT SWITCHER — a button and a popover, top right.

   Not a <dialog>: a modal traps focus and dims the page behind it, which is far
   too much ceremony for "go to another tenant", and the page you are leaving
   should stay readable while you decide. Above 900px it is positioned against
   the trigger, so it lands under it; at 900px and below it hangs from the
   header instead (see the block after .switcher__menu[hidden]).
   --------------------------------------------------------------------------- */
.switcher { position: relative; display: inline-flex; }
.switcher[hidden] { display: none; }
.switcher__trigger {
  display: inline-flex;
  align-items: baseline;
  gap: calc(var(--u) * 0.75);
  max-width: 16rem;
  padding: calc(var(--u) * 0.75) calc(var(--u) * 1.5);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  color: var(--ink);
  font: inherit;
  font-size: .9rem;
  font-weight: 600;
  cursor: pointer;
}
.switcher__trigger:hover { border-color: var(--green); color: var(--ink-strong); }
.switcher__trigger:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }
.switcher__what {
  flex: 0 0 auto;
  font-size: .68rem;
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--muted);
}
.switcher__name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.switcher__caret { flex: 0 0 auto; color: var(--muted); font-size: .7rem; }

.switcher__menu {
  position: absolute;
  top: calc(100% + var(--u));
  right: 0;
  z-index: 30;
  width: min(24rem, calc(100vw - var(--u) * 4));
  max-height: min(70vh, 34rem);
  overflow-y: auto;
  padding: calc(var(--u) * 1.5);
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: 0 12px 32px rgb(0 0 0 / 28%);
  text-align: left;
}
/* `hidden` IS the switch, exactly as it is for the panels. A display rule that
   out-ranked it would leave a menu painted over the page with aria-expanded
   saying it was closed. */
.switcher__menu[hidden] { display: none; }
/* AT 900px AND BELOW THE MENU HANGS FROM THE HEADER, NOT FROM THE TRIGGER.
   Under the trigger, `right: 0` lines the menu's right edge up with the
   trigger's, and once the top bar wraps the trigger can sit anywhere along its
   row: on a phone it sat near the left edge and the menu ran 56-89px off the
   screen, where nothing scrolls to it. Hung from the header's own box it keeps
   a gutter on the right and is never wider than the header leaves room for. */
@media (max-width: 900px) {
  body.app-body header.site .wrap { position: relative; }
  body.app-body .switcher { position: static; }
  body.app-body .switcher__menu {
    right: calc(var(--u) * 2);
    width: min(24rem, calc(100% - var(--u) * 4));
  }
}
/* /app's own navs wrap. The global `nav` rule does not, and the 900px menu
   rules belong to the marketing header (#navigation), so without this the top
   bar and the footer links ran past a phone's edge. At 900px and below the top
   bar keeps to the right of its row, so the switcher's menu, hung from the
   header's right side, opens under its trigger. */
body.app-body nav { flex-wrap: wrap; }
@media (max-width: 900px) {
  body.app-body #topnav { margin-left: auto; justify-content: flex-end; }
}
.switcher__title,
.switcher__foot {
  margin: 0 0 var(--u);
  padding: 0 calc(var(--u) * 0.5);
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--muted);
}
.switcher__foot {
  margin: calc(var(--u) * 1.5) 0 0;
  font-size: .78rem;
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  line-height: 1.45;
}
.switcher__empty { margin: 0; padding: calc(var(--u) * 1); font-size: .88rem; color: var(--muted); }
.switcher__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: calc(var(--u) * 0.5); }
.switcher__list li { margin: 0; }
.switcher__row {
  display: flex;
  flex-direction: column;
  gap: calc(var(--u) * 0.25);
  padding: calc(var(--u) * 1.25) calc(var(--u) * 1.5);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  color: var(--ink);
  text-decoration: none;
}
.switcher__row:hover { background: var(--surface); border-color: var(--green); color: var(--ink-strong); }
.switcher__row:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }
.switcher__row--current { background: var(--green-dim); border-color: color-mix(in srgb, var(--green) 40%, transparent); }
.switcher__row-name { font-weight: 650; color: var(--ink-strong); overflow-wrap: anywhere; }
.switcher__row-state { font-size: .8rem; color: var(--muted); }

/* The Account trigger. It is a link, beside the switcher, never inside it. */
.topnav__account[hidden] { display: none; }

/* ---------------------------------------------------------------------------
   THE SUBSCRIPTION SECTION
   --------------------------------------------------------------------------- */
.sub-label { font-size: 1.15rem; }
.plan-form {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  gap: calc(var(--u) * 2);
  margin: 0 0 calc(var(--u) * 1.5);
}
.plan-form .field { margin: 0; min-width: min(22rem, 100%); }

/* THE PREREQUISITE BLOCK — the same block on Subscription and on the
   Integrations chain, so the two cannot disagree about a tenant.

   FOUR KINDS, AND THE COLOUR IS THE KIND. `--unmet` is the trial checklist and
   renders ONLY on 'pending'; `--connection` is the same chain read on a tenant
   whose trial is not the question, and it is deliberately the same warn colour
   because the missing piece is identical — only the sentence differs.
   `--stalled` is ours-not-yours, so it is dashed like the other two states this
   page does not blame the customer for. Unreadable is dashed for the same
   reason: "one item failed to read" is not a lesser answer than "one item is
   unmet" — neither starts a clock. */
.prereq { border-left: 3px solid var(--muted); }
.prereq--met { border-left-color: var(--green); }
.prereq--unmet { border-left-color: var(--warn); }
.prereq--connection { border-left-color: var(--warn); }
.prereq--stalled { border-left-color: var(--warn); border-left-style: dashed; }
.prereq--unknown { border-left-style: dashed; }
.prereq__list { margin-bottom: 0; }
.prereq__list li { max-width: var(--measure); }

/* A FAILURE READS AS A FAILURE. The note treatment, and a dashed neutral edge
   rather than the green a `.note` normally carries — a broken read must not
   borrow the look of something that worked. */
.note--failure { border-left-style: dashed; border-left-color: var(--muted); }

/* T-565: the usage card's headline count reads at the sub-label's size, and
   tabular numerals keep "1,187 of 1,200" steady as it updates. */
.sub-usage__count { font-size: 1.15rem; font-variant-numeric: tabular-nums; }
/* The opened manage panel sits inside its card, under the row of controls. */
.sub-manage__detail { margin-top: calc(var(--u) * 2); }
/* Separates two controls in ONE panel that do different things — the free
   tier switch and the paid checkout. They must not read as one control. */
.sub-manage__rule {
  border: 0;
  border-top: 1px solid var(--line);
  margin: calc(var(--u) * 2.5) 0;
}

/* ---- motion layer ----------------------------------------------------
   The hidden state is scoped to html.motion, and ONLY assets/motion.js
   sets that class - after it has collected every element it is going to
   reveal and attached the observer. So a blocked or broken script cannot
   leave anything invisible: no class, no rule, no hidden content.

   .is-now is for elements already on screen when the page lands. They get
   the end state with the transition switched off, because animating what
   the visitor is already looking at is a flicker, not a reveal. */
.motion [data-rv]{
  opacity:0; transform:translateY(12px);
  transition:opacity .55s cubic-bezier(.22,.61,.36,1),
             transform .55s cubic-bezier(.22,.61,.36,1);
  will-change:opacity, transform;
}
.motion [data-rv].is-in{ opacity:1; transform:none; }
.motion [data-rv].is-now{ transition:none; }

/* Belt and braces. motion.js already returns before setting the class when
   reduced motion is on; this also covers the case where it is turned on
   after the class exists. */
@media (prefers-reduced-motion:reduce){
  .motion [data-rv]{ opacity:1; transform:none; transition:none; }
}

/* ---- the allocation scene (bill split) -------------------------------
   Ported from the marketing-preview build, whose palette is already ours:
   its --navy and --emerald are this file's --navy and --green, to the hex.

   ONE DELIBERATE DIFFERENCE. There the whole thing is scroll-driven: the
   section is 190svh tall, the scene is sticky inside it, and progress is
   (86 - box.top) / span, so the reader has to keep scrolling to finish the
   animation. Here it plays itself once, on a timer, when the scene is in
   view - so the page is an ordinary length and nobody is held hostage to
   the wheel. The CHOREOGRAPHY is copied exactly; only its clock changed. */
.act--ledger{background:#EDF6F3}

.ledger-scene{display:grid;grid-template-columns:1.1fr .6fr 1fr;align-items:center;
  max-width:1000px;margin:calc(var(--u)*5) auto 0}

.supplier-record{padding:26px;background:#fff;border:1px solid #cbdfd8;
  border-radius:14px;box-shadow:0 14px 30px rgb(24 49 83 / .043)}
.supplier-record .record-top{display:flex;justify-content:space-between;gap:10px;
  font-size:10px;color:var(--muted);margin-bottom:20px;text-transform:uppercase;
  letter-spacing:.04em}
.supplier-record h3{font-size:21px;margin:0}
.supplier-record>p{font-size:12px;color:var(--muted);margin:5px 0 0}
.supplier-record>strong{display:block;font-size:40px;font-weight:650;
  letter-spacing:-.04em;color:var(--navy);margin:12px 0}
.supplier-record>strong>span{font-size:28px}
.paper-rule{height:1px;background:var(--line);margin-bottom:12px}
.ledger-entry{display:flex;justify-content:space-between;gap:8px;font-size:12px;
  padding:8px 0;font-variant-numeric:tabular-nums}
.supplier-record small{font-size:10px;color:var(--muted);display:block;margin-top:12px}

/* The connectors. --trace-a/--trace-b run 0 -> 1 and draw each path; the
   default of 1 means a drawn path when no script ever runs. */
.connector-space{height:290px}
.connector-space svg{height:100%;width:100%;overflow:visible;display:block}
.connector-space path{fill:none;stroke-width:2.5}
/* There is deliberately NO track behind these. A pale guide line showed the
   whole route before anything travelled it, which is the opposite of the
   effect: the line should look like it is finding its way to the card, not
   running along rails somebody already laid. */
.path-birch{stroke:var(--navy);stroke-dasharray:1;
  stroke-dashoffset:calc(1 - var(--trace-a,1))}
.path-halsted{stroke:var(--green);stroke-dasharray:1;
  stroke-dashoffset:calc(1 - var(--trace-b,1))}

.job-records{display:grid;gap:24px}
.job-record{display:grid;gap:3px;text-align:left;border:1px solid #bdd8ce;
  background:#fff;padding:23px;border-radius:12px;color:var(--navy);
  box-shadow:0 10px 22px rgb(24 49 83 / .027)}
/* The cards arrive when their line reaches them, nudged in from the right
   so the line reads as having delivered them rather than them having been
   there all along. Defaults of 1 and a zero nudge, as everywhere here. */
.job-record--birch{opacity:var(--card-birch,1);
  transform:translateX(calc((1 - var(--card-birch,1)) * 14px))}
.job-record--halsted{opacity:var(--card-halsted,1);
  transform:translateX(calc((1 - var(--card-halsted,1)) * 14px))}
.job-record span{font-size:14px;font-weight:600}
.job-record strong{font-size:29px;letter-spacing:-.03em;font-weight:650;
  font-variant-numeric:tabular-nums}
.job-record small{font-size:11px;color:var(--muted)}

/* Fades, but does not rise. Everything else on the page arrives with a 12px
   lift; inside a pinned stage that reads as a second thing moving while the
   card is already moving, and the eye cannot tell which one is the story. */
.motion .ledger-caption[data-rv]{transform:none}
.ledger-caption{max-width:var(--measure);margin:calc(var(--u)*4) auto 0;
  font-size:.9rem;color:var(--muted);line-height:1.55}
.ledger-caption strong{color:var(--ink)}

.story-equation{margin-top:calc(var(--u)*4);text-align:center}
/* The equation's label rides in on the FIRST figure - it shares --reveal-birch
   rather than taking a variable of its own, so it cannot drift away from the
   thing it introduces. At rest it is hidden too: "the bill alone" means the
   bill alone. */
.story-equation>span{display:block;color:var(--muted);font-size:12px;
  margin-bottom:4px;opacity:var(--reveal-birch,1)}
.story-equation strong{font-size:22px;color:var(--navy);font-weight:600;
  font-variant-numeric:tabular-nums}
.story-equation strong>span{display:inline-block}

/* Set by the inline script in the document head, before first paint, and
   removed again at DOMContentLoaded if the driver never claimed it. This
   class is the ONLY thing that starts the section hidden - the scene here,
   the screenshot further down - and every default stays 1, so no-script,
   reduced-motion and narrow all render the finished scene. */
.alloc-armed [data-allocation]{
  --trace-a:0;--trace-b:0;
  --card-birch:0;--card-halsted:0;
  --reveal-birch:0;--reveal-halsted:0;--reveal-total:0;
}

/* The amounts arrive as their connector lands. Default 1: with no script,
   with reduced motion, or on a narrow screen, every figure is simply there. */
[data-allocation-reveal="birch"]{opacity:var(--reveal-birch,1)}
[data-allocation-reveal="halsted"]{opacity:var(--reveal-halsted,1)}
[data-allocation-reveal="total"]{opacity:var(--reveal-total,1)}

@media (prefers-reduced-motion:reduce){
  [data-allocation-reveal]{opacity:1 !important}
  .path-birch,.path-halsted{stroke-dashoffset:0 !important}
  .job-record--birch,.job-record--halsted{opacity:1 !important;transform:none !important}
}

/* Stacked: the connectors describe a left-to-right journey that does not
   exist in one column, so they go rather than rotate into nonsense. */
@media (max-width:900px){
  .ledger-scene{grid-template-columns:1fr;gap:calc(var(--u)*2.5)}
  .connector-space{display:none}
  .job-record--birch,.job-record--halsted{opacity:1 !important;transform:none !important}
  .supplier-record>strong{font-size:32px}
  .job-record strong{font-size:24px}
  [data-allocation-reveal]{opacity:1 !important}
}

/* ---- the screenshot that proves the split -------------------------
   This replaced a pinned stage that zoomed the Halsted card and morphed it
   into the screenshot. It worked and it was measured, but two scroll holds
   in one section meant the page took control twice, and the morph read as a
   trick rather than as evidence. The scene now simply stays, the card it
   ends on lights up, and the real deal record fades in underneath carrying
   the same two figures in the same colour. Same claim, no sleight of hand.

   Nothing here is position:sticky, nothing holds the scroll, and the
   section is an ordinary height again. */

/* The card the screenshot is about. Three quick pulses, then it stays lit -
   the flash catches the eye, the steady state is what ties it to the two
   highlights in the screenshot below. forwards, so it holds the last frame
   rather than snapping back to white. */
/* PAIRED stops and linear timing. Single stops with `ease` interpolate the
   whole way between them, so the three flashes smeared into one slow ramp -
   sampled in-page, it read as two soft peaks rather than three flashes.
   Holding each colour flat across a pair and crossing in 2% makes each
   pulse a flat on/off. Three pulses, then it stays lit. */
@keyframes halsted-flash{
  0%,   6%   {background:#fff;    border-color:#bdd8ce}
  8%,  16%   {background:#FFF6DC; border-color:#E8C96B}
  18%, 26%   {background:#fff;    border-color:#bdd8ce}
  28%, 36%   {background:#FFF6DC; border-color:#E8C96B}
  38%, 46%   {background:#fff;    border-color:#bdd8ce}
  48%, 100%  {background:#FFF6DC; border-color:#E8C96B}
}
.job-record--halsted[data-emph-on]{
  animation:halsted-flash .9s linear forwards;
  box-shadow:0 10px 22px rgb(24 49 83 / .06), 0 0 0 1px #E8C96B inset}

/* --shot-in defaults to 1, so no script means a visible screenshot. It is
   hidden only under html.alloc-armed, which the head script sets BEFORE first
   paint, and motion.js sets it back to 1 inline the moment the figure's top
   edge reaches the bottom of the window - on its own position, not the
   scene's clock - and the transition fades it up.

   Hidden by THIS rule and never by a script after first paint (PR #103,
   re-check of 2026-10-08): a script that set it to 0 after the page had
   painted it made the transition below play as a fade OUT on load. And
   motion.js keeps the generic reveal layer off this element - two owners of
   one opacity is the bug that has already bitten three times here. */
.ledger-shot{margin:calc(var(--u)*5) auto 0;max-width:1000px;padding:0;
  opacity:var(--shot-in,1);transition:opacity .85s ease}
.alloc-armed .ledger-shot{--shot-in:0}
.ledger-shot__frame{position:relative;display:block;width:100%}
.ledger-shot img{display:block;width:100%;height:auto;border-radius:12px;
  border:1px solid #cbdfd8;box-shadow:0 14px 34px rgb(24 49 83 / .10)}
.ledger-shot figcaption{margin:calc(var(--u)*2.5) auto 0;max-width:var(--measure);
  font-size:.875rem;color:var(--muted);line-height:1.55;text-align:center}
.ledger-shot figcaption strong{color:var(--ink)}

/* The two highlights. Percentages are of the IMAGE box, which is why
   .ledger-shot__frame exists. MEASURED by scanning 04-cost-lines.png for
   ink: the address at 5.03/4.19 and the ABC Supply row's ink at x 547-1702,
   y 422-451 of 1770x573. REPLACE THE SCREENSHOT AND BOTH ARE WRONG - they
   are pixel positions in disguise, not a layout that reflows.
   multiply, not a paint-over, so the screenshot's own ink darkens through
   them and stays readable: row text lands at 8.91:1. */
.ledger-shot__hi{position:absolute;border-radius:5px;background:#FFE9A3;
  mix-blend-mode:multiply;opacity:0;transition:opacity .6s ease .15s}
.ledger-shot[data-emph-on] .ledger-shot__hi{opacity:1}
.ledger-shot__hi--addr{left:4.2%;top:2.6%;width:15.4%;height:11.6%}
.ledger-shot__hi--row{left:29.5%;top:71.3%;width:68.2%;height:9.4%;
  transition-delay:.45s}

@media (prefers-reduced-motion:reduce){
  /* No flash and no fade, but the highlights STAY - they are information,
     not decoration, and the screenshot is the evidence for the claim. */
  .job-record--halsted[data-emph-on]{animation:none;
    background:#FFF6DC;border-color:#E8C96B}
  .ledger-shot{opacity:1 !important}
  .ledger-shot__hi{transition:none !important}
}

/* Narrow screens get the finished state outright - the scene's connectors
   are display:none there, so there is no sequence to be the second half of. */
@media (max-width:900px){
  .ledger-shot{opacity:1 !important;transition:none}
}

/* PRINT GETS THE FINISHED PAGE. Printing a page nobody had scrolled through
   dropped everything the reveal layer and the scene were still holding back
   (PR #103, R5): pages 4-8 of the homepage's PDF came out empty. The values
   motion.js writes are inline, so only !important reaches past them. */
@media print{
  header.site{position:static}
  .motion [data-rv]{opacity:1 !important;transform:none !important;transition:none !important}
  [data-allocation]{--trace-a:1 !important;--trace-b:1 !important;
    --card-birch:1 !important;--card-halsted:1 !important;
    --reveal-birch:1 !important;--reveal-halsted:1 !important;--reveal-total:1 !important}
  .ledger-shot{opacity:1 !important;transition:none !important}
}

/* ---- site footer -----------------------------------------------------
   Ported from the marketing-preview build, measured off its stylesheet:
   the 2fr/1fr/1fr/1fr grid, 36px gutter, 175px mark, 13px links on 12px
   leading, the 11px base bar on a rule, and both of its breakpoints
   (900px -> three columns, 680px -> two and the base bar stacks).

   ONE DELIBERATE DIFFERENCE: a border-top. The preview separates a white
   footer from an off-white page by colour alone, which is the 1.06 step
   this project has already measured as invisible. A rule is what actually
   divides them.

   The portal at /app keeps the old compact `footer.site` - it is an
   application, and the .app-body override exists for exactly that. */
/* No margin-top: the preview lets padding-top:52px do all the spacing, and
   an 80px strip of page ground between a full-bleed band and a white footer
   reads as a gap rather than as breathing room.

   main's own 80px padding-bottom does the same thing, and is RIGHT for a
   page whose content ends in ordinary prose - it stops that butting into the
   footer. It is only wrong when the last thing on the page is a full-bleed
   band, which already has its own padding and its own colour. So it is
   dropped for exactly that case, and left alone everywhere else. */
main.wrap:has(> .act--bleed:last-child){padding-bottom:0}
.site-foot{background:var(--surface);padding-top:52px;
  border-top:1px solid var(--line)}
.foot-grid{display:grid;grid-template-columns:2fr 1fr 1fr 1fr;gap:36px}
.site-foot .foot-brand{display:inline-block}
.site-foot .foot-brand img{width:175px;height:auto;display:block}
.site-foot p{color:var(--muted);font-size:13px;margin-top:20px;line-height:1.5}
.site-foot .micro{font-size:11px;margin-top:14px}
.site-foot h2{font-size:13px;letter-spacing:0;margin:0 0 15px;color:var(--ink)}
.foot-grid>div>a:not(.foot-brand){display:block;text-decoration:none;
  font-size:13px;color:var(--muted);margin-bottom:12px}
.foot-grid>div>a:not(.foot-brand):hover{color:var(--nav-hover)}
.foot-base{display:flex;justify-content:space-between;gap:24px;
  padding-block:24px;margin-top:40px;border-top:1px solid var(--line);
  font-size:11px;color:var(--muted)}

@media (max-width:900px){.foot-grid{grid-template-columns:1.5fr 1fr 1fr;gap:24px}}
@media (max-width:680px){
  .foot-grid{grid-template-columns:1fr 1fr}
  .foot-base{flex-direction:column;gap:8px}
}


/* ======================================================================
   SUBSCRIPTION PAGE - the billing-page layout (pilot for the portal's
   modernised look). Scoped to the portal; the marketing pages never load
   these classes. Order on the page: current plan, its controls, usage,
   what the tenant has, the fine print, and cancel alone at the foot.
   ====================================================================== */
body.app-body .panel-sub{color:var(--muted);margin:-.4rem 0 calc(var(--u)*3)}
body.app-body #subscription{display:flex;flex-direction:column;gap:calc(var(--u)*2)}
body.app-body #subscription > .card{margin:0}
/* A ghost <button> had no background of its own, so every one in the portal
   wore the browser's grey ButtonFace fill. Links never showed it. */
body.app-body button.btn-ghost{background:transparent}
body.app-body button.btn-ghost:hover{background:var(--surface-2)}

/* The plan card and the controls under it read as ONE block. */
body.app-body .sub-plan{border-radius:var(--radius) var(--radius) 0 0;padding-bottom:calc(var(--u)*2.5);
  background:linear-gradient(180deg,var(--surface-2),var(--surface))}
body.app-body #subscription > .sub-plan + .sub-manage,
body.app-body #subscription > .sub-plan + .note + .sub-manage{margin-top:calc(var(--u)*-2);border-top:1px solid var(--line-soft);
  border-radius:0 0 var(--radius) var(--radius)}
body.app-body .sub-plan__eyebrow{font-size:.72rem;font-weight:700;letter-spacing:.09em;text-transform:uppercase;
  color:var(--muted);margin:0 0 calc(var(--u)*1.25)}
body.app-body .sub-plan__head{display:flex;align-items:center;flex-wrap:wrap;gap:calc(var(--u)*1.5)}
body.app-body .sub-plan .sub-label{margin:0;font-size:1.65rem;line-height:1.2;letter-spacing:-.01em}
body.app-body .sub-plan .sub-label strong{color:var(--ink-strong);font-weight:700}
body.app-body .sub-plan__meta{margin:calc(var(--u)*1) 0 calc(var(--u)*2);color:var(--ink);font-size:.95rem}
body.app-body .sub-plan__days{display:none} /* the badge carries the day count */
body.app-body .sub-plan p{max-width:var(--measure)}

body.app-body .sub-badge{display:inline-flex;align-items:center;gap:.4rem;font-size:.75rem;font-weight:700;
  padding:.28rem .7rem;border-radius:999px;border:1px solid var(--line);color:var(--ink);background:var(--surface-2)}
body.app-body .sub-badge::before{content:"";width:.45rem;height:.45rem;border-radius:50%;background:currentColor}
body.app-body .sub-badge--ok{color:var(--green-deep);background:var(--green-dim);
  border-color:color-mix(in srgb,var(--green) 40%,transparent)}
body.app-body .sub-badge--trial{color:#9cc8ff;background:#16243a;border-color:#2b4466}
body.app-body .sub-badge--warn{color:var(--warn);background:var(--warn-bg);
  border-color:color-mix(in srgb,var(--warn) 45%,transparent)}
body.app-body .sub-badge--neutral{color:var(--muted)}

/* Manage: headings quiet, the primary action filled. */
body.app-body .sub-manage h3{font-size:.95rem;margin-bottom:.35rem}
body.app-body .sub-manage__actions{display:flex;flex-wrap:wrap;gap:calc(var(--u)*1.25);margin-top:calc(var(--u)*2)}
body.app-body #subscription .btn-primary{background:var(--green);border-color:var(--green);color:var(--on-green)}
body.app-body #subscription .btn-primary:hover{background:var(--green-bright);border-color:var(--green-bright);color:var(--on-green)}

/* Usage meter */
body.app-body .meter{height:.6rem;border-radius:999px;background:var(--surface-2);border:1px solid var(--line);
  overflow:hidden;margin:calc(var(--u)*1.25) 0 .4rem;max-width:34rem}
body.app-body .meter__fill{display:block;height:100%;background:var(--green);border-radius:inherit}
body.app-body .meter--mid .meter__fill{background:var(--warn)}
body.app-body .meter--high .meter__fill{background:var(--orange)}
body.app-body .meter__pct{font-size:.8rem;color:var(--muted);margin:0 0 calc(var(--u)*2)}
body.app-body .sub-usage__count{font-size:1.05rem}

/* The fine print reads as fine print. */
body.app-body .sub-cta{background:transparent;box-shadow:none}

/* Cancel: alone, quiet, last. */
body.app-body .sub-danger{display:grid;grid-template-columns:1fr auto;align-items:center;gap:calc(var(--u)*1.5) calc(var(--u)*3);
  margin-top:calc(var(--u)*2);padding:calc(var(--u)*2.5) calc(var(--u)*3);border:1px solid var(--line-soft);
  border-radius:var(--radius)}
body.app-body .sub-danger h4{margin:0 0 .2rem;font-size:.95rem;color:var(--ink-strong)}
body.app-body .sub-danger p{margin:0}
body.app-body .sub-danger > .note{grid-column:1 / -1;margin:0}

body.app-body .sub-about{margin-top:calc(var(--u)*3);color:var(--muted);font-size:.88rem;max-width:var(--measure)}
body.app-body .sub-about summary{cursor:pointer;color:var(--ink)}

@media (max-width:640px){
  body.app-body .sub-danger{grid-template-columns:1fr}
  body.app-body .sub-plan .sub-label{font-size:1.35rem}
}

/* ======================================================================
   TENANT HEADER WEIGHT. Full on the Overview; on every other tenant page
   the big header steps aside and the crumb becomes a slim identity bar,
   so the section title leads. Set by renderHero() via data-weight.
   ====================================================================== */
body.app-body .hero[data-weight="compact"]{display:none}
body.app-body .tenant-crumb[data-weight="compact"]{
  margin-bottom:calc(var(--u)*3.5);padding:calc(var(--u)*1.1) calc(var(--u)*1.75);
  border:1px solid var(--line-soft);border-radius:var(--radius-sm);background:var(--surface);
  font-size:.85rem;color:var(--muted)}
body.app-body .tenant-crumb[data-weight="compact"] strong{color:var(--ink-strong);font-size:.95rem}
body.app-body .tenant-crumb[data-weight="compact"] code{font-size:.75rem}
body.app-body .tenant-crumb[data-weight="compact"] + .auth-banner,
body.app-body .tenant-crumb[data-weight="compact"] ~ .app-panel > h2:first-child{margin-top:0}

/* The Overview's identity card: same family as the Subscription plan card,
   with the crumb joined beneath it as its footer row. */
body.app-body .hero[data-weight="overview"]{
  display:grid;grid-template-columns:auto 1fr;column-gap:calc(var(--u)*2.25);align-items:center;
  margin-bottom:0;padding:calc(var(--u)*3);border:1px solid var(--line);border-bottom-color:var(--line-soft);
  border-radius:var(--radius) var(--radius) 0 0;background:linear-gradient(180deg,var(--surface-2),var(--surface));
  box-shadow:var(--shadow-1)}
body.app-body .hero[data-weight="overview"]::before{
  content:attr(data-initial);grid-row:1 / span 3;display:grid;place-items:center;
  width:3.5rem;height:3.5rem;border-radius:14px;font-size:1.5rem;font-weight:800;
  color:var(--green-deep);background:var(--green-dim);border:1px solid color-mix(in srgb,var(--green) 40%,transparent)}
body.app-body .hero[data-weight="overview"] .eyebrow{margin:0 0 .15rem;font-size:.72rem;color:var(--muted);letter-spacing:.09em}
body.app-body .hero[data-weight="overview"] h1{margin:0;font-size:clamp(1.4rem,1.1rem + .9vw,1.85rem)}
body.app-body .hero[data-weight="overview"] .lede{margin:.25rem 0 0;font-size:.92rem;color:var(--muted)}
body.app-body .hero[data-weight="overview"] .lede strong{color:var(--ink)}
body.app-body .hero[data-weight="overview"] ~ .tenant-crumb:not([hidden]){
  margin-bottom:calc(var(--u)*4);padding:calc(var(--u)*1.25) calc(var(--u)*3);border:1px solid var(--line);
  border-top:0;border-radius:0 0 var(--radius) var(--radius);background:var(--surface);font-size:.85rem}
body.app-body .hero[data-weight="overview"] ~ .auth-banner:not([hidden]){margin-top:calc(var(--u)*2)}
/* The account banner ("QuickBooks connected.") sits BETWEEN the card and its
   footer row in the markup, so while it shows, the joined card drew as two
   halves around it, on the first-connect screen of all places. While it is
   visible each half closes on its own: the header takes all four corners and
   the crumb its top border back. */
body.app-body .hero[data-weight="overview"]:has(+ .auth-banner:not([hidden])){
  border-radius:var(--radius);border-bottom-color:var(--line)}
body.app-body .hero[data-weight="overview"] ~ .auth-banner:not([hidden]) + .tenant-crumb:not([hidden]){
  border-top:1px solid var(--line);border-radius:var(--radius)}
@media (max-width:640px){
  body.app-body .hero[data-weight="overview"]{padding:calc(var(--u)*2.25)}
  body.app-body .hero[data-weight="overview"]::before{width:2.75rem;height:2.75rem;font-size:1.2rem}
}

/* Inside the Overview card the footer row is the way back out, so it does
   not repeat the name the headline already carries. The markup is unchanged
   (the crumb tests read it); only what is drawn changes. */
body.app-body .hero[data-weight="overview"] ~ .tenant-crumb strong,
body.app-body .hero[data-weight="overview"] ~ .tenant-crumb > span[aria-hidden]{display:none}
body.app-body .hero[data-weight="overview"] ~ .tenant-crumb > a::before{content:"\2190\00a0"}
body.app-body .hero[data-weight="overview"] ~ .tenant-crumb > code::before{content:"Portal ";font-family:var(--font-body,inherit);color:var(--muted)}

/* The step chain sat flush on whatever card followed it. */
body.app-body #overview .steps{margin-bottom:calc(var(--u)*3)}

/* ======================================================================
   PORTAL-WIDE BOX TREATMENT. Every card on every portal page takes the
   same gradient as the Subscription plan card and the Overview header,
   and every box in a panel's column sits one --stack apart, so nothing
   butts against its neighbour and the rhythm is the same on each page.
   ====================================================================== */
body.app-body{--stack:calc(var(--u)*2.5)}
body.app-body .card{background:linear-gradient(180deg,var(--surface-2),var(--surface))}

/* The boxes in a panel's column: cards, notes, warnings, the step chain
   and the trial banner. Direct children only, so a note INSIDE a card
   keeps its own spacing. */
body.app-body .app-panel > :is(.card,.note,.warn,.steps,.scroll),
body.app-body .app-panel > div > :is(.card,.note,.warn,.steps,.scroll),
body.app-body .app-panels > .trial-banner{margin-top:0;margin-bottom:var(--stack)}
body.app-body .app-panel > div > :is(.card,.note,.warn,.steps):last-child{margin-bottom:0}

/* The subscription column was already a flex stack; give it the same step. */
body.app-body #subscription{gap:var(--stack)}
body.app-body #subscription > :is(.card,.note,.warn,.sub-danger){margin:0}
body.app-body #subscription > .sub-plan + .sub-manage,
body.app-body #subscription > .sub-plan + .note + .sub-manage{margin-top:calc(var(--stack) * -1)}
body.app-body #subscription > .sub-manage{background:var(--surface)}
/* The step chain and the slim tenant bar are boxes too. */
body.app-body .step,
body.app-body .tenant-crumb[data-weight="compact"]{background:linear-gradient(180deg,var(--surface-2),var(--surface))}
/* A table inside a .scroll wrapper carried its own bottom margin on top of
   the wrapper's, doubling every gap after a table (40px, not --stack). */
body.app-body .scroll > table{margin-bottom:0}

/* The trial banner in the card family: the same top-lit gradient, tinted by
   state, with the coloured left edge kept as the state marker. */
body.app-body .trial-banner{border-radius:var(--radius);box-shadow:var(--shadow-1)}
body.app-body .trial-banner--trial{
  background:linear-gradient(180deg,color-mix(in srgb,var(--green) 22%,var(--surface-2)),var(--green-dim));
  border-color:color-mix(in srgb,var(--green) 35%,transparent);border-left-color:var(--green)}
body.app-body .trial-banner--expired,
body.app-body .trial-banner--warn{
  background:linear-gradient(180deg,color-mix(in srgb,var(--warn) 16%,var(--surface-2)),var(--warn-bg))}
body.app-body .trial-banner--pending,
body.app-body .trial-banner--failure,
body.app-body .trial-banner--unknown,
body.app-body .trial-banner--loading{background:linear-gradient(180deg,var(--surface-2),var(--surface))}
/* The tinted gradients start lighter than the flat fills they replaced, and
   --muted on them measured 3.94-4.04:1 at the text's own position (trial and
   reauth), under AA's 4.5:1 for 13px text. That line is the sync sentence, so
   it is lifted toward --ink inside the tinted states: 6.0:1 or better on
   either gradient, measured at 1360px and 390px. The neutral states keep
   --muted, which still passes there (5.55:1). */
body.app-body .trial-banner:is(.trial-banner--trial,.trial-banner--expired,.trial-banner--warn) .step__state{
  color:color-mix(in srgb,var(--muted) 55%,var(--ink))}

/* ======================================================================
   LEFT RAIL, MODERNISED (after HubSpot's 2025 navigation): compact rows,
   full pill shapes, a light fill for the current page instead of a
   bordered green box, no underline, icon and label centred on one line.
   ====================================================================== */
body.app-body .app-rail{padding:calc(var(--u)*1.5) calc(var(--u)*1.25) calc(var(--u)*3)}
body.app-body .app-nav ul{gap:2px}
body.app-body .app-nav a{
  min-height:2.15rem;padding:.4rem .85rem;gap:.7rem;border:0;border-radius:999px;
  color:#c3ccd8;font-size:.9rem;font-weight:500;line-height:1.2;text-decoration:none}
body.app-body .app-nav a:hover{background:rgb(255 255 255 / .06);color:var(--ink-strong)}
body.app-body .app-nav a[aria-current]{
  background:rgb(255 255 255 / .12);color:var(--ink-strong);font-weight:600;
  border:0;text-decoration:none}
body.app-body .app-nav a[aria-current] .nav-ico{color:var(--green-bright)}
body.app-body .app-nav .nav-ico{width:1.05rem;height:1.05rem;stroke-width:1.6}
body.app-body .app-nav .tag{font-size:.6rem;padding:.12rem .45rem}

/* Section labels: quiet, and the second group is set off by a hairline. */
body.app-body .app-nav__title{
  margin:calc(var(--u)*1.5) 0 calc(var(--u)*.6);padding:0 .85rem;
  font-size:.68rem;font-weight:600;letter-spacing:.08em;color:#7d8ea3}
body.app-body .app-nav ul + .app-nav__title{
  margin-top:calc(var(--u)*1.75);padding-top:calc(var(--u)*1.75);border-top:1px solid var(--line)}
/* The tenant's name: the body face, not monospace, on one line. */
body.app-body .app-nav__title code{
  display:block;margin-top:.2rem;font-family:inherit;font-size:.86rem;font-weight:600;
  letter-spacing:0;text-transform:none;color:var(--ink-strong);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}

/* Collapse: a quiet row in the same shape as the links. */
body.app-body .rail-toggle{
  border:0;border-radius:999px;padding:.4rem .85rem;margin-bottom:calc(var(--u)*1);
  font-size:.78rem;font-weight:500;letter-spacing:.02em;text-transform:none;color:#7d8ea3}
body.app-body .rail-toggle:hover{background:rgb(255 255 255 / .06);color:var(--ink-strong)}

/* Collapse lives at the foot of the rail and stays there: pushed down by the
   flex column, and sticky so a long rail that scrolls still shows it. */
body.app-body .app-rail{padding-bottom:0}
body.app-body .rail-toggle{
  margin-top:auto;margin-bottom:0;position:sticky;bottom:0;
  padding:.7rem .85rem;border-radius:0;border-top:1px solid var(--line);
  background:var(--surface)}
body.app-body .rail-toggle:hover{background:var(--surface);color:var(--ink-strong)}
@media (max-width:860px){body.app-body .rail-toggle{position:static}}

/* The collapsed rail's icon-only links, ONLY where the rail can collapse.
   At 48rem and below the rail is a row of chips with no Collapse control
   (the 48rem block above hides it), but the collapsed state is remembered
   (jobline.rail) and survives a narrower window. The link rule is (0,4,2), so
   unscoped it out-ranked that block's (0,3,1) undo and squeezed every chip to
   2.3rem with its label overlapping the next, with no way to expand. The
   min-width is the exact complement of that block's max-width. */
@media (min-width:48.0625rem){
  body.app-body .app-shell[data-rail="collapsed"] .app-nav a{width:2.3rem;margin-inline:auto;padding:0}
  body.app-body .app-shell[data-rail="collapsed"] .rail-toggle{padding-inline:0}
}
