/* ==========================================================================
   DentaCloudPH — marketing site design system
   Palette is the ClinicSol dark theme. Every colour is a token; nothing below
   the :root block hard-codes a brand hex. Change a value here, it changes
   site-wide.
   ========================================================================== */

:root {
  /* Tells the browser to render its OWN controls light — checkboxes, scrollbars,
     select dropdowns, date pickers, spinners. Flipped with the theme on
     2026-09-22; left at dark it paints native controls dark on a paper ground,
     which on /book-a-demo.html is the consent checkbox that gates submission. */
  color-scheme: light;

  /* ── LIGHT THEME — 2026-09-22 ────────────────────────────────────────────
     Founder's decision, and it returns the site to its own brand direction:
     D-004 / docs/03-brand-brief.md chose a WARM PAPER ground, #F5F0EA — "the
     one ground, never interrupted". The dark navy this replaces predates that
     brief. Colours below are taken from the accepted Manila palette in
     dentacloudph.com/web/app/globals.css, which the rebuilt site already runs.

     WHAT THIS IS NOT: a faithful port of the IN REGISTER system. That system
     has no card chrome at all — paper, one inset band, dark plates, and pure
     white reserved for form fields. This site is built entirely from cards, so
     porting it exactly would be a rebuild rather than a re-theme. This keeps
     the card architecture and dresses it in the real brand colours. The full
     system lands with the rebuilt site.

     PLATES STAY DARK. --plate is "product plate + footer" in the brief, so the
     footer and product panels remain #08131F and read as windows cut into the
     sheet. That is the system working, not an oversight. */
  --primary:      #0078D4;   /* Brand blue. Button fill, focus ring. */
  --primary-600:  #0063B1;   /* Blue as TEXT — 5.42 on paper. Never use --primary for text. */
  --primary-700:  #004E8C;
  /* --secondary is this stylesheet's generic accent: links, focus rings,
     eyebrows, FAQ summaries, stat units — 16 rules, nearly all of them sitting
     on the ground. Cyan on paper is about 1.6:1, so the token becomes the
     text-safe blue and cyan keeps its one legitimate home below. */
  --secondary:    #0063B1;
  --on-plate:     #50E6FF;   /* Cyan. ONLY on dark plates (the footer). Never on paper. */
  --bg:           #F5F0EA;   /* paper — the one ground */
  --surface:      #ECE6DE;   /* paper-2 — the only inset band */
  --card:         #FFFFFF;
  --border:       rgb(15 23 42 / .22);
  --text:         #0F172A;   /* ink — 15.76 on paper */
  --muted:        #4E5A6B;   /* 5.0 on the tinted panels, not just on paper. */   /* ink-3 — 5.14, captions and meta */
  --gold:         #D4AF37;

  /* Derived */
  --border-soft:  rgb(15 23 42 / .12);
  --card-hover:   #FBF8F4;
  --plate:        #08131F;   /* Footer and product panels. */
  --plate-2:      #132238;
  --ink-2:        #3A4657;   /* 8.45 — body copy where --text is too heavy */
  --gold-ink:     #6B5310;   /* Gold is 2.1 on paper. This is the text-safe one. */

  /* Ground treatments — the complete set. Nothing outside this list is a
     ground. Flat fills only: a gradient has a contrast value at every point
     and a test at only one, which is how .cta-band acquired four failures
     nobody could see. Blue is capped at .06 and gold at .12 because #0078D4
     (L .182) costs 2.1x more contrast per unit alpha than #D4AF37 (L .449).
     --ground-mark retires with the founding offer. See docs/21. */
  --ground-wash:  #E6E9E9;   /* = rgb(0 120 212 / .06) over the paper, flattened */
  --ground-mark:  #F1E8D5;   /* = rgb(212 175 55 / .12) over the paper, flattened */
  --rule-plate:   rgb(245 240 234 / .18);   /* hairline INSIDE a plate */
  --ink-inv:      #F5F0EA;   /* Text on a dark plate — tracks the paper. */   /* Gold is 2.1 on paper. This is the text-safe one. */
  --success:      #0B6039;   /* 4.25 on paper at the previous value */
  --warning:      #8A6300;
  --danger:       #B5301A;

  /* Type scale — fluid, clamps between mobile and desktop */
  --fs-display: clamp(2.4rem, 1.2rem + 4.2vw, 4.25rem);
  --fs-h1:      clamp(2.1rem, 1.3rem + 3vw, 3.25rem);
  --fs-h2:      clamp(1.7rem, 1.2rem + 2vw, 2.5rem);
  --fs-h3:      clamp(1.2rem, 1.05rem + .6vw, 1.5rem);
  --fs-lead:    clamp(1.05rem, .98rem + .45vw, 1.25rem);
  --fs-body:    1rem;
  --fs-sm:      .875rem;
  --fs-xs:      .78rem;

  /* Layout */
  --container: 1180px;
  --gutter: clamp(1.15rem, 4vw, 2rem);
  --section-y: clamp(4rem, 9vw, 7.5rem);

  --r-sm: 8px;
  --r-md: 14px;
  --r-lg: 20px;
  --r-xl: 28px;
  --r-pill: 999px;

  /* Softened for paper. Black at .65 is a dark-theme shadow: on a light ground
     it reads as dirt rather than depth. Ink at low alpha instead. */
  --shadow-md: 0 10px 28px -14px rgb(15 23 42 / .28);
  --shadow-lg: 0 28px 60px -26px rgb(15 23 42 / .32);
  --shadow-glow: 0 0 0 1px rgb(0 120 212 / .35), 0 16px 40px -14px rgb(0 120 212 / .30);

  --ease: cubic-bezier(.22,.61,.36,1);
  --header-h: 72px;
}

/* ---------- Reset ---------- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; scroll-padding-top: calc(var(--header-h) + 16px); }
body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: Geist, "DM Sans", system-ui, -apple-system, sans-serif;
  font-size: var(--fs-body);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}
img, svg, video { display: block; max-width: 100%; height: auto; }
h1,h2,h3,h4 { margin: 0 0 .5em; line-height: 1.15; letter-spacing: -.02em; font-weight: 700; }
h1 { font-size: var(--fs-h1); }
h2 { font-size: var(--fs-h2); }
h3 { font-size: var(--fs-h3); letter-spacing: -.01em; }
p { margin: 0 0 1rem; }
a { color: var(--secondary); text-decoration: none; }
a:hover { text-decoration: underline; }
ul { margin: 0; padding: 0; list-style: none; }
:focus-visible { outline: 2px solid var(--secondary); outline-offset: 3px; border-radius: 4px; }

/* ---------- Layout primitives ---------- */
.container { width: 100%; max-width: var(--container); margin-inline: auto; padding-inline: var(--gutter); }
.container--narrow { max-width: 820px; }
.section { padding-block: var(--section-y); position: relative; }
.section--tight { padding-block: clamp(2.5rem, 5vw, 4rem); }
.section--surface { background: var(--surface); border-block: 1px solid var(--border-soft); }
/* The section directly under a page hero. It used to carry an inline
   padding-top:0 on nine pages, on the reasoning that the hero's own bottom
   padding was enough space. It is not: the hero has its own background, so the
   first card started on the exact pixel the hero ended and read as overlapping
   it. Declared after --tight so it wins on pages carrying both. */
.section--after-hero { padding-top: clamp(2rem, 5vw, 3rem); }
.center { text-align: center; }
.muted { color: var(--muted); }
.lead { font-size: var(--fs-lead); color: var(--muted); max-width: 62ch; }
.grid { display: grid; gap: clamp(1rem, 2.2vw, 1.75rem); }
/* Explicit column counts rather than auto-fit.
   auto-fit sizes by available width, so .grid-2 resolved to THREE columns in
   the 1180px container — the class promised two and the layout gave three,
   which is why every four-card section using it came out 3 + 1 with the last
   card stranded alone ("Financial & revenue reports" on features, "Four things
   we will not compromise on" on about). Naming the counts makes the layout
   predictable at every width instead of a function of the container. Two
   columns also gives each card ~50% more width, which closes most of the
   height gap between a card carrying a screenshot and one that is only text. */
.grid-2 { grid-template-columns: 1fr; }
.grid-3 { grid-template-columns: 1fr; }
.grid-4 { grid-template-columns: 1fr; }
@media (min-width: 560px) { .grid-4 { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 620px) { .grid-3 { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 720px) { .grid-2 { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 900px) { .grid-4 { grid-template-columns: repeat(4, 1fr); } }
@media (min-width: 980px) { .grid-3 { grid-template-columns: repeat(3, 1fr); } }

/* Cards sized to their own content instead of stretching to the tallest in the
   row. Stretch is the right default — equal-height cards read as a set, and
   any card carrying a visual gets it bottom-anchored (see .card below) so the
   extra height is used. It is the wrong default when the cards are pure text
   or a small table and their copy lengths differ sharply: on pricing.html the
   add-on pair left 370px of nothing under the shorter card at 768px, because
   its neighbour's description wrapped to twice as many lines. A ragged bottom
   edge is a far smaller cost than a hole that size. */
.grid--top { align-items: start; }

/* Balanced last row.
   An item count that does not divide by the column count leaves the last row
   short. A row that is merely short is fine — two cards under three reads as a
   grid. A row holding exactly ONE card does not: it reads as something that
   fell off, which is what "Photos, forms & medical history" and "Financial &
   revenue reports" were doing. These rules catch only that case — the last
   item landing at the start of a fresh row — and let it span the full width,
   so it closes the block deliberately instead of dangling.

   :nth-child() counts from the start, so the position that begins a row is
   1, 3, 5… in two columns, 3n+1 in three, 4n+1 in four.

   `of :not([hidden])` is doing real work, not tidiness. Several grids carry
   placeholder cards that ship hidden until there is content for them — the
   Facebook card in contact.html's "Ways to reach us" is one. Plain
   :nth-child() counts those, so that grid's last card sat at DOM position 4
   (even) while being the 3rd card actually on screen, the rule missed it, and
   the card stayed stranded. Counting only what renders makes the arithmetic
   match what a visitor sees. Browsers without `of S` support simply skip these
   rules and get today's short last row — no worse, just not balanced. */
.grid-2 > :nth-last-child(1 of :not([hidden])):nth-child(odd of :not([hidden])),
.grid-3 > :nth-last-child(1 of :not([hidden])):nth-child(odd of :not([hidden])),
.grid-4 > :nth-last-child(1 of :not([hidden])):nth-child(odd of :not([hidden])) { grid-column: 1 / -1; }
@media (min-width: 900px) {
  .grid-4 > :nth-last-child(1 of :not([hidden])):nth-child(odd of :not([hidden])) { grid-column: auto; }
  .grid-4 > :nth-last-child(1 of :not([hidden])):nth-child(4n + 1 of :not([hidden])) { grid-column: 1 / -1; }
}
@media (min-width: 980px) {
  .grid-3 > :nth-last-child(1 of :not([hidden])):nth-child(odd of :not([hidden])) { grid-column: auto; }
  .grid-3 > :nth-last-child(1 of :not([hidden])):nth-child(3n + 1 of :not([hidden])) { grid-column: 1 / -1; }
}
/* Single-column layouts have no short row to balance — a lone item in a
   one-column grid is simply the whole grid. */
@media (max-width: 559.98px) { .grid-4 > :nth-last-child(1 of :not([hidden])):nth-child(odd of :not([hidden])) { grid-column: auto; } }
@media (max-width: 619.98px) { .grid-3 > :nth-last-child(1 of :not([hidden])):nth-child(odd of :not([hidden])) { grid-column: auto; } }
@media (max-width: 719.98px) { .grid-2 > :nth-last-child(1 of :not([hidden])):nth-child(odd of :not([hidden])) { grid-column: auto; } }

/* Section heading block */
.sec-head { max-width: 64ch; margin-bottom: clamp(2rem, 4vw, 3.25rem); }
.eyebrow {
  display: inline-flex; align-items: center; gap: .5rem;
  font-size: var(--fs-xs); font-weight: 600; letter-spacing: .14em; text-transform: uppercase;
  color: var(--secondary); margin-bottom: .85rem;
}
.eyebrow::before { content: ""; width: 22px; height: 2px; background: currentColor; border-radius: 2px; }

/* ==========================================================================
   Centring contract — one source of truth
   text-align:center does three-quarters of the job and then stops. It does not
   move flex or grid children (a .btn-row keeps its default flex-start), and it
   does not centre a block that carries its own max-width (.lead, .sec-head
   still hug the left edge of their column). So every centred region on the
   site needed the same three corrections, and they were being added by hand,
   one region at a time: .center got them, .page-hero got them later, and
   .hero-inner and .cta-band never did — which is why the homepage hero and the
   CTA band at the foot of five pages shipped with a centred headline sitting
   over a left-hugging button row. Six more places had been patched with inline
   style="justify-content:center" attributes.

   The three rules below carry the whole contract, and each one names the same
   region list, so a centred headline can no longer end up with an off-centre
   button under it. TO CENTRE A NEW REGION, ADD ITS SELECTOR TO ALL THREE
   :where() LISTS — nothing else, and nothing inline.

   :where() keeps the region half of each selector at zero specificity, so a
   component rule still overrides the contract without needing !important, and
   the flat form (rather than CSS nesting) keeps this working in browsers
   older than the 2023 nesting rollout. */
/* Flex rows: the actual bug. Buttons, reassurance ticks and the email capture
   are flex containers, so they need an explicit main-axis centre. */
:where(.center, .hero-inner, .page-hero, .cta-band, .offer-head, .offer)
  :where(.btn-row, .reassure, .capture),
.btn-row.center,
.reassure.center,
.capture.center { justify-content: center; }

/* Constrained blocks: a max-width box needs auto side margins, or it centres
   its text inside a box that is itself still pinned to the left. */
:where(.center, .hero-inner, .page-hero, .cta-band, .offer-head, .offer)
  :where(.lead, .sec-head),
.lead.center,
.sec-head.center,
.center { margin-inline: auto; }
/* `.center` centres itself too: auto side margins are a no-op on a block that
   already fills its parent, so this only bites on the ones that carry their
   own max-width — which is exactly the case that was being fixed inline. */

/* The eyebrow's leading dash is a left-edge marker — it anchors a heading that
   starts at the left. Centred, it hangs off the front of the first word and
   reads as a stray hyphen. */
:where(.center, .hero-inner, .page-hero, .cta-band, .offer-head, .offer) .eyebrow::before,
.eyebrow.center::before { display: none; }

/* ---------- Buttons ---------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: .55rem;
  padding: .85rem 1.5rem; border-radius: var(--r-pill);
  font-weight: 600; font-size: .97rem; line-height: 1;
  border: 1px solid transparent; cursor: pointer;
  text-decoration: none; white-space: nowrap;
  transition: transform .18s var(--ease), box-shadow .18s var(--ease), background-color .18s var(--ease), border-color .18s var(--ease);
}
/* Hover is gated behind a real pointer. A touch tap fires :hover and then LEAVES
   it applied until the user taps elsewhere, so on a phone every button you press
   stays lifted behind you — the lift reads as "still selected" when it is just
   stale. Desktop keeps the affordance; touch gets the press state below instead. */
@media (hover: hover) and (pointer: fine) {
  .btn:hover { text-decoration: none; transform: translateY(-2px); }
}
/* Press feedback. This used to be translateY(0), which only CANCELLED the hover
   lift — so a press with no hover (every tap on a phone, and any keyboard-driven
   activation) produced no visible response at all. scale is the acknowledgement:
   it fires wherever the press came from, and it reads as the surface giving way
   under the finger rather than as the button moving somewhere. */
.btn:active { transform: scale(.97); }
.btn svg { width: 18px; height: 18px; flex: none; }
.btn-primary {
  /* Filled with the 600/700 end of the ramp: white on #0078D4 is 4.10 and
     this label is 16px. The glow underneath stays the true brand blue. */
  background: linear-gradient(135deg, var(--primary-600), var(--primary-700));
  color: #fff;
  /* On the dark theme this was a glow and .75 read as light coming off the
     button. On paper the same value reads as a coloured halo, so it drops to a
     shadow the ground can actually take. */
  box-shadow: 0 8px 20px -8px rgb(0 78 140 / .38);
}
.btn-primary:hover { box-shadow: 0 16px 36px -10px rgb(0 120 212 / .9); }
.btn-ghost { background: rgb(15 23 42 / .04); color: var(--text); border-color: var(--border); }
.btn-ghost:hover { background: rgb(15 23 42 / .08); border-color: var(--primary); }
.btn-gold { background: linear-gradient(135deg, #E6C24F, var(--gold)); color: #1A1405; box-shadow: 0 10px 26px -10px rgba(212,175,55,.6); }
.btn-lg { padding: 1.05rem 2rem; font-size: 1.05rem; }
.btn-sm { padding: .6rem 1.1rem; font-size: .85rem; }
.btn-block { width: 100%; }
.btn-row { display: flex; flex-wrap: wrap; gap: .85rem; }

/* Reassurance line under CTAs — the highest-leverage microcopy on the page */
.reassure {
  display: flex; flex-wrap: wrap; gap: .4rem 1.25rem;
  font-size: var(--fs-sm); color: var(--muted); margin-top: 1.15rem;
}
.reassure li { display: inline-flex; align-items: center; gap: .4rem; }
.reassure svg { width: 15px; height: 15px; color: var(--success); flex: none; }

/* A section heading that is also a link to its own page. Inherits the heading
   colour so the page still reads as a page rather than a list of links, and
   only declares itself on hover and focus. */
h2 > a, h3 > a { color: inherit; text-decoration: none; }
h2 > a:hover, h3 > a:hover { color: var(--primary-600); text-decoration: underline; text-underline-offset: 3px; }
h2 > a:focus-visible, h3 > a:focus-visible { outline: 2px solid var(--primary-600); outline-offset: 3px; border-radius: 2px; }

/* ---------- Spec list ----------------------------------------------------
   The feature-page template's one new component. .reassure is a horizontal
   wrap for short reassurance chips under a CTA; this is its vertical sibling,
   for the 4-6 line specification under each H2 on a product page. Separate
   class rather than a modifier because the two share no layout at all. */
.speclist { display: grid; gap: .7rem; margin: 1.1rem 0 2.25rem; max-width: 62ch; }
.speclist li { display: flex; align-items: flex-start; gap: .7rem; color: var(--muted); }
.speclist li strong { color: var(--text); font-weight: 600; }
.speclist svg { width: 18px; height: 18px; flex: none; margin-top: .22rem; color: var(--success); }

/* ---------- Badges / pills ---------- */
.pill {
  display: inline-flex; align-items: center; gap: .5rem;
  padding: .4rem .85rem; border-radius: var(--r-pill);
  background: rgb(0 120 212 / .12); border: 1px solid rgb(0 120 212 / .3);
  color: var(--secondary); font-size: var(--fs-sm); font-weight: 500;
}
.pill--gold { background: rgb(212 175 55 / .12); border-color: rgb(212 175 55 / .35); color: var(--gold-ink); }
/* On --ground-mark this pill was the same tint twice: its background IS the
   recipe the ground is made from, so it composited half a step DARKER than its
   own panel and read as the weakest thing in the offer. Two rules at once —
   no stacking, and gold-on-gold, which docs/21 reserves for the offer total.
   Inside the offer it takes G2, which is exactly what the stack chips below it
   already do; the gold edge keeps the signal and --gold-ink on white is 7.32. */
.offer .pill--gold { background: var(--card); border-color: rgb(212 175 55 / .45); }
.dot { width: 7px; height: 7px; border-radius: 50%; background: var(--success); flex: none; }
.dot--pulse { animation: pulse 2.4s var(--ease) infinite; }
@keyframes pulse {
  0%,100% { box-shadow: 0 0 0 0 rgb(11 96 57 / .35); }
  50%     { box-shadow: 0 0 0 6px rgb(11 96 57 / 0); }
}

/* ---------- Cards ---------- */
.card {
  background: var(--card);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-lg);
  padding: clamp(1.35rem, 2.4vw, 1.9rem);
  transition: transform .25s var(--ease), border-color .25s var(--ease), background-color .25s var(--ease), box-shadow .25s var(--ease);
}
/* Pointer-gated: several of these cards are links, and on touch the lift would
   persist after the tap — reading as a selected state that nothing set. */
@media (hover: hover) and (pointer: fine) {
  /* Lift plus a 1.02 zoom — CC Digital's "zoom on hover", which the reference
     set rates and which costs nothing extra: transform is already the animated
     property, so this is a different value on a transition that exists rather
     than a new one. Kept at 1.02: enough to feel the card respond, small enough
     that text inside does not visibly resample. */
  .card--hover:hover {
    transform: translateY(-4px) scale(1.02);
    border-color: rgb(0 120 212 / .5);
    background: var(--card-hover);
    box-shadow: var(--shadow-md);
  }
  /* The icon leads the card by a hair on hover — the same detail as the
     entrance, so hover and arrival share one personality instead of reading as
     two separate effects. Cancels the entrance's .04s delay: 40ms is right for
     "settles after the card", wrong for "responds to my cursor". */
  .card--hover:hover .icon-box { transform: scale(1.06); transition-delay: 0s; }
}
/* Cards that are links still need to acknowledge a press, on any input. */
a.card--hover:active { transform: scale(.99); }
.card h3 { margin-bottom: .5rem; }
.card p:last-child { margin-bottom: 0; }

/* Cards in a grid row stretch to the tallest card in that row. When one card
   carries a product screenshot and its neighbour is three lines of text, the
   short one ends up with the difference as dead space between its last line
   and its bottom edge — 379px of it in the Finance section on features.html.
   Making the card a column and letting the visual take the leftover space
   pins the picture to the bottom edge, so the card reads as a deliberate
   composition (text top, image bottom) instead of text floating in a box.
   Text-only cards keep their slack, but two columns rather than three already
   cut most of it. */
.card { display: flex; flex-direction: column; }
.card > .bento-visual { margin-top: auto; }

.icon-box {
  width: 46px; height: 46px; border-radius: var(--r-md);
  display: grid; place-items: center; margin-bottom: 1.1rem;
  background: rgb(0 120 212 / .10);
  border: 1px solid rgb(0 120 212 / .28);
  color: var(--secondary);
}
.icon-box svg { width: 22px; height: 22px; }
.icon-box--gold { background: rgb(212 175 55 / .12); border-color: rgba(212,175,55,.3); color: var(--gold-ink); }

/* ---- Icon lift ----------------------------------------------------------
   The one addition taken from the reference set: Self Space's "gentle
   animations of the icons". The icon settles a fraction after its card, which
   is what makes a panel read as ARRIVING rather than fading in — the card is
   the movement, the icon is the detail that says the movement finished.

   .94, not 0 — nothing in the real world appears from nothing, and a
   scale(0) entry is the single most common tell of a template animation.
   Rides the card's own reveal, so it costs one property on 20 elements and
   adds no observer, no delay chain and no bytes of JS.

   `transform` only: the box has a gradient background and a border, and
   animating either would repaint. Scale composites.

   ONE transition, declared on .icon-box itself, so the entrance and the hover
   cannot disagree. Declaring a second one under .card--hover would have won on
   specificity and silently retimed the entrance.

   The start state is scoped to :not(.is-in) rather than undone by .is-in. A
   `transform: none` on the revealed state outranks .card--hover:hover — 0,4,0
   against 0,3,0 — and silently kills the hover scale, which is exactly what it
   did until this was corrected. Scoping the START means the reveal system
   declares no transform at all once finished, so hover wins by default rather
   than by fighting. */
.icon-box { transition: transform .38s var(--ease); }
.js [data-reveal]:not(.is-in) .icon-box { transform: scale(.86); transition-delay: .09s; }

/* ==========================================================================
   Pre-launch announcement bar
   Scrolls away rather than sticking — it sets context on arrival, then gets
   out of the way. Removed at launch by `node switch-cta.js signup`.
   ========================================================================== */
.announce {
  background: var(--ground-wash);
  border-bottom: 1px solid var(--border-soft);
  padding: .62rem var(--gutter);
  text-align: center;
  font-size: var(--fs-sm);
  color: var(--text);
}
.announce span.dot { display: inline-block; margin-right: .45rem; vertical-align: middle; }
/* The bar is a 20% blue wash on paper, so it is the darkest ground any link
   sits on. --primary-600 measured 4.18 here; the 700 step clears at 5.7. */
.announce a { color: var(--primary-700); font-weight: 600; white-space: nowrap; text-decoration: underline; text-underline-offset: 2px; }
.announce[hidden] { display: none; }

/* ==========================================================================
   Header
   ========================================================================== */
.site-header {
  position: sticky; top: 0; z-index: 100;
  height: var(--header-h);
  display: flex; align-items: center;
  background: rgb(245 240 234 / .82);
  backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
  border-bottom: 1px solid transparent;
  transition: border-color .25s var(--ease), background-color .25s var(--ease);
}
.site-header.is-stuck { border-bottom-color: var(--border-soft); background: rgb(245 240 234 / .96); }
.site-header .container { display: flex; align-items: center; gap: 1.5rem; }

.brand { display: flex; align-items: center; gap: .65rem; margin-right: auto; text-decoration: none; }
.brand:hover { text-decoration: none; }
.brand img { width: 30px; }

/* ---- Logo draw-on ---------------------------------------------------------
   The shield outline draws itself in cyan, the filled mark fades in behind it,
   the outline retires. ~1s total.

   ONCE PER SESSION, and that constraint is the whole design. The header logo
   appears on every page of a twelve-page site — animating it on every load puts
   it in the "tens of times a day" bucket, which is where an effect stops being
   a flourish and becomes a stutter between you and the page you asked for. The
   inline <head> snippet sets `logo-seen` from sessionStorage before first paint,
   so it plays on arrival and never again that visit.

   pathLength="1" normalises the path to a unit length, so the dash maths needs
   no measuring and survives any future edit to the shield geometry.

   Only the HEADER logo is inlined. The footer stays an <img> — two inline
   copies would mean duplicate gradient and clipPath ids on one document. */
.brand-logo { display: block; flex: none; width: 30px; height: 34px; }
.brand-logo__draw { opacity: 0; }

.js:not(.logo-seen) .brand-logo__body {
  transform-box: fill-box;
  transform-origin: center;
  animation: dc-logo-body .5s var(--ease) .5s both;
}
.js:not(.logo-seen) .brand-logo__draw {
  stroke-dasharray: 1;
  animation: dc-logo-draw .72s var(--ease) both,
             dc-logo-retire .28s linear .66s both;
}

@keyframes dc-logo-draw  { from { stroke-dashoffset: 1; opacity: 1 } to { stroke-dashoffset: 0; opacity: 1 } }
@keyframes dc-logo-retire{ to   { opacity: 0 } }
@keyframes dc-logo-body  { from { opacity: 0; transform: scale(.9) } to { opacity: 1; transform: none } }
.brand-text { display: flex; flex-direction: column; line-height: 1.08; }
.brand-name { font-weight: 700; font-size: 1.08rem; letter-spacing: -.02em; color: var(--text); }
/* --primary as text is 4.00 on paper and the brief forbids it outright.
   --primary-600 is the text-safe blue (5.42). */
.brand-name span { color: var(--primary-600); }
/* .75rem = 12px, the floor below which Lighthouse flags text as too small to
   read and below which this genuinely is hard to read on a phone. */
.brand-tag { font-size: .75rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }

.nav { display: flex; align-items: center; gap: .35rem; }
.nav a {
  padding: .5rem .8rem; border-radius: var(--r-sm);
  color: var(--muted); font-size: .93rem; font-weight: 500; text-decoration: none;
  transition: color .18s var(--ease), background-color .18s var(--ease);
}
.nav a:hover { color: var(--text); background: rgb(15 23 42 / .05); text-decoration: none; }
.nav a[aria-current="page"] { color: var(--text); }
.header-cta { display: flex; align-items: center; gap: .6rem; }

.nav-toggle {
  display: none; width: 42px; height: 42px; border-radius: var(--r-sm);
  background: rgb(15 23 42 / .05); border: 1px solid var(--border-soft);
  color: var(--text); cursor: pointer; padding: 0; place-items: center;
}
.nav-toggle svg { width: 20px; height: 20px; }

@media (max-width: 940px) {
  .nav-toggle { display: grid; }
  .nav, .header-cta { display: none; }

  /* FULL-SCREEN MENU.
     The previous version grew the header in normal flow and wrapped the panels
     onto their own rows, on the reasoning that a hard-coded offset would drift.
     It drifted anyway, and worse: .brand, .nav-toggle, .nav and .header-cta are
     four flex siblings of .container, so at narrow widths they wrapped into a
     ragged column with the toggle stranded underneath the CTA button, over a
     page that kept scrolling behind the panel.

     Covering the viewport removes the class of bug rather than the instance —
     there is no wrap left to get wrong — and it is what a phone menu should do
     anyway: one screen, one decision, everything thumb-reachable. */
  .site-header.is-open {
    position: fixed; inset: 0;
    /* dvh, not vh: with vh the bottom of the panel sits under the browser's own
       chrome on iOS Safari, which is exactly where the CTA must not be. */
    height: 100dvh;
    background: var(--bg);
    /* The translucent blur is right for a 72px bar floating over content. Behind
       a full screen of menu it only makes the links harder to read. */
    backdrop-filter: none; -webkit-backdrop-filter: none;
    border-bottom: 0;
    overflow-y: auto;
  }
  .site-header.is-open .container {
    display: flex; flex-direction: column; align-items: stretch;
    min-height: 100dvh;
    padding-block: 1rem 1.5rem;
  }
  /* Pinned rather than given a row of its own: the close control has to stay
     under the thumb at the top, and the markup has no wrapper to hang a row on. */
  .site-header.is-open .nav-toggle { position: absolute; top: 1rem; right: var(--gutter); }
  .site-header.is-open .brand { align-self: flex-start; min-height: 42px; }

  .site-header.is-open .nav,
  .site-header.is-open .header-cta { display: flex; }

  .site-header.is-open .nav {
    flex: 1 1 auto;
    flex-direction: column; align-items: stretch; justify-content: center;
    gap: .15rem;
    margin-block: 1.5rem;
  }
  .site-header.is-open .nav a {
    padding: .85rem .1rem;
    font-size: clamp(1.45rem, 6.5vw, 1.9rem);
    font-weight: 600; letter-spacing: -.02em;
    color: var(--text);
    border-radius: 0;
  }
  /* Rules BETWEEN items, not under each one: a border-bottom on every link
     leaves a hairline under the last with nothing beneath it. Using the
     adjacent-sibling combinator also skips a hidden link correctly — it draws
     no line of its own, and the visible link after it still gets one. */
  .site-header.is-open .nav a + a { border-top: 1px solid var(--border-soft); }
  .site-header.is-open .nav a:hover,
  .site-header.is-open .nav a:focus-visible { background: transparent; color: var(--primary); }
  .site-header.is-open .nav a[aria-current="page"] { color: var(--primary); }
  /* `.nav a { display: inline-flex }` further down outranks the UA's
     `[hidden] { display: none }`, so a pending link would otherwise reappear. */
  .site-header.is-open .nav a[hidden] { display: none; }

  .site-header.is-open .header-cta {
    flex-direction: column; align-items: stretch; gap: .6rem;
    margin-top: auto;
  }
  .site-header.is-open .header-cta .btn { width: 100%; }

  /* ---- Opening ----------------------------------------------------------
     A stagger, not a single fade: the eye follows the list down, which is the
     order the links are meant to be read in. 40ms apart is the smallest step
     that still reads as sequence rather than as one block arriving late.

     The delay itself is set in JS, off the VISIBLE links: a link hidden behind
     data-pending still consumes an nth-of-type slot, which left an 80ms hole
     where every other step is 40ms. --menu-step is the fallback if the script
     never assigns one. */
  .site-header.is-open .nav a { animation: menu-in .28s var(--ease) var(--menu-step, .04s) both; }
  .site-header.is-open .header-cta { animation: menu-in .28s var(--ease) var(--menu-cta, .26s) both; }

  /* ---- Closing ----------------------------------------------------------
     Faster than the open, and as one group. Opening is an invitation and can
     take its time; closing is an answer already given, and a staggered exit
     just keeps the visitor waiting for a menu they have finished with. */
  .site-header.is-open.is-closing { pointer-events: none; }
  .site-header.is-open.is-closing .nav a { animation: none; }
  .site-header.is-open.is-closing .nav,
  .site-header.is-open.is-closing .header-cta { animation: menu-out .15s var(--ease) both; }

  /* Hold the page still underneath. Without this the body scrolls behind the
     menu and closing it returns you somewhere you never chose to be. */
  html:has(.site-header.is-open) { overflow: hidden; }
}

/* ==========================================================================
   Hero
   ========================================================================== */
.hero { position: relative; padding-block: clamp(3.5rem, 8vw, 6.5rem) clamp(3rem, 6vw, 5rem); overflow: hidden; }
.hero-inner { position: relative; z-index: 1; text-align: center; }
.hero h1 { font-size: var(--fs-display); max-width: 18ch; margin-inline: auto; }
.hero h1 .grad {
  background: linear-gradient(100deg, var(--primary) 10%, var(--secondary) 90%);
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
.hero .lead { margin-inline: auto; margin-top: 1.25rem; max-width: 58ch; }

/* Ambient glow — pure CSS, no images, costs nothing to load */
.glow { position: absolute; border-radius: 50%; filter: blur(90px); pointer-events: none; z-index: 0; }
.glow--a { width: 620px; height: 620px; top: -280px; left: 50%; transform: translateX(-50%); background: rgb(0 120 212 / .14); }
.glow--b { width: 420px; height: 420px; top: 120px; right: -160px; background: rgb(80 230 255 / .20); }
.glow--c { width: 380px; height: 380px; top: 260px; left: -160px; background: rgb(212 175 55 / .14); }

/* Faint grid, masked so it fades before it fights the content */
.hero::before {
  content: ""; position: absolute; inset: 0; z-index: 0;
  background-image: linear-gradient(rgb(15 23 42 / .07) 1px, transparent 1px),
                    linear-gradient(90deg, rgb(15 23 42 / .07) 1px, transparent 1px);
  background-size: 64px 64px;
  -webkit-mask-image: radial-gradient(ellipse 80% 55% at 50% 30%, #000 20%, transparent 75%);
          mask-image: radial-gradient(ellipse 80% 55% at 50% 30%, #000 20%, transparent 75%);
}

/* Product shot */
.hero-shot { position: relative; margin-top: clamp(2.5rem, 5vw, 4rem); z-index: 1; }
.shot-frame {
  position: relative; border-radius: var(--r-xl);
  border: 1px solid var(--border-soft); background: var(--surface);
  padding: 8px; box-shadow: var(--shadow-lg);
  overflow: hidden;
}
.shot-frame img { border-radius: calc(var(--r-xl) - 10px); width: 100%; }
/* The hero master is already cropped to the dashboard, so no CSS cropping is needed. */
.shot-frame .shot-img { display: block; width: 100%; height: auto; }
/* The tour is a <video>; it needs the same treatment the shots get, and its own
   radius, because a video element does not inherit the frame's clipping. */
.shot-frame video { display: block; width: 100%; height: auto; border-radius: calc(var(--r-xl) - 10px); background: var(--surface); }
/* For a capture that already contains a laptop/phone body. */
.shot-frame--bare { border: 0; background: none; padding: 0; box-shadow: none; overflow: visible; }
.shot-frame--bare::after { display: none; }
.shot-frame--bare img, .shot-frame--bare picture img { border-radius: 0; }
/* The bottom fade existed to blend a DARK capture into the dark page. The captures are
   now light, and the same gradient lays a grey wash over white cards. The frame's own
   border, radius and shadow already contain the shot. */
.shot-frame::after { content: none; }
/* Floating stat chips over the product shot */
.chip {
  position: absolute; z-index: 2;
  display: flex; align-items: center; gap: .6rem;
  padding: .65rem .95rem; border-radius: var(--r-md);
  background: rgba(24,43,69,.94); border: 1px solid var(--border);
  backdrop-filter: blur(10px); box-shadow: var(--shadow-md);
  font-size: var(--fs-sm); font-weight: 600;
  animation: float 6s ease-in-out infinite;
}
/* A chip is a dark plate sitting on the product shot, so its text is inverse
   ink. Without this it inherited --text and rendered dark on dark (1.25). */
.chip { color: var(--ink-inv); }
.chip small { display: block; font-size: var(--fs-xs); font-weight: 400; color: rgb(245 240 234 / .72); }
.chip--tl { top: 8%; left: -14px; }
.chip--br { bottom: 24%; right: -14px; animation-delay: -3s; }
@keyframes float { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-10px); } }
@media (max-width: 720px) { .chip { display: none; } }

/* ---------- Logo / trust strip ---------- */
.trust-strip { border-block: 1px solid var(--border-soft); padding-block: 1.75rem; }
.trust-strip p { text-align: center; font-size: var(--fs-sm); color: var(--muted); margin-bottom: 1.1rem; letter-spacing: .04em; }
.marquee { overflow: hidden; -webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent); mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent); }
.marquee-track { display: flex; gap: 3.5rem; width: max-content; animation: scroll-x 34s linear infinite; }
.marquee:hover .marquee-track { animation-play-state: paused; }
@keyframes scroll-x { to { transform: translateX(-50%); } }
.marquee-item { color: var(--muted); font-weight: 600; font-size: 1rem; opacity: .6; white-space: nowrap; }

/* ---------- Stat row ---------- */
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 1px; background: var(--border-soft); border: 1px solid var(--border-soft); border-radius: var(--r-lg); overflow: hidden; }
.stat { background: var(--surface); padding: 1.6rem 1.25rem; text-align: center; }
.stat-num { font-size: clamp(1.9rem, 1.3rem + 2vw, 2.6rem); font-weight: 700; letter-spacing: -.03em; color: var(--text); line-height: 1.05; font-variant-numeric: tabular-nums; }
.stat-num .unit { color: var(--secondary); }
.stat-label { font-size: var(--fs-sm); color: var(--muted); margin-top: .35rem; }

/* ==========================================================================
   Before / after — the pain-to-relief section
   ========================================================================== */
.ba { display: grid; gap: 1.25rem; grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr)); align-items: start; }
.ba-col { border-radius: var(--r-lg); padding: clamp(1.35rem, 2.4vw, 1.9rem); border: 1px solid var(--border-soft); }
.ba-col--before { background: rgb(181 48 26 / .05); border-color: rgb(181 48 26 / .22); }
.ba-col--after  { background: rgb(11 96 57 / .05); border-color: rgb(11 96 57 / .25); }
.ba-col h3 { display: flex; align-items: center; gap: .6rem; font-size: 1.15rem; }
/* An unsized inline SVG lays out at 300x150, so these two are not optional. */
.ba-col h3 svg { width: 21px; height: 21px; flex: none; }
.ba-col--before h3 svg { color: var(--danger); }
.ba-col--after  h3 svg { color: var(--success); }
.ba-col ul { margin-top: 1rem; display: grid; gap: .85rem; }
.ba-col li { display: flex; gap: .7rem; align-items: flex-start; font-size: .95rem; color: var(--muted); }
.ba-col li svg { width: 18px; height: 18px; flex: none; margin-top: .18rem; }
.ba-col--before li svg { color: var(--danger); }
.ba-col--after  li svg { color: var(--success); }
.ba-col--after li { color: var(--text); }

/* ==========================================================================
   Bento feature grid
   ========================================================================== */
.bento { display: grid; gap: clamp(1rem, 2vw, 1.4rem); grid-template-columns: repeat(6, 1fr); }
.bento > * { grid-column: span 6; }
@media (min-width: 760px) {
  .bento > * { grid-column: span 3; }
  .bento .span-2 { grid-column: span 2; }
  .bento .span-4 { grid-column: span 4; }
  .bento .span-6 { grid-column: span 6; }
}
.bento-visual { margin-top: 1.35rem; border-radius: var(--r-md); overflow: hidden; border: 1px solid var(--border-soft); background: var(--bg); }

/* The mini mock components that lived here — .mock-row, .tag, .teeth/.tooth
   and .bars/.bar — were CSS-drawn feature illustrations from before there were
   screenshots to show. See "Real product screenshots inside bento cards" below:
   the markup was replaced by actual captures and these styles were left behind,
   unused on all 12 pages. Removed 2026-09-22, along with the last Tailwind hue
   on the site (#FBBF24 in .tag--warn). Do not re-add: a drawn mock of a product
   that exists is a claim you then have to keep true in two places. */

/* ==========================================================================
   Pricing
   ========================================================================== */
.billing-toggle {
  display: inline-flex; align-items: center; gap: .35rem; padding: .35rem;
  background: var(--surface); border: 1px solid var(--border-soft); border-radius: var(--r-pill);
}
.billing-toggle button {
  border: 0; background: transparent; color: var(--muted); cursor: pointer;
  padding: .55rem 1.15rem; border-radius: var(--r-pill); font: inherit; font-size: .9rem; font-weight: 600;
  transition: background-color .2s var(--ease), color .2s var(--ease);
  display: inline-flex; align-items: center; gap: .5rem;
}
.billing-toggle button.is-active { background: var(--primary-600); color: #fff; }
.save-badge { font-size: var(--fs-xs); font-weight: 700; padding: .1rem .45rem; border-radius: var(--r-pill); background: rgba(212,175,55,.2); color: var(--gold-ink); }
.billing-toggle button.is-active .save-badge { background: rgb(8 19 31 / .28); color: #fff; }
/* The calculator uses the same control driven by aria-pressed rather than a
   class, so its selected option was never styled — chosen looked identical to
   unchosen. Same treatment, keyed off the ARIA state that was already there and
   is what a screen reader announces. */
.billing-toggle button[aria-pressed="true"] { background: var(--primary-600); color: #fff; }
.billing-toggle button[aria-pressed="true"] .save-badge { background: rgb(8 19 31 / .28); color: #fff; }

/* Four plans, so the column count steps 1 → 2 → 4 and never through 3: at
   three columns the fourth plan drops onto its own row, and a pricing table
   where one tier sits alone underneath reads as an afterthought rather than a
   peer of the other three. Two columns gives a balanced 2 x 2 in between. */
.plans { display: grid; gap: 1.15rem; grid-template-columns: 1fr; align-items: start; margin-top: clamp(2rem, 4vw, 3rem); }
@media (min-width: 620px)  { .plans { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1060px) { .plans { grid-template-columns: repeat(3, 1fr); } }
.plan {
  position: relative; display: flex; flex-direction: column;
  background: var(--card); border: 1px solid var(--border-soft); border-radius: var(--r-lg);
  padding: 1.6rem 1.4rem;
  transition: transform .25s var(--ease), border-color .25s var(--ease), box-shadow .25s var(--ease);
}
/* Same reason as .btn:hover — a tap leaves the lift applied, so on a phone the
   last plan card you touched stays raised as though it were selected. */
@media (hover: hover) and (pointer: fine) {
  .plan:hover { transform: translateY(-4px); border-color: rgb(0 120 212 / .45); box-shadow: var(--shadow-md); }
}
.plan--featured {
  border-color: var(--primary); background: linear-gradient(180deg, rgb(0 120 212 / .1), var(--card) 45%);
  box-shadow: var(--shadow-glow);
}
@media (min-width: 1060px) {
  .plan--featured { transform: scale(1.035); }
  .plan--featured:hover { transform: scale(1.035) translateY(-4px); }
}
.plan-ribbon {
  position: absolute; top: -12px; left: 50%; transform: translateX(-50%);
  background: linear-gradient(135deg, #E6C24F, var(--gold)); color: #1A1405;
  font-size: var(--fs-xs); font-weight: 800; letter-spacing: .07em; text-transform: uppercase;
  padding: .3rem .8rem; border-radius: var(--r-pill); white-space: nowrap;
  box-shadow: 0 6px 18px -6px rgba(212,175,55,.7);
}
.plan-name { font-size: 1.1rem; font-weight: 700; margin-bottom: .2rem; }
/* min-height sized to the longest description (3 lines) so the price and CTA
   land on the same baseline across all four cards. */
.plan-for { font-size: var(--fs-sm); color: var(--muted); min-height: 5em; margin-bottom: 1.1rem; }
.plan-price { display: flex; align-items: baseline; gap: .3rem; flex-wrap: wrap; }
.plan-price .amt { font-size: 2.4rem; font-weight: 700; letter-spacing: -.035em; font-variant-numeric: tabular-nums; }
.plan-price .per { font-size: var(--fs-sm); color: var(--muted); }
.plan-note { font-size: var(--fs-xs); color: var(--muted); min-height: 2.6em; margin-top: .35rem; }
.plan .btn { margin-block: 1.25rem 1.35rem; }
.plan-feats { display: grid; gap: .7rem; font-size: .9rem; border-top: 1px solid var(--border-soft); padding-top: 1.3rem; }
.plan-feats li { display: flex; gap: .6rem; align-items: flex-start; color: var(--muted); }
.plan-feats li svg { width: 16px; height: 16px; flex: none; margin-top: .22rem; color: var(--primary); }
.plan-feats li strong { color: var(--text); font-weight: 600; }
.plan-feats .head { color: var(--text); font-weight: 600; font-size: var(--fs-sm); display: block; }

/* Comparison table */
.table-wrap { overflow-x: auto; border: 1px solid var(--border-soft); border-radius: var(--r-lg); background: var(--surface); }
table.compare { width: 100%; border-collapse: collapse; min-width: 760px; font-size: .92rem; }
table.compare th, table.compare td { padding: .9rem 1rem; text-align: center; border-bottom: 1px solid var(--border-soft); }
table.compare thead th { background: var(--card); font-size: var(--fs-sm); }
table.compare th:first-child, table.compare td:first-child { text-align: left; color: var(--muted); font-weight: 500; min-width: 240px; }
table.compare tbody tr:hover { background: rgb(0 120 212 / .05); }
table.compare tbody tr:last-child td { border-bottom: 0; }
table.compare .is-featured { background: rgb(0 120 212 / .07); }
table.compare .row-group td { background: var(--card); font-weight: 700; color: var(--text); text-align: left; font-size: var(--fs-xs); letter-spacing: .1em; text-transform: uppercase; }
.tick { color: var(--success); }
.cross { color: rgb(15 23 42 / .55); }

/* (Removed 2026-08-31: the .compare--compact rules styled three small tables in
   the add-on cards. That section is now one .addons table and no element on any
   page carries the class. Its overrides — position:static, box-shadow:none,
   background:none — reset a sticky first column that no longer exists here.) */

/* ==========================================================================
   Add-on price table
   ==========================================================================
   One grouped table rather than three cards side by side. Eleven prices split
   across three cards gave each one a third of the width, which is what put a
   scrollbar over the price column — and it also made the reader compare across
   cards to answer the only question they have: what does this cost.

   TWO COLUMNS, ALWAYS. The per-unit rate sits under the price rather than in a
   third column, because a third column is what forces a minimum width, and a
   minimum width is what breaks at 360px. Name left, price right, rate beneath —
   the same shape from a small Android up to desktop, so nothing reflows into a
   different layout the user has to re-learn. `table-layout: fixed` with no
   min-width means the table can never demand more room than it is given. */
/* 560, not 680. At 680 with a 50/50 split the gap between a row name and its
   price reached 524px — the eye had to cross three-quarters of the table to bind
   "+50 GB" to "₱499", which is the one job the row has. Narrower table, weighted
   columns (below), and the pair reads as a pair. */
.addons-wrap {
  max-width: 560px;
  margin-inline: auto;
  border: 1px solid var(--border-soft);
  border-radius: var(--r-lg);
  /* --card, not --surface: the section behind this IS --surface, so the panel
     had no ground of its own and was defined only by a 1px hairline, while the
     group bands (--card) read as raised. Figure and ground were inverted. */
  background: var(--card);
  overflow: hidden;   /* clips the group-header fill to the rounded corners */
}
/* Weighted rather than the 50/50 `fixed` gives by default. The name column
   carries the longest strings ("SMS credits · one-time top-up"); the price
   column never needs more than "₱11.96 / GB" at ~78px. */
table.addons th:first-child,
table.addons td:first-child { width: 60%; }
table.addons th:last-child,
table.addons td:last-child  { width: 40%; }
table.addons {
  width: 100%;
  min-width: 0;
  table-layout: fixed;
  border-collapse: collapse;
}
table.addons thead th {
  font-size: var(--fs-xs);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--muted);
  font-weight: 600;
  text-align: left;
  padding: .85rem 1.1rem;
  background: var(--card);
  border-bottom: 1px solid var(--border-soft);
}
/* The "Price" label has to sit over its own figures. Without this the thead rule
   above wins on specificity (0,1,2 vs .addons-num's 0,1,0) and left-aligns a
   header over a right-aligned column — at 768 the label ended 261px from where
   its data ended. */
table.addons thead th.addons-num { text-align: right; }
table.addons tbody th,
table.addons tbody td {
  padding: .85rem 1.1rem;
  border-bottom: 1px solid var(--border-soft);
  vertical-align: baseline;
}
table.addons tbody tr:last-child th,
table.addons tbody tr:last-child td { border-bottom: 0; }

/* Row label. `scope="row"` keeps it a header for screen readers while reading
   as body text — a price list is a lookup, and the name is the lookup key. */
table.addons tbody th[scope="row"] {
  text-align: left;
  font-weight: 500;
  color: var(--text);
}

/* Group headers. Same visual language as the plan comparison's .row-group, so
   the two tables on this page read as one system rather than two components. */
.addons-group th {
  /* text-align is NOT inherited here — a bare <th> defaults to center, and
     without this line the group labels sat 84px right of every row name beneath
     them. The one rule this block was missing. */
  text-align: left;
  background: var(--surface);
  font-size: var(--fs-xs) !important;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--text);
  font-weight: 700 !important;
  padding-top: 1rem !important;
  padding-bottom: .6rem !important;
}

/* tabular-nums so the peso figures align down the column optically. Without it
   the 1s are narrow and the prices read as ragged even when they are flush. */
.addons-num {
  text-align: right;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  line-height: 1.3;
}
.addons-rate,
.addons-note {
  display: block;
  font-size: .78rem;
  font-weight: 400;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  margin-top: .15rem;
}
.addons-note { white-space: normal; }

/* The cheapest unit rate in each group, marked. This is the one comparison the
   table exists to support and the reader would otherwise do with a calculator. */
/* Always its own line, at every width — not inline above 430px.
   Inline, it needed 222px against a column that only reached 222px at 561px
   wide, so across the whole 431–560 band it wrapped anyway AND kept its
   margin-left, landing 8px right of the name's flush edge. Two identical badges
   six rows apart rendered two different ways, and row heights diverged by 8.8px.
   Blocking it everywhere removes the band, the indent and the height mismatch.

   Colour: on the dark theme this was pale blue on a blue wash. Inverted, that
   pairing composited to 1.76:1 on paper. The deep brand blue on a 10% wash of
   itself is 7.3:1. */
.addons-badge {
  display: block;
  width: fit-content;
  margin: .3rem 0 0;
  padding: .14rem .5rem;
  border-radius: var(--r-pill);
  background: rgb(0 120 212 / .10);
  color: var(--primary-700);
  font-size: .7rem;
  font-weight: 600;
  letter-spacing: .02em;
  text-transform: uppercase;
  white-space: nowrap;
}
table.addons tbody tr.is-best th[scope="row"] { font-weight: 600; }

@media (hover: hover) and (pointer: fine) {
  table.addons tbody tr:not(.addons-group):hover { background: rgb(0 120 212 / .05); }
}

/* Small phones — 360px Android up to a 390px iPhone. The layout does not change
   shape, it just tightens: the badge drops to its own line so it can never push
   the price column, and the horizontal padding halves to buy back ~24px for the
   numbers. */
@media (max-width: 430px) {
  table.addons thead th,
  table.addons tbody th,
  table.addons tbody td { padding: .75rem .7rem; }
  .addons-group th { padding-left: .7rem !important; padding-right: .7rem !important; }
  table.addons { font-size: .95rem; }
  /* .78, not .74. The rate is the number that lets a dentist see ₱7.99/GB beats
     ₱11.96/GB — it is the comparison the table exists for, and it was set
     smaller than anything else on the page. */
  .addons-rate, .addons-note { font-size: .78rem; }
}

/* The three explanatory notes. Stacked at the SAME width as the table rather
   than in a grid-3 across the full container: at 1440 the centred 560px table
   and a full-width grid shared no edge (218px apart), and between 600 and 999
   the three-item grid resolved to two columns, orphaning the third with a 337px
   hole beside it. One column under the table has neither problem at any width. */
.addons-notes {
  max-width: 560px;
  margin: 1.5rem auto 0;
  display: grid;
  gap: .9rem;
  align-content: start;
}

/* From 900px the notes move BESIDE the table instead of under it.
   Stacked, a 560px table centred in a 1240px container left ~340px of dead
   space either side, and because prices right-align to the table edge the only
   way to shorten the name-to-price gap is to shorten the table. Putting the
   footnotes in that space lets the table drop to 480 — which pulls the gap in
   by ~80px per row — and gives the section a left edge instead of floating it
   in the middle of nothing. */
@media (min-width: 900px) {
  .addons-layout {
    display: grid;
    grid-template-columns: minmax(0, 480px) minmax(0, 1fr);
    gap: 3rem;
    align-items: start;
    max-width: 980px;
    margin-inline: auto;
  }
  .addons-layout .addons-wrap { max-width: none; margin-inline: 0; }
  .addons-layout .addons-notes { max-width: none; margin: 0; gap: 1.15rem; }
}
.addons-foot { font-size: .88rem; margin-bottom: 0; }

/* ==========================================================================
   Testimonials
   ========================================================================== */
.quote { display: flex; flex-direction: column; gap: 1.1rem; }
.quote blockquote { margin: 0; font-size: 1.02rem; line-height: 1.65; }
.quote-by { display: flex; align-items: center; gap: .8rem; margin-top: auto; }
.avatar { width: 42px; height: 42px; border-radius: 50%; flex: none; display: grid; place-items: center; font-weight: 700; font-size: .95rem; color: #fff; background: linear-gradient(135deg, var(--primary-600), var(--primary-700)); }
.quote-by b { display: block; font-size: .93rem; }
.quote-by span { font-size: var(--fs-xs); color: var(--muted); }
.stars { display: flex; gap: 2px; color: var(--gold-ink); }
.stars svg { width: 15px; height: 15px; }
.placeholder-note {
  border: 1px dashed rgba(212,175,55,.45); background: rgba(212,175,55,.06);
  color: var(--gold-ink); border-radius: var(--r-md); padding: .8rem 1rem;
  font-size: var(--fs-sm); margin-top: 1.5rem;
}

/* ==========================================================================
   Steps — vertical timeline
   Four numbered stops joined by a continuous rail, so the sequence is carried
   by the picture rather than by the reader remembering that four cards in a
   row are meant to be read in order.

   This replaced a four-column layout whose connector was a horizontal rule
   between the cards, switched off below 900px. That meant the one viewport
   where the steps genuinely stacked into a list — a phone — was the viewport
   with nothing left to say they were sequential at all. A vertical rail is the
   same drawing at every width, so the ordering never has to be inferred.

   The rail is built from each step's own ::after rather than one line behind
   the whole list, so it stops exactly at the last marker and needs no magic
   height. That is also why the steps are spaced with padding-bottom instead of
   grid gap: a gap would open a break in the rail at every junction.
   ========================================================================== */
.steps {
  counter-reset: step;
  display: grid;
  gap: 0;
  max-width: 680px;
  margin-inline: auto;
}
.step {
  position: relative;
  padding-left: 4.25rem;
  padding-bottom: clamp(1.75rem, 3vw, 2.5rem);
}
.step:last-child { padding-bottom: 0; }

/* The numbered marker. Circular rather than the rounded square it replaced —
   a circle reads as a point on a line, a square reads as a card. */
.step::before {
  counter-increment: step; content: counter(step);
  position: absolute; top: 0; left: 0; z-index: 1;
  width: 44px; height: 44px; border-radius: 50%;
  display: grid; place-items: center;
  font-weight: 700; font-size: 1.05rem; font-variant-numeric: tabular-nums;
  background: linear-gradient(135deg, rgb(0 120 212 / .28), rgb(80 230 255 / .1));
  border: 1px solid rgb(0 120 212 / .45);
  color: var(--secondary);
}

/* The rail. Starts 6px below the marker and runs to the next one, so there is
   a clean breath either side of every stop. The gap is geometry rather than a
   ring of background colour painted over the line — this block is used on
   .section--surface on the homepage and on the plain background of
   book-a-demo, and a colour-matched ring would be wrong on one of them.
   It fades downward, which reads as forward motion without claiming any of
   the steps are already done. */
.step::after {
  content: ""; position: absolute;
  left: 22px; transform: translateX(-1px);
  top: 50px; bottom: 6px; width: 2px;
  background: linear-gradient(180deg, rgb(0 120 212 / .55), var(--border-soft));
}
.step:last-child::after { display: none; }

/* Optically centres the heading on the 44px marker beside it. */
.step h3 { margin: 0 0 .4rem; padding-top: .55rem; }
.step p:last-child { margin-bottom: 0; }

/* On a narrow phone the 68px gutter is a big share of the width. Tighten the
   marker and the gutter together so the copy keeps a readable measure. */
@media (max-width: 480px) {
  .step { padding-left: 3.4rem; }
  .step::before { width: 36px; height: 36px; font-size: .95rem; }
  .step::after { left: 18px; top: 42px; }
  .step h3 { padding-top: .3rem; }
}

/* ==========================================================================
   FAQ
   ========================================================================== */
.faq { max-width: 800px; margin-inline: auto; border-top: 1px solid var(--border-soft); }
.faq details { border-bottom: 1px solid var(--border-soft); }
.faq summary {
  cursor: pointer; list-style: none; padding: 1.15rem .25rem;
  font-weight: 600; font-size: 1.02rem;
  display: flex; align-items: center; gap: 1rem;
  transition: color .2s var(--ease);
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary:hover { color: var(--secondary); }
.faq summary::after {
  content: ""; margin-left: auto; flex: none;
  width: 10px; height: 10px; border-right: 2px solid var(--muted); border-bottom: 2px solid var(--muted);
  transform: rotate(45deg) translateY(-3px); transition: transform .25s var(--ease);
}
.faq details[open] summary::after { transform: rotate(-135deg) translateY(-3px); }
.faq details[open] summary { color: var(--secondary); }
.faq-body { padding: 0 .25rem 1.3rem; color: var(--muted); font-size: .96rem; }
.faq-body p:last-child { margin-bottom: 0; }

/* ==========================================================================
   CTA band
   ========================================================================== */
.cta-band {
  position: relative; overflow: hidden; border-radius: var(--r-xl);
  padding: clamp(2.5rem, 6vw, 4.5rem) clamp(1.5rem, 4vw, 3.5rem); text-align: center;
  border: 1px solid rgb(0 120 212 / .3);
  background: var(--ground-wash);
}
/* The blurred radial that used to sit here is gone, and with it the only
   unbounded ground on the site. It was also a direct D-004 rules-out — "no
   radial glows" — and the element most likely to look dated: a blurred blue
   halo behind a centred CTA is the most category-generic gesture in health
   SaaS. Its z-index partner went too; nothing in the band is positioned. */
.cta-band h2 { max-width: 20ch; margin-inline: auto; }

/* Inline email capture — one field, the lowest-friction entry point there is */
.capture { display: flex; gap: .6rem; max-width: 480px; margin: 1.6rem auto 0; flex-wrap: wrap; }
.capture input {
  flex: 1 1 240px; min-width: 0;
  padding: .95rem 1.15rem; border-radius: var(--r-pill);
  background: #FFFFFF; border: 1px solid var(--border);
  color: var(--text); font: inherit; font-size: .97rem;
  transition: border-color .2s var(--ease), box-shadow .2s var(--ease);
}
.capture input::placeholder { color: var(--muted); }
.capture input:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgb(0 120 212 / .2); }

/* ==========================================================================
   Forms (contact / demo)
   ========================================================================== */
.form-card { background: var(--card); border: 1px solid var(--border-soft); border-radius: var(--r-lg); padding: clamp(1.5rem, 3vw, 2.25rem); }
.field { margin-bottom: 1.05rem; }
.field label { display: block; font-size: var(--fs-sm); font-weight: 600; margin-bottom: .4rem; }
.field .req { color: var(--primary); }
.field input, .field select, .field textarea {
  width: 100%; padding: .8rem 1rem; border-radius: var(--r-md);
  background: #FFFFFF; border: 1px solid var(--border);
  color: var(--text); font: inherit; font-size: .95rem;
  transition: border-color .2s var(--ease), box-shadow .2s var(--ease);
}
.field textarea { min-height: 130px; resize: vertical; }
/* The outline stays. It used to be `outline: none` with a border recolour plus a
   3px rgb(0 120 212 / .2) halo standing in for it — which works for a text input
   (the recoloured border reaches 4.99:1 against the field) but gives a native
   checkbox nothing at all: it has no border of ours to recolour, so the halo was
   the entire indicator, and composited over the card it reads 1.29:1. Invisible.
   On /book-a-demo.html that is the consent box, which gates submission.
   Keeping a real outline covers every control type with one rule. */
.field input:focus-visible,
.field select:focus-visible,
.field textarea:focus-visible {
  outline: 2px solid var(--secondary);
  outline-offset: 2px;
  border-color: var(--primary);
  box-shadow: 0 0 0 3px rgb(0 120 212 / .2);
}

/* No ::placeholder rule existed for .field, so the UA default rgb(117,117,117)
   applied — 4.06:1 on --bg, a fail. The textarea placeholder carries guidance
   that is not repeated in its label, so it is not decorative text. */
.field input::placeholder,
.field textarea::placeholder { color: var(--muted); opacity: 1; }
.field-row { display: grid; gap: 0 1rem; grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr)); }

/* Consent checkbox. Was six inline style attributes duplicated across the two
   forms; the <label> wraps the input, so the whole sentence is the tap target
   and the box only has to be big enough to see and aim at. */
.field--check label {
  display: flex; align-items: flex-start; gap: .7rem;
  font-weight: 400; font-size: .9rem; line-height: 1.5; cursor: pointer;
}
.field--check input[type="checkbox"] {
  width: 1.25rem; height: 1.25rem; margin-top: .15rem;
  flex: none; accent-color: var(--primary); cursor: pointer;
}
@media (pointer: coarse) {
  .field--check input[type="checkbox"] { width: 1.4rem; height: 1.4rem; }
}
.form-note { font-size: var(--fs-xs); color: var(--muted); margin-top: .9rem; }

/* ==========================================================================
   Footer
   ========================================================================== */
/* No margin-top. Every page ends with a .section, which already carries a full
   --section-y of bottom padding, so the margin was doubling it — 128px of empty
   dark space above the footer on a phone before its own 44px padding even
   started. The footer is separated by a border and its own background; it does
   not need a gap as well. */
.site-footer { border-top: 1px solid var(--border-soft); background: var(--surface); padding-block: clamp(2.75rem, 5vw, 4rem) 2rem; }
/* One exception: a --surface section is the same colour as the footer, so with
   no gap the two merge into a single slab divided by a hairline (book-a-demo).
   A small band of page background restores the boundary.
   Selected through main, because the footer is a sibling of <main> rather than
   of the section — an adjacent-sibling selector here matches nothing. */
main:has(> .section--surface:last-child) + .site-footer {
  margin-top: clamp(1.5rem, 3vw, 2.5rem);
}
.footer-grid { display: grid; gap: 2.25rem; grid-template-columns: repeat(auto-fit, minmax(min(100%, 170px), 1fr)); }
.footer-brand { grid-column: 1 / -1; }
/* From 900px up the footer stops being an equal-width auto-fit row. The brand
   column carries the tagline, the description, the badges and the full
   registered address; the other three carry a list of links each. Splitting
   them evenly squeezed the brand to ~209px on a 1024px laptop, which broke
   "Dental Clinic Management" across two lines and ran the address to ten. */
@media (min-width: 900px) {
  .footer-grid { grid-template-columns: minmax(16rem, 1.3fr) repeat(3, minmax(0, 1fr)); }
  .footer-brand { grid-column: auto; max-width: none; }
}
.footer-grid h4 { font-size: var(--fs-xs); letter-spacing: .1em; text-transform: uppercase; color: var(--muted); margin-bottom: 1rem; }
.footer-grid ul { display: grid; gap: .6rem; }
.footer-grid a { color: var(--text); font-size: .92rem; opacity: .82; text-decoration: none; }
/* The footer is a dark plate, so cyan is correct here — and only here. */
/* --on-plate is CYAN, and D-026 made cyan plate-only. On the light footer it
   composites to about 1.1:1 against --surface, so hovering a footer link made
   it VANISH. A hover state renders in no screenshot and no contrast sweep —
   this survived the whole re-theme because nothing was hovering when we
   measured. */
.footer-grid a:hover,
.footer-solutions a:hover { opacity: 1; color: var(--primary-600); }

/* The ten product pages, their own row rather than a fifteen-item column. */
.footer-solutions { margin-top: 2.5rem; padding-top: 1.75rem; border-top: 1px solid var(--border-soft); }
.footer-solutions h4 { font-size: var(--fs-xs); letter-spacing: .1em; text-transform: uppercase; color: var(--muted); margin-bottom: 1rem; }
.footer-solutions ul { display: flex; flex-wrap: wrap; gap: .5rem 1.5rem; }
.footer-solutions a { color: var(--text); font-size: .92rem; opacity: .82; text-decoration: none; }
.footer-bottom { display: flex; flex-wrap: wrap; gap: 1rem; justify-content: space-between; align-items: center; margin-top: 2.75rem; padding-top: 1.5rem; border-top: 1px solid var(--border-soft); font-size: var(--fs-sm); color: var(--muted); }
.footer-badges { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: 1.1rem; }
.badge { display: inline-flex; align-items: center; gap: .4rem; font-size: var(--fs-xs); padding: .35rem .7rem; border-radius: var(--r-sm); background: rgb(15 23 42 / .04); border: 1px solid var(--border-soft); color: var(--muted); }
.badge svg { width: 13px; height: 13px; color: var(--success); flex: none; }

/* ==========================================================================
   Interior page hero
   ========================================================================== */
.page-hero { position: relative; padding-block: clamp(3rem, 6vw, 4.75rem) clamp(2rem, 4vw, 3rem); text-align: center; overflow: hidden; }
.page-hero .container { position: relative; z-index: 1; }
.page-hero::before {
  content: ""; position: absolute; inset: 0; z-index: 0;
  background-image: linear-gradient(rgb(15 23 42 / .06) 1px, transparent 1px),
                    linear-gradient(90deg, rgb(15 23 42 / .06) 1px, transparent 1px);
  background-size: 64px 64px;
  -webkit-mask-image: radial-gradient(ellipse 70% 70% at 50% 20%, #000 10%, transparent 70%);
          mask-image: radial-gradient(ellipse 70% 70% at 50% 20%, #000 10%, transparent 70%);
}

/* Long-form prose (legal, blog) */
.prose { max-width: 74ch; margin-inline: auto; }
.prose h2 { margin-top: 2.5rem; font-size: 1.5rem; }
.prose h3 { margin-top: 1.75rem; font-size: 1.15rem; }
.prose p, .prose li { color: var(--muted); }
.prose ul { list-style: disc; padding-left: 1.35rem; display: grid; gap: .5rem; margin-bottom: 1rem; }
.prose strong { color: var(--text); }

/* ==========================================================================
   Motion — reveal on scroll. Everything opts out under reduced-motion.
   ========================================================================== */
/* Entrance reveal. Retuned 2026-08-31 against three Elementor sites the trade
   press rates for motion (Self Space, CC Digital, London Youth Games).
   The finding that mattered: the one praised specifically for its transitions
   animates SIX elements, on transform, at 0.2s. The one doing the most motion
   ships Animate.css and weighs 2,497 KB against this site's 159 KB.

   So the mechanism here was already right — IntersectionObserver, one-shot,
   no library — and only the numbers were wrong:

     .7s  -> .46s   700ms reads as lag. Nothing in the references sat above .2s.
     22px -> 14px   long travel amplifies a slow duration; short travel reads
                    as arrival rather than as sliding.
     .40s -> .30s   stagger steps drop from 80ms to 60ms, so the fifth card in
                    a row lands at 760ms instead of 1,100ms. Emil's range is
                    30-80ms between items; the old top delay alone exceeded the
                    whole animation budget.

   Unchanged: --ease. cubic-bezier(.22,.61,.36,1) turns out to be the same curve
   Self Space uses (0.215,0.61,0.355,1), arrived at independently.

   GATED ON .js — and not cosmetic. These rules used to apply unconditionally, so
   with JavaScript disabled every revealable element stayed at opacity 0 forever:
   twenty of them on features.html, including every feature card. The page
   rendered essentially blank. site.js's own header claims the reveal "is
   disabled by the no-js class never being removed" — that mechanism was never
   built; there is no no-js class anywhere in the codebase.

   Hiding is now opt-in: the inline snippet in <head> adds `js` to <html> before
   first paint, so no-JS gets the content and JS gets the animation. Visible is
   the only safe default for content.

   HIDDEN STATE SCOPED TO :not(.is-in), not undone by .is-in. Declaring
   `transform: none` on the revealed state made the reveal system outrank every
   intentional transform underneath it — .card--hover:hover lost its lift AND its
   new 1.02 scale on 28 cards, and .plan--featured lost the scale(1.035) that
   marks the recommended tier. Once an element has arrived, this system now
   declares no transform at all, so anything else is free to win normally. */
/* ---- The single entrance: FADE IN UP + a slight ZOOM ---------------------
   Elementor's catalogue offers fade/zoom/bounce in five directions each. Taking
   one and using it everywhere is the difference between a site with motion and
   a site that looks assembled from a plugin menu.

   CHOSEN — fadeInUp combined with a 0.97 scale. Elementor calls the pair
   "Zoom In Up". Together they read as arriving toward the reader; either alone
   is weaker: fade with no travel is invisible, travel with no fade is a slide.

   REJECTED — Bounce, in all five directions. Overshoot is the single strongest
   "template" tell there is; it is the 2014 look this brand is trying not to
   have, and it adds ~200ms of settle to every element for no information.

   REJECTED — Left and Right, for fade and zoom alike. A horizontal entrance
   moves an element outside the viewport before it comes in, which is the most
   common cause of a phantom horizontal scrollbar on a phone. Already banned in
   docs/03-brand-brief.md for that reason.

   VISIBILITY — 24px, not 14px. The first pass cut travel almost in half at the
   same time as the duration, and the two compounded into something technically
   present and practically invisible: the founder reported seeing no animation
   at all, on a page where every rule was firing correctly. Subtle is a choice
   you earn on a site people already trust; a pre-launch page has to be legible
   first. 24px over .55s is clearly visible and still well short of the 1.1s the
   original ladder took to finish. */
.js [data-reveal] { transition: opacity .55s var(--ease), transform .55s var(--ease); }
.js [data-reveal]:not(.is-in) { opacity: 0; transform: translateY(24px) scale(.97); }
/* 80ms steps — Emil's upper bound, deliberately at the top of it. At 60ms the
   cascade across a three-card row was too tight to read as a sequence. */
[data-reveal][data-delay="1"] { transition-delay: .08s; }
[data-reveal][data-delay="2"] { transition-delay: .16s; }
[data-reveal][data-delay="3"] { transition-delay: .24s; }
[data-reveal][data-delay="4"] { transition-delay: .32s; }
[data-reveal][data-delay="5"] { transition-delay: .40s; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation-duration: .001ms !important; animation-iteration-count: 1 !important; transition-duration: .001ms !important; }
  /* Scoped to :not(.is-in) — the un-revealed state only. Resetting the revealed
     state instead would outrank .card--hover:hover and strip hover feedback for
     every reduced-motion visitor, which is not what the setting asks for:
     reduced motion means less movement, not no response. With transition-duration
     already clamped above, a hover transform here simply applies instantly.

     Both lines matter even though JS adds .is-in immediately under reduced
     motion — if the script never runs at all, blocked or failed, the page must
     still be correct rather than merely still. Without them the content would be
     invisible and the icons stuck 6% small, reading as a design choice rather
     than a script that never arrived. */
  .js [data-reveal]:not(.is-in) { opacity: 1; transform: none; }
  .js [data-reveal]:not(.is-in) .icon-box { transform: none; }
  /* The logo skips its draw entirely. The global animation-duration clamp above
     would otherwise leave the cyan outline mid-retire — a stray stroke over the
     mark. Cancelling both animations by name is the only reliable reset. */
  .js:not(.logo-seen) .brand-logo__body { animation: none; opacity: 1; transform: none; }
  .js:not(.logo-seen) .brand-logo__draw { animation: none; opacity: 0; }
}

/* Utilities */
/* The UA's [hidden]{display:none} loses to any class that sets display, and
   .btn sets inline-flex — so hidden buttons would still render without this. */
[hidden] { display: none !important; }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
.skip-link { position: absolute; left: -9999px; top: 0; z-index: 200; background: var(--primary-600); color: #fff; padding: .75rem 1.25rem; border-radius: 0 0 var(--r-sm) 0; }
.skip-link:focus { left: 0; }
.mt-2 { margin-top: 2rem; }
.mt-3 { margin-top: 3rem; }

/* ==========================================================================
   Plain two-column policy table + ordered lists in prose
   Added for refund-policy.html / subscriber-agreement.html. `table.compare`
   centres its cells and assumes 5 plan columns; a policy table is left-read
   prose in a grid and needs neither.
   ========================================================================== */
table.plain { width: 100%; border-collapse: collapse; min-width: 32rem; font-size: .92rem; }
table.plain th, table.plain td { padding: .9rem 1.1rem; text-align: left; vertical-align: top; border-bottom: 1px solid var(--border-soft); }
table.plain thead th { background: var(--card); font-size: var(--fs-xs); letter-spacing: .1em; text-transform: uppercase; color: var(--muted); }
table.plain td { color: var(--muted); }
table.plain tbody tr:last-child td { border-bottom: 0; }
table.plain tr.is-emphasis td { color: var(--text); font-weight: 600; }
.prose ol { list-style: decimal; padding-left: 1.35rem; display: grid; gap: .5rem; margin-bottom: 1rem; }
.prose .table-wrap { margin-block: 1.25rem; }

/* ==========================================================================
   Early-access offer — the stacked free-months computation
   Three deductions and a total. Built as a grid rather than a <table> because
   the label wraps to two lines on a phone while the figure must stay on one,
   and a table cannot reflow that way without a horizontal scrollbar.
   ========================================================================== */
.offer {
  margin-top: clamp(2.25rem, 4vw, 3.25rem);
  border: 1px solid rgba(212,175,55,.34);
  border-radius: var(--r-xl);
  background: var(--ground-mark);
  padding: clamp(1.5rem, 4vw, 3rem);
}
.offer-head { text-align: center; max-width: 62ch; margin-inline: auto; }
/* h2 on pricing.html, where the offer is its own top-level section, and h3 on
   index.html, where it sits inside the pricing section under that section's
   h2. Same block, same size — the heading level follows the document outline,
   not the type scale. */
.offer-head h2, .offer-head h3 {
  font-size: clamp(1.6rem, 1.1rem + 2.2vw, 2.4rem);
  margin: 1rem 0 .75rem;
  letter-spacing: -.025em;
  text-wrap: balance;
}
.offer-head p { margin-bottom: 0; }

.offer-calc {
  margin: clamp(1.75rem, 3vw, 2.5rem) auto 0;
  max-width: 640px;
  display: grid;
  gap: 0;
  border: 1px solid var(--border-soft);
  border-radius: var(--r-lg);
  background: var(--surface);
  overflow: hidden;
  counter-reset: none;
  list-style: none;
}
.offer-row {
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-areas: "label value" "note value";
  align-items: center;
  column-gap: 1rem;
  padding: .95rem clamp(1rem, 3vw, 1.5rem);
  border-bottom: 1px solid var(--border-soft);
}
.offer-row:last-child { border-bottom: 0; }
.offer-label { grid-area: label; font-size: .97rem; font-weight: 500; }
.offer-label em {
  font-style: normal; display: inline-block; margin-left: .45rem;
  font-size: var(--fs-xs); font-weight: 700;
  padding: .12rem .5rem; border-radius: var(--r-pill);
  background: rgb(11 96 57 / .15); color: var(--success);
}
.offer-calc-note { grid-area: note; font-size: var(--fs-xs); color: var(--muted); font-variant-numeric: tabular-nums; }
.offer-val {
  grid-area: value; text-align: right; white-space: nowrap;
  font-size: 1.15rem; font-weight: 700; font-variant-numeric: tabular-nums;
  letter-spacing: -.02em;
}
.offer-row.is-cut .offer-val { color: var(--success); }
.offer-row.is-bonus { background: var(--ground-mark); }
.offer-row.is-bonus .offer-label em { background: rgba(212,175,55,.18); color: var(--gold-ink); }
.offer-row.is-total {
  background: rgb(0 120 212 / .1);
  border-top: 1px solid rgb(0 120 212 / .35);
  padding-block: 1.15rem;
}
.offer-row.is-total .offer-label { font-size: 1rem; }
.offer-row.is-total .offer-val { font-size: 1.5rem; color: var(--secondary); }
.offer-val small { font-size: var(--fs-xs); font-weight: 500; color: var(--muted); margin-left: .1rem; }

.offer-foot {
  max-width: 60ch; margin: 1.5rem auto 0; text-align: center;
  font-size: .95rem; color: var(--muted);
}
.offer-foot strong { color: var(--gold-ink); }

/* Under ~430px the figure and the label stop fitting side by side, so the row
   becomes two stacked lines with the figure right-aligned under its label. */
@media (max-width: 430px) {
  .offer-row {
    grid-template-columns: 1fr;
    grid-template-areas: "label" "note" "value";
    row-gap: .3rem;
  }
  .offer-val { text-align: left; }
  .offer-row.is-total .offer-val { font-size: 1.35rem; }
}

/* ==========================================================================
   Responsive corrections
   ========================================================================== */

/* A CTA label long enough to be persuasive ("Reserve your seat and lock this
   in") is wider than a 320px viewport once gutters are taken off. nowrap is
   right on desktop — it stops pill buttons going ragged — and wrong on a
   phone, where the button should simply grow taller instead of pushing the
   whole document sideways. Verified against 320/360/375/390/414/430. */
.btn { max-width: 100%; }
@media (max-width: 560px) {
  .btn { white-space: normal; text-align: center; }
  .btn-lg { padding: .95rem 1.35rem; font-size: 1rem; }
  .btn-row { width: 100%; }
  /* Same contract, same region list: in a centred block on a phone, two CTAs
     side by side each fall to their own content width and wrap ragged. Full
     width stacks them into one clean column. */
  :where(.center, .hero-inner, .page-hero, .cta-band, .offer-head, .offer) .btn-row .btn,
  .btn-row.center .btn { flex: 1 1 100%; }
}

/* The announcement bar carries the whole offer. On a narrow phone the
   explanatory half costs three lines, so it drops and the headline figure
   stays. */
@media (max-width: 560px) {
  .announce { font-size: .8rem; line-height: 1.5; }
  .announce-detail { display: none; }
}

/* ==========================================================================
   Typography — line-rag balance
   A centred block whose first line runs the full measure and whose second line
   is a quarter of it reads as "not centred" even when the box is perfectly
   centred, because the eye reads the silhouette, not the box. text-wrap
   evens the lines out; the max-widths guarantee there is something to even.
   Unsupported browsers simply get today's behaviour.
   ========================================================================== */
h1, h2, h3, .offer-head :is(h2,h3), .cta-band h2 { text-wrap: balance; }
.lead, .faq-body p, .prose p, .plan-for, .offer-foot { text-wrap: pretty; }

.page-hero h1 { max-width: 24ch; margin-inline: auto; }
.page-hero .lead { max-width: 54ch; }

/* ==========================================================================
   Page hero — rank and funnel rhythm
   Alignment is no longer handled here: .page-hero is named in the centring
   contract above, which centres its button row, reassurance line and lead for
   it. What is left below is purely the vertical rhythm of the block.
   ========================================================================== */

/* The breadcrumb was unstyled — inheriting body size in the brightest
   foreground colour, so the least important line in the hero outranked
   everything but the h1. It marks location; it is not a heading. */
.breadcrumb {
  font-size: var(--fs-sm);
  color: var(--muted);
  margin-bottom: clamp(.9rem, 1.8vw, 1.35rem);
}

/* At 52px, a 1.15 line-height opens the gap wide enough that a two-line
   headline reads as two headlines. Display sizes want to be tighter than
   body copy, not the same. */
.page-hero h1 {
  line-height: 1.07;
  margin-bottom: clamp(.85rem, 1.8vw, 1.2rem);
}

/* Funnel rhythm: gaps tighten from breadcrumb through headline to lead, then
   open before the CTA — so the eye accelerates down the block and arrives at
   the button instead of drifting past it. */
.page-hero .lead { margin-bottom: 0; }
.page-hero .btn-row { margin-top: clamp(1.6rem, 3vw, 2.25rem); }
.page-hero .reassure { margin-top: 1.1rem; }
.hero .lead { max-width: 54ch; }

@media (max-width: 360px) { .site-header .brand-tag { display: none; } }

/* The logo is a link, and on a phone it was a 34px-high one — under the 44px
   touch target. The wordmark cannot grow without unbalancing the header, so
   the padding grows instead and the negative inline margin keeps the mark
   optically flush with the container edge. */
@media (max-width: 1024px), (pointer: coarse) {
  .brand { min-height: 44px; padding-block: .25rem; }
  /* The desktop nav still shows between 941px and 1024px, which is exactly an
     iPad in landscape — a touch device reading the desktop layout. At 39px the
     links were under the 44px target on the one device that needs it most. */
  .nav a { min-height: 44px; display: inline-flex; align-items: center; }
}

/* ==========================================================================
   Wide tables on a phone
   .compare has a 760px min-width and .plain a 32rem one, so both scroll inside
   .table-wrap on a phone. Scrolling you cannot see is scrolling you do not do:
   a right-edge fade shows there is more, and it clears itself once you reach
   the end. Progressive enhancement — browsers without animation-timeline just
   keep the fade, which is still truthful at the scroll position that matters.
   ========================================================================== */
.table-wrap { position: relative; -webkit-overflow-scrolling: touch; }
@media (max-width: 820px) {
  .table-wrap::after {
    content: ""; position: absolute; top: 0; right: 0; bottom: 0; width: 44px;
    pointer-events: none; border-radius: 0 var(--r-lg) var(--r-lg) 0;
    background: linear-gradient(90deg, transparent, var(--bg));
  }
  .table-scroll-hint {
    display: flex; align-items: center; gap: .45rem;
    font-size: var(--fs-xs); color: var(--muted); margin: 0 0 .6rem;
  }
  .table-scroll-hint::after { content: "\2192"; font-size: 1rem; line-height: 1; }

  /* Pin the feature column. Scrolling a 760px comparison inside a 390px phone
     pushes the feature name off the left edge, so by the time the Pro and
     Enterprise columns are in view you are reading four figures with nothing
     to say what they are figures for. Sticky keeps the question on screen
     while you scroll the answers. Cells need an opaque background of their
     own — the row's colour is painted behind, and the scrolled content would
     otherwise show straight through the pinned column. */
  table.compare th:first-child,
  table.compare td:first-child {
    position: sticky; left: 0; z-index: 2;
    background: var(--surface);
    box-shadow: 1px 0 0 var(--border-soft);
    min-width: 150px; max-width: 46vw;
  }
  table.compare thead th:first-child { background: var(--card); z-index: 3; }
  /* A group heading spans all five columns, so there is nothing to its right
     to scroll under it — pinning it would only make it slide out of its row. */
  table.compare .row-group td:first-child { position: static; box-shadow: none; }
  table.compare tbody tr:hover td:first-child { background: var(--surface); }
}
/* Scoped to desktop. This was unscoped and sat AFTER the @media (max-width:820px)
   block that sets `display: flex` — same specificity, later wins — so the swipe
   hint never rendered anywhere, including the phones it was written for. The
   arrow, the copy and the media query were all live; only the affordance was
   dead. If hiding it was deliberate, delete the rule rather than un-scoping it,
   so the next reader is not left guessing again. */
@media (min-width: 821px) {
  .table-scroll-hint { display: none; }
}

@supports (animation-timeline: scroll(self inline)) {
  @media (max-width: 820px) {
    .table-wrap::after {
      animation: fade-out-at-end linear both;
      animation-timeline: scroll(self inline);
      animation-range: contain;
    }
    @keyframes fade-out-at-end { 0%, 80% { opacity: 1; } 100% { opacity: 0; } }
  }
}

/* The header CTA is btn-sm because the desktop header is 72px tall. Inside the
   open mobile menu it is the primary conversion button on the page and was
   rendering 35px high — under the 44px touch target. Give it full size there. */
@media (max-width: 940px) {
  .site-header.is-open .header-cta .btn {
    width: 100%; padding: .9rem 1.25rem; font-size: .97rem;
  }
}

/* The billing toggle was 41px tall — just under the 44px touch target, and it
   is the control that decides which price a visitor reads. */
.billing-toggle button { min-height: 44px; }

/* Footer links were 19px tall with a 16px gap between them — the same fiddly
   target this file already fixed for .nav and .billing-toggle, on the one part
   of the page that holds the legal documents a clinic goes looking for.
   The list gap is absorbed INTO the target rather than added to it, so the rows
   get easier to hit without the footer growing by the same amount. */
@media (max-width: 1024px), (pointer: coarse) {
  .footer-grid ul { gap: 0; }
  .footer-grid ul a { min-height: 44px; display: flex; align-items: center; }
  .footer-solutions ul a { min-height: 44px; display: flex; align-items: center; }
}

/* <picture> is inline by default, which leaves a descender gap under the shot
   inside its frame. */
.shot-frame picture { display: block; }
.shot-frame picture img { border-radius: calc(var(--r-xl) - 10px); width: 100%; }

/* Interior-page leads are one or two sentences, which is exactly the range
   text-wrap: balance handles well — pretty only fixes the last-line orphan and
   leaves a 676px line above a 306px one, which is what reads as off-centre. */
.page-hero .lead, .cta-band .lead, .offer-head p { text-wrap: balance; }

/* ── Real product screenshots inside bento cards ───────────────────────────────
   Replaces the former CSS mock-ups (.teeth/.tooth, .bars/.bar, .mock-row) with
   actual captures of the app. Each shot is cropped to the card with
   object-fit:cover and pinned to the top-left, so the part of the UI that
   carries the meaning stays visible at small sizes instead of being centred
   and cut on both sides. Sourced from a seeded demo clinic — never real
   patient data. See assets/img/app-*.png. */
.bento-shot { display: block; width: 100%; height: 100%; object-fit: cover; object-position: top left; }
.bento-visual--shot { padding: 0; line-height: 0; }
.bento-visual--strip { aspect-ratio: 2576 / 178; }
.bento-visual--strip .bento-shot { object-fit: contain; object-position: center; background: var(--bg); }
.bento-visual--stats { aspect-ratio: 245 / 130; }
.bento-visual--stats .bento-shot { object-fit: contain; object-position: center; background: var(--bg); }
.bento-visual--card  { height: 228px; }
.bento-visual--list  { height: 228px; }

/* btn-sm exists for the 72px desktop header. Used in page content — the
   "Not ready for a call?" card on book-a-demo, the sidebar CTAs — it rendered
   39px tall, under the 44px touch target, on a real conversion button.
   pointer:coarse catches touch laptops and iPads that the width query misses. */
@media (max-width: 1024px), (pointer: coarse) {
  .btn-sm { min-height: 44px; }
}

/* index.html's bento grid uses different spans from features.html, so the same
   image would render 18px tall in the narrow charting card and letterboxed in
   the wide commissions one. These two modifiers carry the shapes that page needs:
   a portrait findings panel for the narrow card, a wide KPI row for the wide one. */
.bento-visual--panel { height: 228px; }
.bento-visual--panel .bento-shot { object-fit: cover; object-position: top center; }
.bento-visual--statsrow { aspect-ratio: 700 / 120; }
.bento-visual--statsrow .bento-shot { object-fit: contain; object-position: center; background: var(--bg); }

/* ==========================================================================
   Offer — savings-first redesign (2026-08-24)
   Previously this card opened with a 4-line subtraction worksheet (×, −, ÷)
   topped by the single biggest number on the page (₱34,860). That anchors a
   visitor on cost before they have seen any benefit, and itemised arithmetic
   reads as a coupon-site trick rather than something a mid-to-high-paying
   clinic owner trusts at a glance. The fix: lead with what they gain — the
   months, then the pesos — and move the exact working into a closed-by-
   default disclosure. Nothing is hidden, it just is not the first thing read.
   ========================================================================== */

/* The "2 + 2 = 4" equation. This is the answer to a real point of confusion:
   the billing toggle's badge says "2 months free" (annual only), and this
   section separately claims "4 months free" (annual + reserving). Spelling
   the addition out removes any need to reconcile the two numbers in your
   head — it is done for you, in one glance. */
.offer-stack {
  display: flex; align-items: center; justify-content: center;
  gap: clamp(.5rem, 2vw, 1.25rem);
  margin: clamp(1.75rem, 3vw, 2.5rem) 0;
  flex-wrap: wrap;
}
.stack-chip {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  padding: 1rem 1.1rem; border-radius: var(--r-lg);
  background: var(--card); border: 1px solid var(--border-soft);
  min-width: 118px;
}
.stack-num {
  font-size: clamp(1.9rem, 1.5rem + 1vw, 2.4rem); font-weight: 800;
  line-height: 1; letter-spacing: -.03em; color: var(--text);
  font-variant-numeric: tabular-nums;
}
.stack-label { font-size: var(--fs-xs); color: var(--muted); margin-top: .4rem; line-height: 1.35; }
.stack-op { font-size: 1.5rem; font-weight: 700; color: var(--muted); flex: none; }
.stack-chip--total {
  background: linear-gradient(160deg, rgba(212,175,55,.18), rgba(212,175,55,.06));
  border-color: rgba(212,175,55,.45);
}
.stack-chip--total .stack-num { color: var(--gold-ink); }
.stack-chip--total .stack-label { color: var(--text); font-weight: 600; }
@media (max-width: 480px) {
  /* Below this width, two chips plus their operators no longer share a row
     cleanly — the wrap point lands mid-equation and strands a lone '+' at the
     end of row 1. A single column reads the same equation top to bottom
     without that awkward split. */
  .offer-stack { flex-direction: column; gap: .5rem; }
  .stack-chip { width: 100%; max-width: 220px; flex-direction: row; justify-content: center; gap: .65rem; padding: .75rem 1rem; }
  .stack-num { font-size: 1.5rem; }
  .stack-label { margin-top: 0; text-align: left; }
  /* The <br> stays. Hiding it does not stop the label wrapping at this width —
     it only removes the space, so it rendered "months freewith annual billing"
     and then wrapped anyway, in the wrong place. */
  .stack-op { font-size: 1.1rem; }
}
@media (min-width: 481px) and (max-width: 560px) {
  .offer-stack { gap: .6rem; }
  .stack-chip { min-width: 92px; padding: .8rem .7rem; }
  .stack-op { font-size: 1.15rem; }
}

/* Was / now / savings — the price half of the story, secondary to the months
   above but still the concrete peso payoff a numbers-minded owner wants. */
.offer-savings {
  display: flex; align-items: center; justify-content: center;
  gap: clamp(1.25rem, 4vw, 2.5rem);
  flex-wrap: wrap;
  padding: 1.5rem clamp(1rem, 3vw, 2rem);
  border-radius: var(--r-lg);
  background: var(--surface); border: 1px solid var(--border-soft);
}
.offer-was, .offer-now { text-align: center; }
.offer-was-label, .offer-now-label {
  display: block; font-size: var(--fs-xs); color: var(--muted); margin-bottom: .3rem;
}
.offer-was-amt {
  font-size: 1.3rem; font-weight: 600; color: var(--muted);
  text-decoration: line-through; text-decoration-color: rgb(90 102 120 / .6);
  font-variant-numeric: tabular-nums;
}
.offer-now-amt {
  font-size: clamp(2rem, 1.6rem + 1.2vw, 2.6rem); font-weight: 800; letter-spacing: -.03em;
  color: var(--text); font-variant-numeric: tabular-nums;
}
.offer-save-badge {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  padding: .85rem 1.5rem; border-radius: var(--r-lg);
  background: linear-gradient(135deg, #E6C24F, var(--gold));
  box-shadow: 0 10px 26px -10px rgba(212,175,55,.6);
}
.offer-save-amt {
  font-size: 1.35rem; font-weight: 800; color: #1A1405; letter-spacing: -.02em;
  font-variant-numeric: tabular-nums;
}
.offer-save-sub { font-size: var(--fs-xs); font-weight: 600; color: rgba(26,20,5,.75); margin-top: .15rem; }

/* The exact working — closed by default. <details> rather than JS: keyboard
   and screen-reader operable for free, and it degrades to always-visible if
   CSS fails rather than hiding the content outright. */
.offer-details {
  max-width: 640px; margin: 1.5rem auto 0;
  border: 1px solid var(--border-soft); border-radius: var(--r-lg);
  background: var(--surface); overflow: hidden;
}
.offer-details summary {
  cursor: pointer; list-style: none; padding: .95rem 1.2rem;
  font-size: .92rem; font-weight: 600; color: var(--secondary);
  display: flex; align-items: center; gap: .6rem;
}
.offer-details summary::-webkit-details-marker { display: none; }
.offer-details summary::after {
  content: ""; margin-left: auto; flex: none;
  width: 9px; height: 9px; border-right: 2px solid var(--muted); border-bottom: 2px solid var(--muted);
  transform: rotate(45deg) translateY(-2px); transition: transform .2s var(--ease);
}
.offer-details[open] summary::after { transform: rotate(-135deg) translateY(-2px); }
.offer-details[open] summary { border-bottom: 1px solid var(--border-soft); }
.offer-details .offer-calc { margin: 0; max-width: none; border: 0; border-radius: 0; background: transparent; }

/* "You keep" column in the every-plan comparison table — the takeaway figure,
   bolded and gold so it draws the eye instead of the two cost columns on
   either side of it. */
td.offer-keep { color: var(--gold-ink); font-weight: 700; }

/* ==========================================================================
   Every-plan savings — proportional bars
   Replaces a five-column currency table (Plan / 14 months at monthly rate /
   You pay / You keep / Effective per month). That table was accurate and said
   nothing: it asked the reader to hold two peso figures in their head, subtract
   them, and do it four times before noticing the point — which is that the
   deal is *identical* on every plan. Four months free out of fourteen, whether
   the clinic pays ₱1,290 or ₱7,990 a month.

   A proportion is what carries that, and length is the visual channel people
   judge most accurately, so each plan gets one bar split 10 : 4. The bars are
   the same width and sit directly above one another, so the split lands on the
   same vertical line down all four rows — the invariant is visible before a
   single number is read. The exact figures stay on the row underneath, so
   nothing is lost against the table it replaces; they are just no longer the
   thing you have to decode first.

   The 71.43% / 28.57% split is hard-coded rather than passed in per row: it is
   the same fraction for every plan by definition, and writing it once means a
   future edit cannot accidentally make one plan's bar disagree with the others.
   ========================================================================== */
.save-list {
  max-width: 760px; margin: 1.5rem auto 0;
  display: grid; gap: .85rem;
  list-style: none; padding: 0;
}
.save-row {
  display: grid;
  /* All three columns are fixed except the bar. Sizing the last one to `auto`
     let it track its own content, and "₱31,960" is wider than "₱5,160" — so
     every row got a slightly different bar width and the 10 : 4 split stopped
     landing on a common vertical line. Each row is its own grid; nothing
     aligns them but identical track sizes. */
  grid-template-columns: 168px 1fr 120px;
  /* The figures line runs under the bar AND the saving rather than just the
     bar. Confined to the bar column it had ~390px, and Enterprise — the only
     row carrying a "from" and six-digit numbers — wrapped to a second line,
     leaving that row visibly taller than the other three. Spanning both
     columns is copy-length independent, so the row heights stay even however
     the prices change. */
  grid-template-areas: "plan bar  keep" "plan meta meta";
  align-items: center;
  column-gap: 1.25rem; row-gap: .45rem;
  padding: 1rem 1.25rem;
  border: 1px solid var(--border-soft); border-radius: var(--r-lg);
  background: var(--surface);
}
.save-row.is-emphasis {
  border-color: rgba(212,175,55,.4);
  background: linear-gradient(160deg, rgba(212,175,55,.1), rgba(212,175,55,.03));
}

.save-plan { grid-area: plan; display: flex; flex-direction: column; gap: .35rem; align-items: flex-start; }
.save-plan-name { font-weight: 700; font-size: 1.05rem; color: var(--text); }
.save-row.is-emphasis .save-plan-name { color: var(--gold-ink); }
.save-plan-tag {
  font-size: .75rem; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  padding: .16rem .5rem; border-radius: var(--r-pill);
  background: linear-gradient(135deg, #E6C24F, var(--gold)); color: #1A1405;
  white-space: nowrap;
}

/* The bar. overflow:hidden lets the two segments share one pill outline. */
.save-bar {
  grid-area: bar;
  display: flex; height: 34px;
  border-radius: var(--r-pill); overflow: hidden;
  border: 1px solid var(--border-soft);
  background: var(--surface);
}
.save-seg {
  display: flex; align-items: center; justify-content: center;
  font-size: var(--fs-xs); font-weight: 600; white-space: nowrap;
  overflow: hidden;
}
.save-seg--paid {
  width: 71.4286%;   /* 10 of 14 months */
  background: linear-gradient(90deg, rgb(0 120 212 / .4), rgb(0 120 212 / .24));
  color: var(--text);
}
.save-seg--free {
  width: 28.5714%;   /* 4 of 14 months */
  background: linear-gradient(135deg, #E6C24F, var(--gold));
  color: #1A1405;
}

.save-meta {
  grid-area: meta;
  font-size: var(--fs-sm); color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.save-meta s { color: var(--muted); text-decoration-color: rgb(90 102 120 / .6); }
.save-meta strong { color: var(--text); font-weight: 600; }

.save-keep {
  grid-area: keep; text-align: right;
  display: flex; flex-direction: column; align-items: flex-end; gap: .1rem;
}
.save-keep-amt {
  font-size: 1.3rem; font-weight: 800; color: var(--gold-ink);
  letter-spacing: -.02em; font-variant-numeric: tabular-nums; line-height: 1.1;
}
.save-keep-label { font-size: var(--fs-xs); color: var(--muted); }

/* Under ~760px the three columns stop fitting: the plan name and the saving
   take the top line, the bar spans full width beneath them, and the figures
   sit under that. The bars stay equal width, so the 10 : 4 split still lines
   up down the list — which is the whole reason the block exists. */
@media (max-width: 760px) {
  .save-row {
    grid-template-columns: 1fr auto;
    grid-template-areas: "plan keep" "bar bar" "meta meta";
    align-items: start; row-gap: .7rem;
  }
  .save-plan { flex-direction: row; align-items: center; gap: .5rem; flex-wrap: wrap; }
  .save-keep { align-items: flex-end; }
  .save-keep-amt { font-size: 1.15rem; }
}

/* At phone width the paid segment's label stops fitting on one line inside its
   share of the bar. The free months are the message, so that label is the one
   that survives; the paid segment keeps its length, which is what encodes the
   proportion, and loses only its caption. */
@media (max-width: 480px) {
  .save-seg--paid { font-size: 0; }
  .save-bar { height: 30px; }
  .save-row { padding: .9rem 1rem; }
}

/* ---- Mobile: tables that restack instead of scrolling ---------------------
   Classic CSS-only responsive-table technique: hide the header row and turn
   each <tr> into a card. One semantic <table> serves every viewport — nothing
   duplicated, nothing to keep in sync. */
@media (max-width: 640px) {
  /* Policy tables (privacy / refund / subscriber agreement) are two columns of
     prose: a situation and what follows from it. Sideways scrolling is the
     wrong answer for that shape — the pair belongs together and reads as one
     block. Stack them: term first, then its explanation, one card per row.
     The <thead> labels are generic enough ("Situation" / "What happens") that
     the pairing survives losing them. */
  table.plain { min-width: 0; }
  table.plain thead { display: none; }
  table.plain, table.plain tbody, table.plain tr, table.plain td { display: block; width: 100%; }
  table.plain tr {
    padding: .9rem 1.1rem; margin-bottom: .7rem;
    border: 1px solid var(--border-soft); border-radius: var(--r-lg);
    background: var(--surface);
  }
  table.plain tr:last-child { margin-bottom: 0; }
  table.plain td { padding: 0; border-bottom: 0; }
  /* The first cell is the row's heading, not a labelled value. */
  table.plain td:first-child {
    color: var(--text); font-weight: 600; margin-bottom: .3rem;
  }
  table.plain tr.is-emphasis {
    border-color: rgba(212,175,55,.4);
    background: linear-gradient(160deg, rgba(212,175,55,.1), rgba(212,175,55,.03));
  }
  .prose .table-wrap:has(table.plain) { background: transparent; border: 0; border-radius: 0; }
}


/* ==========================================================================
   Pricing calculator — /pricing.html #calculator
   A clinic owner comparing systems at nine at night wants one question
   answered: what does MY clinic cost. The plan cards answer it for a clinic
   with one branch; this answers it for theirs.
   Entirely progressive: with JS off the figures stay at their server-rendered
   defaults and every control is still a working form element.
   ========================================================================== */
.calc {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 1.5rem;
  align-items: start;
}
@media (max-width: 860px) { .calc { grid-template-columns: 1fr; } }

.calc-panel {
  background: var(--card);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-lg);
  padding: 1.5rem;
}

.calc-field { border: 0; padding: 0; margin: 0 0 1.5rem; }
.calc-field:last-child { margin-bottom: 0; }
.calc-field > legend,
.calc-label {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
  width: 100%;
  padding: 0;
  margin-bottom: .6rem;
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--text);
}
.calc-hint { margin: .5rem 0 0; font-size: var(--fs-xs); color: var(--muted); }

/* Plan chooser — real radios, visually a segmented control. */
.calc-plans { display: grid; grid-template-columns: repeat(3, 1fr); gap: .5rem; }
.calc-plans label { position: relative; display: block; cursor: pointer; }
.calc-plans input {
  position: absolute; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer;
}
.calc-plans span {
  display: flex; flex-direction: column; gap: .15rem; align-items: center;
  padding: .7rem .4rem;
  border: 1px solid var(--border-soft);
  border-radius: var(--r-md);
  background: var(--surface);
  font-size: var(--fs-sm); font-weight: 600; color: var(--text);
  transition: border-color .15s var(--ease), background .15s var(--ease);
}
.calc-plans span small { font-size: var(--fs-xs); font-weight: 500; color: var(--muted); }
.calc-plans input:checked + span {
  border-color: var(--primary);
  background: color-mix(in srgb, var(--primary) 8%, var(--surface));
}
.calc-plans input:focus-visible + span { outline: 2px solid var(--primary); outline-offset: 2px; }

/* Branch slider. The number beside the label is the answer to "where am I". */
.calc-count { font-variant-numeric: tabular-nums; font-weight: 700; color: var(--primary); }
.calc-range { width: 100%; margin: 0; accent-color: var(--primary); height: 1.5rem; }
.calc-range:focus-visible { outline: 2px solid var(--primary); outline-offset: 4px; }
.calc-scale {
  display: flex; justify-content: space-between;
  margin-top: .25rem; font-size: var(--fs-xs); color: var(--muted);
}

/* Result panel */
.calc-result { background: var(--surface); }
.calc-row {
  display: flex; justify-content: space-between; align-items: baseline; gap: 1rem;
  padding: .6rem 0;
  border-bottom: 1px solid var(--border-soft);
  font-size: var(--fs-sm);
}
.calc-row:last-of-type { border-bottom: 0; }
.calc-row dt { color: var(--muted); margin: 0; }
.calc-row dd { margin: 0; font-variant-numeric: tabular-nums; font-weight: 600; color: var(--text); }
.calc-row--total { border-top: 2px solid var(--border); border-bottom: 0; padding-top: .9rem; margin-top: .3rem; }
.calc-row--total dt { color: var(--text); font-weight: 600; }
.calc-row--total dd { font-size: var(--fs-h3); font-weight: 700; }
.calc-row[hidden] { display: none; }

/* Block, not flex: as a flex container the <strong> and the text either side of
   it become separate items, so the sentence broke after the amount instead of
   wrapping like prose. */
.calc-keep {
  margin-top: 1rem; padding: .8rem 1rem;
  border-radius: var(--r-md);
  background: color-mix(in srgb, var(--gold) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--gold) 35%, transparent);
  font-size: var(--fs-sm);
}
.calc-keep strong { font-variant-numeric: tabular-nums; }
.calc-foot { margin: 1rem 0 0; font-size: var(--fs-xs); color: var(--muted); }

/* ==========================================================================
   Back to top
   The legal pages run past 1,200px of prose on a phone; the pricing page is
   longer. Injected by site.js rather than sitting in twelve files, so it cannot
   drift page to page.
   ========================================================================== */
.to-top {
  position: fixed; right: var(--gutter); bottom: var(--gutter); z-index: 90;
  width: 44px; height: 44px;          /* the same touch target as everything else */
  display: grid; place-items: center;
  border: 1px solid var(--border-soft); border-radius: 50%;
  background: var(--card); color: var(--primary-600);
  box-shadow: var(--shadow-md);
  opacity: 0; transform: translateY(8px); pointer-events: none;
  transition: opacity .25s var(--ease), transform .25s var(--ease);
}
.to-top svg { width: 20px; height: 20px; transform: rotate(-90deg); }
.to-top.is-visible { opacity: 1; transform: none; pointer-events: auto; }
.to-top:hover { background: var(--card-hover); border-color: var(--primary); }
.to-top:focus-visible { outline: 2px solid var(--secondary); outline-offset: 3px; }

/* ==========================================================================
   First-visit splash
   The mark draws itself once per session, centred. Deliberately NOT a loading
   screen: the page behind it is already painted and the overlay never blocks a
   click, so it costs nothing if a visitor ignores it. Skipped entirely for
   repeat views in the same session and under prefers-reduced-motion.
   ========================================================================== */
.splash {
  position: fixed; inset: 0; z-index: 200;
  display: grid; place-items: center;
  background: var(--bg);
  pointer-events: none;
  animation: splash-out .45s var(--ease) .62s both;
}
.splash img { width: 78px; height: 88px; animation: splash-mark .62s var(--ease) both; }
@keyframes menu-in  { from { opacity: 0; transform: translateY(10px) } to { opacity: 1; transform: none } }
@keyframes menu-out { to { opacity: 0; transform: translateY(6px) } }
@keyframes splash-mark { from { opacity: 0; transform: scale(.82) } to { opacity: 1; transform: none } }
@keyframes splash-out  { to { opacity: 0; visibility: hidden } }

@media (prefers-reduced-motion: reduce) {
  .to-top { transition: none; }
  .splash { display: none; }
}
