/* Marketing page only. Shares tokens and component classes with app.css.
   Spacing/geometry tokens (--s1..--s6, --radius, --measure) and the .badge / .b-* / .ic
   family now live in app.css so both sheets agree; only the mobile spacing override and
   marketing-specific rules remain here.

   ======================================================================================
   THE PAGE STANDARD. Read this before adding a page or a call to action. It is written
   down here because it was previously carried in nobody's head and every page drifted from
   every other one; tests/test_marketing.py now holds most of it, but the reasons only live
   here. /producto is the reference for container rhythm - not because it earns traffic (it
   took two human visitors in a month) but because its rhythm is the one that reads right.
   The homepage and /datos are where the traffic is and where a clearer CTA actually earns
   the click.

   A. THE TWO FAMILIES.
      Marketing - landing, producto, solution, buildings, munihub, munipage, guides. One
      <section class="hero">, then a strict alternation of <section class="band"><div
      class="wrap"> and <section class="wrap">, then the shared close, then the explore
      rail. NEVER two consecutive section.band: the band's background and border-block are
      identical, so two in a row render as one tall band with a hairline drawn through the
      middle of it, which reads as a mistake rather than as two subjects.
      Article - guide, audit, energy_report. Keeps <article class="guide"> for the prose,
      because 21px is the right h2 inside a long read and 34px is not. It adopts everything
      else the marketing family uses: the shared close, the shared stat component (.tiles),
      the .gcta block, and .guide.narrow in place of an inline max-width.

   B. THE SHARED CLOSE. Every page in both families ends with exactly one of:
        <section class="band"><div class="wrap cta"> h2, p.sub, one .btn.primary, one .btn
      Centred - .cta below does that. One shape, so a reader who has scrolled to the bottom
      of any page meets the same offer in the same place.

   C. THE EXPLORE RAIL. Then exactly one <section class="wrap"> holding an h2 and a
      <div class="rel-grid"> of three <a class="rel">, pointing at pages this visitor has
      not seen. The three come from the route, never from the template: a hardcoded trio
      goes stale the day a page is added and nobody notices, because a stale link still
      renders. One of the three carries class="rel glow".

   D. CTA HIERARCHY.
      At most one .btn.primary in the hero, one in the close, and one per other section, and
      no two .btn.primary on a page may share a destination - two identical primaries are
      not twice the invitation, they are a reader wondering which one is the real one.
      One destination, one label. Labels name the object: "Empezar" was the most-used and
      least specific string on the site, and it splits into pricing.free_cta on free-tier
      paths and cta.start_plan ("Empezar con {plan}") on a plan card.
      ONE SPANISH VERB PER ACTION - consultar is looking a licence number up in the
      register, comprobar is checking a portfolio, a building or a property for compliance.
      A heading and the button under it always use the same one.
      Every CTA href carries ?from=<page>, or the click cannot be attributed and the whole
      exercise of choosing between these labels is unmeasurable.

   E. THE ROTATING GLOW. .glow, defined in app.css - see the essay there for how and why it
      works through --rim. Placement is the same in both languages: the close block's card
      and its primary button on every page, one a.rel in the explore rail, and on the
      homepage the checker's .lookup card INSTEAD of the close, because the checker is the
      highest-traffic conversion element on the site. Three per page, no more.

   F. NEVER a style="" attribute in one of these templates. There were 38, six of them the
      same declaration repeated three or four times, which is how .gcta ended up with its
      layout written out by hand on four separate pages. Each is a class below. The only two
      that stay are the honeypot lines in audit.html and energy_report.html, where the
      off-screen parking is part of the trap rather than part of the design.
   ====================================================================================== */

body.marketing { background: var(--plane); -webkit-font-smoothing: antialiased; }

/* --- the third button ------------------------------------------------------------------
   .btn.ghost existed only as `.adm-filter .btn.ghost { background: transparent }` in app.css,
   which is to say it existed on one admin toolbar and nowhere else. The upsell macro at
   _pricing.html:58 renders it at the foot of /auditoria, /informe-energetico and the audit
   result, where it inherited the plain secondary look exactly - same glass, same rim, same
   weight - so the upsell offered two buttons of equal visual rank and the tier below
   secondary had no representation in the system at all.
   Here it is: label and nothing else at rest, a hairline on hover. No fill, no rim, and the
   blur switched off, because .btn:not(.small) in app.css turns backdrop-filter on for every
   button and a blur behind a transparent control smears the ground for no reason. This sheet
   loads after app.css, so these win the tie without !important. */
.btn.ghost { background: none; border-color: transparent; color: var(--ink-2);
  box-shadow: none; -webkit-backdrop-filter: none; backdrop-filter: none; }
.btn.ghost:hover { color: var(--ink); border-color: var(--border); box-shadow: none; }
/* padding-inline only: several sections carry .wrap themselves, and a shorthand here would
   override their block padding and collapse the section rhythm to nothing. */
.wrap { max-width: 1080px; margin: 0 auto; padding-inline: var(--s3); }
section { padding-block: var(--s6); scroll-margin-top: 76px; }
section.band { background: var(--surface); border-block: 1px solid var(--border); }
section > .wrap > h2, section.wrap > h2 { margin-bottom: var(--s1); }
/* margin-block only - setting the inline margins here would outrank .cta's auto-centring
   and pin centred copy to the left of its own max-width box. */
section > .wrap > .sub, section.wrap > .sub { max-width: var(--measure);
  margin-block: 0 var(--s4); font-size: 15px; }
/* trailing small print should not stack its own margin on top of the section padding */
section > .wrap > :last-child, section.wrap > :last-child { margin-bottom: 0; }

/* marketing tables get more room than the dense app ones */
.marketing .scroll table th { padding: 14px 18px; }
.marketing .scroll table td { padding: 16px 18px; }

/* --- header ------------------------------------------------------------------ */
.top { border-bottom: 1px solid var(--border);
  background: color-mix(in srgb, var(--surface) 72%, transparent);
  -webkit-backdrop-filter: saturate(140%) blur(14px);
  backdrop-filter: saturate(140%) blur(14px); position: sticky; top: 0; z-index: 5; }
.topbar { display: flex; align-items: center; justify-content: space-between; height: 64px; }
.topbar .logo { font-size: 14px; letter-spacing: .12em; text-transform: uppercase;
  color: var(--ink); font-weight: 600; padding: 0; }
.topbar nav { display: flex; align-items: center; gap: var(--s3); }
.topbar nav a { color: var(--ink-2); font-size: 14px; }
.topbar nav a:hover { color: var(--ink); text-decoration: none; }
.topbar nav a.btn { color: var(--ink); }
.topbar nav a.signin { color: var(--ink); font-weight: 500; }

/* --- solutions menu ----------------------------------------------------------------
   A <details> rather than a script: the CSP forbids inline JavaScript, and details/summary
   is keyboard-operable and announced by screen readers with no code at all. The panel is
   absolutely positioned so opening it does not push the header around. */
.navmenu { position: relative; }
.navmenu > summary { list-style: none; cursor: pointer; color: var(--ink-2); font-size: 14px;
  display: inline-flex; align-items: center; gap: 6px; }
.navmenu > summary::-webkit-details-marker { display: none; }
.navmenu > summary::after { content: ""; width: 6px; height: 6px; border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor; transform: rotate(45deg) translate(-2px, -2px);
  transition: transform .15s ease; }
.navmenu[open] > summary::after { transform: rotate(-135deg) translate(-2px, -2px); }
.navmenu > summary:hover, .navmenu[open] > summary { color: var(--ink); }
.navmenu-panel { position: absolute; top: calc(100% + 12px); left: 50%; transform: translateX(-50%);
  min-width: 320px; display: grid; gap: 2px; padding: var(--s2);
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-card);
  box-shadow: 0 18px 40px -18px rgba(0, 0, 0, .55); z-index: 10; }
.navmenu-panel a { display: grid; grid-template-columns: 28px 1fr; column-gap: 10px;
  row-gap: 2px; align-items: start; padding: 10px 12px; border-radius: var(--r-ctl);
  color: var(--ink); }
.navmenu-panel a .nm-ic { grid-row: span 2; width: 28px; height: 28px; border-radius: 9px;
  display: grid; place-items: center; margin-top: 1px; color: var(--accent);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 22%, transparent); }
.navmenu-panel a:hover .nm-ic { background: color-mix(in srgb, var(--accent) 24%, transparent); }
.navmenu-panel a:hover { background: color-mix(in srgb, var(--accent) 12%, transparent);
  text-decoration: none; }
.navmenu-panel a strong { font-size: 14px; font-weight: 600; }
.navmenu-panel a .muted { font-size: 12.5px; line-height: 1.35; }

/* --- the phone menu ---------------------------------------------------------------------
   A <details>, like the Solutions panel above and for the same CSP reason, but opening to a
   sheet rather than a dropdown. Hidden above 900px; the media query there turns it on and
   turns the desktop nav off in the same breath, so the two are never both in the bar.

   The summary IS the hamburger. Three bars drawn from one element - a 2px background plus a
   ::before and ::after offset above and below - because three <span>s in the markup would be
   three things to keep in step, and this shape has to become an X when the menu opens.

   Layout is a flex column with a scrolling middle and a sticky foot. That is not decoration:
   the drawer carries fifteen links now, which is taller than a phone, and the action at the
   bottom would otherwise be the one item a reader has to scroll back to. overscroll-behavior
   on the scroller stops a flick at the end of the list from scrolling the page behind it,
   which is the thing that makes a drawer feel like a page instead of a panel. */
.drawer { display: none; position: relative; }
/* .top is position: sticky with z-index 5, which makes it a stacking context - so the panel's
   z-index 20 is 20 WITHIN the header, and the header is 5 against the page. Anything outside
   it with a higher number paints over the sheet; .search-suggest is 40. While the drawer is
   open the header has to outrank the page, and :has() is how an ancestor learns that. */
header.top:has(.drawer[open]) { z-index: 60; }
.drawer > summary { list-style: none; cursor: pointer; width: 40px; height: 40px;
  display: grid; place-items: center; border-radius: var(--r-ctl); color: var(--ink);
  margin-right: -8px; }
.drawer > summary::-webkit-details-marker { display: none; }
.burger-bars, .burger-bars::before, .burger-bars::after {
  content: ""; display: block; width: 20px; height: 2px; border-radius: 2px;
  background: currentColor; transition: transform .18s ease, opacity .12s ease; }
.burger-bars { position: relative; }
.burger-bars::before { position: absolute; top: -6px; }
.burger-bars::after { position: absolute; top: 6px; }
/* Open, the three bars become an X: the middle one goes, the outer two return to centre and
   cross. Same element, so there is nothing to keep in sync. */
.drawer[open] > summary .burger-bars { background: transparent; }
.drawer[open] > summary .burger-bars::before { transform: translateY(6px) rotate(45deg); }
.drawer[open] > summary .burger-bars::after { transform: translateY(-6px) rotate(-45deg); }
/* Open, the control moves out of the bar and onto the sheet, because the sheet covers the
   bar. While it stayed put it was underneath the panel it had just opened - elementFromPoint
   at the middle of the hamburger returned a heading inside the drawer - so the X was never
   visible and, with the script absent, nothing on screen could close the menu at all.
   z-index sits above the panel's 20; both are inside .top's stacking context, so the two
   numbers are only ever compared with each other. */
.burger-label { display: none; font-size: 14px; font-weight: 600; }
.drawer[open] > summary { position: fixed; top: 0; right: 0; z-index: 21;
  /* The panel's own width, so this is the sheet's head rather than a pill floating on it.
     Opaque on purpose: the list scrolls underneath, and a translucent head would have the
     links sliding visibly through the word "Cerrar". */
  /* 64px, not an invented number: it is .topbar's height, so the head's bottom border lands
     on the same line as the header's own and the two read as one bar rather than as two rules
     a few pixels apart. */
  width: min(360px, 88vw); height: 64px; margin: 0; padding: 0 var(--s3); gap: 10px;
  grid-auto-flow: column; align-items: center; justify-content: end;
  background: var(--surface); border-bottom: 1px solid var(--border); border-radius: 0; }
.drawer[open] > summary .burger-label { display: block; }

/* The backdrop hangs off .drawer, not off the panel, and the difference only shows during a
   swipe. As the panel's own ::before it inherited the panel's transform, so dragging the sheet
   90px to the right dragged the dimming with it and left a 90px strip of undimmed page down
   the left edge - the page appearing to brighten as the menu was pulled away. .drawer itself
   never moves, so its ::before stays put. Still a pseudo-element rather than a div: the markup
   stays one <details> with nothing extra to style, hide and remember.
   z-index 19 puts it under the panel (20) and the close control (21) and over the page. */
.drawer[open]::before { content: ""; position: fixed; inset: 0; z-index: 19;
  background: rgba(0, 0, 0, .5);
  -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px); }
.drawer-panel { position: fixed; inset: 0 0 0 auto; width: min(360px, 88vw); z-index: 20;
  display: flex; flex-direction: column;
  background: var(--surface); border-left: 1px solid var(--border);
  box-shadow: -24px 0 60px -30px rgba(0, 0, 0, .7); }
.drawer-scroll { flex: 1; overflow-y: auto; overscroll-behavior: contain;
  /* Clears the 64px head above, plus a little air before the first heading. */
  padding: calc(64px + var(--s2)) var(--s2) var(--s2); }
.drawer-h { font-size: 12px; letter-spacing: .12em; text-transform: uppercase;
  color: var(--muted); margin: var(--s3) 0 var(--s1); padding-inline: 12px; }
.drawer-scroll > .drawer-h:first-child { margin-top: 0; }
/* Same row as .navmenu-panel a, stated separately rather than shared: that rule is inside an
   absolutely positioned dropdown and this one is not, and joining them with a comma would
   mean every future change to either had to be safe for both. */
.drawer-link { display: grid; grid-template-columns: 28px 1fr; column-gap: 10px; row-gap: 2px;
  align-items: start; padding: 11px 12px; border-radius: var(--r-ctl); color: var(--ink); }
.drawer-link .nm-ic { grid-row: span 2; width: 28px; height: 28px; border-radius: 9px;
  display: grid; place-items: center; margin-top: 1px; color: var(--accent);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 22%, transparent); }
.drawer-link:hover { background: color-mix(in srgb, var(--accent) 12%, transparent);
  text-decoration: none; }
.drawer-link:hover .nm-ic { background: color-mix(in srgb, var(--accent) 24%, transparent); }
.drawer-link strong { font-size: 14.5px; font-weight: 600; }
.drawer-link .muted { font-size: 12.5px; line-height: 1.35; }

.drawer-foot { display: flex; align-items: center; flex-wrap: wrap; gap: var(--s2);
  padding: var(--s2) var(--s3) calc(var(--s2) + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid var(--border);
  background: var(--surface-2); }
.drawer-foot .signin { color: var(--ink); font-weight: 500; font-size: 14px; }
/* Full width on its own row, which is the prominence it gets instead of .btn.primary - the
   header's action has never been primary, and one that appeared on every page in every
   drawer would outrank whatever the page itself is asking for. */
.drawer-cta { flex: 1 0 100%; justify-content: center; }

@media (prefers-reduced-motion: no-preference) {
  .drawer[open] .drawer-panel, .drawer[open] > summary { animation: drawer-in .22s ease-out; }
  .drawer[open]::before { animation: drawer-fade .22s ease-out; }
  @keyframes drawer-in { from { transform: translateX(100%); } }
  @keyframes drawer-fade { from { opacity: 0; } }
}

/* .langs lives in app.css: the signed-in sidebar carries the same switcher and loads app.css
   only, while every page that loads landing.css loads app.css too. */

/* --- hero -------------------------------------------------------------------- */
.hero { padding: 104px 0 var(--s6); position: relative; overflow: hidden; }
.hero::before {                       /* soft brand glow rising into the hero */
  content: ""; position: absolute; inset: -40% 30% auto -10%; height: 620px;
  background: radial-gradient(closest-side, color-mix(in srgb, var(--accent) 22%, transparent), transparent);
  pointer-events: none; z-index: 0;
}
/* Second, wider wash low and to the right, so the glow reads as ambient light rather than a
   single lamp. */
.hero::after {
  content: ""; position: absolute; inset: auto -20% -50% 35%; height: 520px;
  background: radial-gradient(closest-side, var(--glow-1), transparent);
  pointer-events: none; z-index: 0;
}
.hero > .wrap { position: relative; z-index: 1; }
.eyebrow { font-size: 12px; letter-spacing: .14em; text-transform: uppercase;
  color: var(--muted); margin: 0 0 var(--s2); }
/* The line-height, the padding and the matching negative margin are one fix. This text is
   painted by background-clip: text, and a clipped background only exists inside the element's
   box. At 1.02 the box was shorter than the glyphs, so descenders on the last line (the g of
   "listing") and accents above the first (the Ó and É of the Spanish headlines) were cut off.
   The padding grows the painted box past the glyphs; the negative margin takes it back out of
   the layout, so the headline sits exactly where it did. */
.hero h1 { font-size: clamp(38px, 5.4vw, 64px); font-weight: 800; line-height: 1.08;
  padding: .1em .06em .16em;
  margin: -.1em -.06em calc(var(--s3) - .16em);
  letter-spacing: -0.028em; max-width: 20ch; text-wrap: balance;  /* the <br> sets the break */
  width: fit-content;  /* box hugs the text so the bleed's 50% is the TEXT centre, not the column's */
  /* A small brand-tint bleed rising from the centre bottom - same trick as the band numerals
     but far subtler: the blue only kisses the base, ink owns the rest. */
  color: var(--ink);
  /* Bleed from the top: centre on the x-height line of the first line (top of the lowercase
     letters, ~20% down the two-line box), washing down into ink. A vertical oval (taller than
     wide) rather than a round blob, so the tint reaches down through the second line instead of
     lighting only the first. */
  background-image: radial-gradient(31% 84% at 50% 20%, var(--accent-2) 0%, var(--accent) 34%, var(--ink) 62%);
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent; }
.lede { font-size: 18px; font-weight: 300; line-height: 1.6; color: var(--ink-2);
  max-width: var(--measure); margin: 0 0 var(--s4); }
.hero .row { gap: var(--s2); margin-bottom: var(--s2); }
.small { font-size: 13px; }

/* --- data-source attribution strip ------------------------------------------- */
.sources { margin-top: var(--s5); padding-top: var(--s3);
  border-top: 1px solid var(--border); max-width: inherit; }
.sources-label { font-size: 11px; letter-spacing: .14em; text-transform: uppercase;
  color: var(--muted); margin: 0 0 var(--s2); max-width: inherit; }
/* A grid rather than a wrapped flex row: at seventeen registers the variable-width items left
   a ragged, lopsided wrap. Equal columns keep the strip balanced at any count and any width. */
.sources ul { list-style: none; display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 2px var(--s3);
  margin: 0; padding: 0; }
.sources li { display: flex; align-items: center; gap: 9px; color: var(--muted);
  padding: 6px 0; }
.sources li.is-live { color: var(--ink-2); }
.sources svg { width: 21px; height: 21px; flex: none; opacity: .7; }
.sources li.is-live svg { opacity: 1; color: var(--good); }
.sources li span { display: flex; flex-direction: column; line-height: 1.25; }
.sources strong { font-size: 13px; font-weight: 650; letter-spacing: .04em; color: inherit; }
.sources em { font-style: normal; font-size: 11.5px; color: var(--muted); }
/* margin-left auto pushes every chip to its column's right edge, so the chips form a straight
   vertical line down each column instead of trailing the ragged text widths. */
.sources .tag { display: inline-block; align-self: center; margin-left: auto;
  font-size: 10px; font-weight: 600;
  letter-spacing: .08em; text-transform: uppercase; color: var(--good);
  border: 1px solid color-mix(in srgb, var(--good) 45%, transparent);
  border-radius: 999px; padding: 1px 7px; }
.sources-note { font-size: 11.5px; color: var(--muted); margin: var(--s2) 0 0;
  max-width: inherit; }

h2 { font-size: clamp(26px, 2.8vw, 34px); margin: 0 0 var(--s1); letter-spacing: -0.02em; }
/* Section headings that lead with an icon: flex the row so the icon is centred on the title's
   line box instead of hanging off the baseline with a hand-tuned vertical-align nudge. Scoped
   with :has(.ic) so the icon-less centred CTA heading is left alone. */
.marketing h2:has(.ic) { display: flex; align-items: center; gap: 9px; }
.marketing h2:has(.ic) > .ic { vertical-align: 0; margin: 0; }
/* Auth page titles are h1 for the document outline but wear the section-heading size:
   a sign-in form under a 54px hero-scale headline would shout at an empty room. */
.authwrap h1 { font-size: clamp(24px, 2.6vw, 30px); margin: 0 0 var(--s1);
  letter-spacing: -0.02em; }
h3 { font-size: 16px; margin: 0 0 var(--s1); letter-spacing: -0.01em; }
/* One accent word inside a heading (the reference's "…modern security landscape" move).
   Available for templates/locales that want to lift a single word to the brand hue. */
.hl { color: var(--accent-2); }

/* --- cards & grids ----------------------------------------------------------- */
/* Only a padding override now - the radius is shared from app.css's .card. Scoped to
   .marketing so it cannot leak into the app shell if the sheet is ever loaded there. */
.marketing .card { padding: var(--s3); }
/* A card that opens with its own heading. Three templates wrote style="margin-top:0" here,
   each independently, and on a marketing page all three were defending against a margin that
   was not there: app.css sets h2 { margin: 28px 0 10px } for the app shell, and the h2 above
   in this sheet already zeroes the top for anything wearing landing.css.
   It is load-bearing in the article family, which is why it is a rule and not a deletion.
   .guide h2 sets var(--s5) so that a section heading has room inside a long read, and any
   card or block heading inside that article inherits 56px of section gap for no reason -
   app.css:1004 already carries this same fix by hand for .guide .upsell > h2.
   The second selector is the plan card, where the heading follows the icon coin rather than
   opening the box; .plan-ic already supplies the gap under itself. */
.card > h2:first-child, .card > .plan-ic + h2 { margin-top: 0; }
.grid3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--s2);
  margin: var(--s3) 0 var(--s5); }
.grid3:last-child { margin-bottom: 0; }
.grid3 .card p { color: var(--ink-2); font-size: 14px; margin: 0; line-height: 1.6; }
.segments { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--s2); }
.seg h3 { margin: 0; }
.seg .pain { color: var(--muted); font-size: 14px; font-style: italic; line-height: 1.6; }
.seg p { font-size: 14px; color: var(--ink-2); line-height: 1.6; }
.price { font-weight: 600; white-space: nowrap; }
.price.big { font-family: var(--font-display); font-size: 30px; font-weight: 800;
  margin: var(--s1) 0 2px; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }
.price.big .muted { font-size: 14px; font-weight: 400; letter-spacing: 0; }

/* --- the cost of getting it wrong -------------------------------------------- */
.figures { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--s2);
  margin: var(--s3) 0 var(--s4); }
/* Glass, matching .card: a lit gradient rim (--rim), interior light pooling from the top-left
   (--card-glow) over a translucent fill (--glass-fill), a top-lit inset highlight and a soft
   ground glow. No backdrop-blur here (these run 4-5 to a row, twice - kept off to hold paint
   cost down; the depth reads from the rim, fill and highlight alone). */
.figure { position: relative; border: 1px solid transparent;
  border-radius: var(--r-card); padding: var(--s3);
  background: var(--card-glow) padding-box, var(--glass-fill) padding-box, var(--rim) border-box;
  box-shadow: inset 0 1px 0 color-mix(in srgb, white 16%, transparent),
              0 12px 32px -20px var(--glow-1); }
/* Ranges are three times the length of a single figure, and a fixed 34px broke "10.001-100.000"
   across two lines mid-number. Scaling with the column keeps a range on one line; where it
   still has to wrap, the only break opportunity inside the string is the en dash, so it breaks
   between the two numbers rather than inside one of them. */
/* White numeral with an intense colour bleed rising from the centre bottom - the penalty hue
   pools at the base and washes up to white at the top, more intense than the reference.
   A radial gradient anchored at 50% bottom does the "coming from below" read; background-clip:
   text paints it through the glyphs, and the solid colour is the fallback where clip is out. */
.figure .n { font-family: var(--font-display); font-size: clamp(22px, 2.2vw, 36px);
  font-weight: 800; line-height: 1.05; letter-spacing: -0.02em; color: var(--critical);
  width: fit-content;  /* box hugs the number so the bleed's 50% is the TEXT centre */
  /* Subtle: white numeral with a small red pool rising from the baseline centre - a hint of
     the penalty hue, not a red number. Small radius, white takes over quickly. */
  background-image: radial-gradient(80% 60% at 50% 90%, var(--critical) 8%, #fff 68%);
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent;
  text-wrap: balance; font-variant-numeric: tabular-nums; }
/* Not a penalty - the nationwide guest-register duty - so it bleeds white into brand, not red. */
.figure .n.calm { color: var(--ink);
  background-image: radial-gradient(80% 60% at 50% 90%, var(--accent-2) 8%, #fff 68%); }
.figure .k { display: flex; align-items: baseline; gap: 6px; font-size: 13px;
  font-weight: 600; color: var(--ink); margin-top: var(--s1); }
.figure .src { display: block; font-size: 12px; color: var(--muted); margin-top: 4px;
  line-height: 1.5; }
/* The caveat under the figures, naming which regions they were read from. Pulled UP a step,
   because .figures already ends on var(--s4) and a note that answers the block above it must
   sit with that block rather than float halfway to the next heading. */
.figures-note { max-width: var(--measure); margin: calc(var(--s3) * -1) 0 var(--s3); }
.enforcement { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--s2); }
.enforcement .card { border-left: 3px solid var(--serious); }
.enforcement h3 { font-size: 15px; }
.enforcement p { font-size: 14px; color: var(--ink-2); margin: 0; line-height: 1.6; }

/* --- lookup ------------------------------------------------------------------ */
/* Same small centre-bottom brand kiss as the hero title. Clip paints only the text glyphs,
   so the leading search icon (its own currentColor fill) is untouched. */
/* Make the licence-check section pop: ambient brand glows behind it, like the hero. Two soft
   radial washes (one accent, one dimmer) sit under the content, which is lifted above them. */
#check { position: relative; overflow: hidden; }
#check::before { content: ""; position: absolute; inset: auto 42% -38% -8%; height: 440px;
  background: radial-gradient(closest-side, color-mix(in srgb, var(--accent) 20%, transparent), transparent);
  pointer-events: none; z-index: 0; }
#check::after { content: ""; position: absolute; inset: -34% -10% auto 46%; height: 400px;
  background: radial-gradient(closest-side, var(--glow-1), transparent);
  pointer-events: none; z-index: 0; }
#check > .wrap { position: relative; z-index: 1; }
#check h2 { color: var(--ink); width: fit-content;  /* box hugs icon+text so 50% is its centre */
  /* Single line - baseline sits ~82% down the box; centre the pool there. */
  background-image: radial-gradient(60% 70% at 50% 82%, var(--accent-2) 0%, var(--accent) 22%, var(--ink) 50%);
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent; }
.lookup { display: flex; gap: var(--s1); max-width: 620px; margin: 0 0 var(--s1); }
/* Lit input: a brand-tinted fill with a top-lit inner highlight and a soft outer brand glow,
   so the field reads as illuminated glass rather than a flat well. */
.lookup input { font-size: 16px; padding: 12px 14px; border-radius: 10px;
  background: linear-gradient(color-mix(in srgb, var(--accent) 12%, var(--surface-2)),
                              color-mix(in srgb, var(--accent) 5%, var(--surface-2)));
  border-color: color-mix(in srgb, var(--accent) 28%, transparent);
  box-shadow: inset 0 1px 0 color-mix(in srgb, white 14%, transparent),
              0 0 28px -8px var(--glow-2); }
/* :focus-visible, not :focus. This field is the first thing on the homepage a pointer user
   clicks, and :focus painted a ring on that click - a ring the rest of the product deliberately
   withholds from the mouse (app.css:79 is :focus-visible for exactly this reason). Keyboard
   travel still lights it up; a click now just puts the caret in. */
.lookup input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px;
  box-shadow: inset 0 1px 0 color-mix(in srgb, white 18%, transparent),
              0 0 34px -6px var(--glow-2); }
.lookup .btn { white-space: nowrap; padding: 12px 20px; }

.result { margin-top: var(--s3); background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: var(--s3); max-width: 780px; }
.band .result { background: var(--surface-2); }

/* Small print closing a section: the sources line, a hub disclaimer, a caveat under a grid.
   Capped at the measure on purpose - it is prose, and prose set to a 900px container is a line
   length nobody finishes. The top margin is a step rather than a paragraph gap because what it
   follows is a grid, and a grid ends flush. */
.sec-note { margin-top: var(--s3); max-width: var(--measure); }

/* The portfolio audit, sitting at the foot of the check band. A rule and a step of space
   separate it from the lookup without starting a second surface. */
.portfolio-cta { margin: var(--s4) 0 0; padding: var(--s4) 0 0;
  border-top: 1px solid var(--border); }
.portfolio-cta h2 { margin-top: 0; }
.result.found { border-left: 3px solid var(--good); }
.result.not_found { border-left: 3px solid var(--critical); }
.result.out_of_region { border-left: 3px solid var(--warning); }
.result.unavailable { border-left: 3px solid var(--warning); }
.result table { margin-top: var(--s2); }
.result th { width: 130px; text-transform: none; letter-spacing: 0; font-size: 13px;
  padding: 7px 12px 7px 0; border: 0; color: var(--muted); }
.result td { padding: 7px 0; border: 0; font-size: 14px; }
.flag { margin-top: var(--s2); padding: var(--s2); border-radius: 10px;
  border: 1px solid var(--border); }
/* The flag is a line of copy and a button under it. A paragraph's own bottom margin is
   bigger than the gap wanted here and its last one would push the button off the padding. */
.flag p { margin: 0 0 10px; }
.flag p:last-child { margin-bottom: 0; }

/* --- closing ----------------------------------------------------------------- */
.cta { text-align: center; }
.cta h2 { max-width: 20ch; margin-inline: auto; }
.cta .sub { margin-inline: auto; }
/* ============ site footer ================================================ */
footer.site { background: var(--surface); border-top: 1px solid var(--border);
  padding-block: var(--s5) var(--s4); margin-top: var(--s6); }
.foot-grid { display: grid; grid-template-columns: 2fr 1fr 1fr 1.4fr; gap: var(--s4) var(--s3);
  padding-bottom: var(--s4); border-bottom: 1px solid var(--rule); }
.foot-brand .brandmark { margin-bottom: var(--s2); }
.byline { font-size: 13px; color: var(--ink-2); line-height: 1.6; margin: 0 0 var(--s3);
  max-width: 34ch; }
.socials { display: flex; gap: 8px; }
.socials a { display: grid; place-items: center; width: 34px; height: 34px; border-radius: 9px;
  border: 1px solid var(--border); color: var(--ink-2); }
.socials a:hover { color: var(--ink); border-color: var(--muted); text-decoration: none; }
.foot-col h3 { font-size: 11px; letter-spacing: .12em; text-transform: uppercase;
  color: var(--muted); margin: 0 0 var(--s2); font-weight: 650; }
.foot-col a { display: block; font-size: 14px; color: var(--ink-2); padding: 4px 0; }
.foot-col a:hover { color: var(--ink); text-decoration: none; }
.foot-contact p { display: flex; align-items: flex-start; gap: 8px; font-size: 13.5px;
  color: var(--ink-2); margin: 0 0 var(--s2); line-height: 1.55; }
/* The glyph centers on the FIRST line of text (so the address pin rides line one, not the
   middle of the block). calc keeps it centered whatever font-size the row inherits:
   half of (one 1.55 line-box minus the 15px icon). */
.foot-contact p .ic { color: var(--muted); flex: none;
  margin-top: calc((1.55em - 15px) / 2); }
/* The email anchor is a contact row, not a nav row: undo .foot-col a's block+padding+14px
   (its 4px top padding pushed the text down while the icon stayed put - the misalignment
   was never the icon). */
.foot-contact p a { display: inline; font-size: inherit; padding: 0; }
.foot-contact a.btn { display: inline-block; margin-top: 4px; }
/* Public search: the bar and its typeahead dropdown (search.js). The form is the
   positioning context so the listbox hugs the input at any width. */
.searchbar { display: flex; gap: 8px; margin-top: 14px; }
.searchbar input { flex: 1; }
/* The general-discovery search on the homepage, deliberately narrower than the licence lookup
   above it (620px) so that the two never read as a pair of equal offers - the exact check is
   the page's job and this one is the way out when the exact check is not what you have. A
   compound selector, not a bare .narrow, because .guide.narrow below is a different width for
   a different reason and the two must not be one name. */
.searchbar.narrow { max-width: 560px; }
.search-host { position: relative; }
.search-suggest { position: absolute; top: calc(100% + 6px); left: 0; right: 0; z-index: 40;
  background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
  box-shadow: 0 12px 32px rgba(0,0,0,.4); overflow: hidden; }
.search-opt { display: block; padding: 9px 12px; color: var(--ink-2);
  border-bottom: 1px solid var(--border); }
.search-opt:last-child { border-bottom: 0; }
.search-opt:hover, .search-opt.is-active { background: color-mix(in srgb, var(--accent) 12%, transparent);
  text-decoration: none; color: var(--ink); }
.search-opt strong { display: inline; }
.search-kind { float: right; font-size: 11px; letter-spacing: .08em; text-transform: uppercase;
  color: var(--muted); margin-left: 8px; }
.search-snip { display: block; font-size: 12.5px; color: var(--muted); margin-top: 2px; }

/* Cookie consent (rendered only when LICENCEDESK_CONSENT_BANNER=1). Fixed bottom sheet;
   accept and reject share the primary style on purpose - the AEPD guide requires rejecting
   to be as easy as accepting. */
.cookie-banner { position: fixed; inset: auto 16px 16px 16px; z-index: 60; margin: 0 auto;
  max-width: 720px; background: var(--surface); border: 1px solid var(--border);
  border-radius: 12px; padding: 16px 18px; box-shadow: 0 12px 40px rgba(0,0,0,.45);
  font-size: 14px; color: var(--ink-2); }
.cookie-banner p { margin: 0 0 12px; line-height: 1.5; }
.cc-actions { display: flex; gap: 10px; flex-wrap: wrap; }
.cc-row { display: flex; gap: 9px; align-items: flex-start; margin: 0 0 10px;
  font-size: 13.5px; line-height: 1.45; }
/* Size and accent come from the shared checkbox rule in app.css; only the optical nudge onto
   the first line of the row's text belongs here. */
.cc-row input { margin: 3px 0 0; }

.foot-legal { padding-top: var(--s3); }
.foot-legal p { font-size: 12px; color: var(--muted); line-height: 1.65; margin: 0 0 6px;
  max-width: 90ch; }
.foot-legal strong { color: var(--ink-2); }

@media (max-width: 900px) {
  .foot-grid { grid-template-columns: 1fr 1fr; }
  .foot-brand { grid-column: 1 / -1; }
}

@media (max-width: 900px) {
  :root { --s6: 60px; --s5: 40px; }
  .hero { padding: 64px 0 var(--s6); }
  .figures { grid-template-columns: repeat(2, 1fr); }
  .grid3, .segments, .enforcement, .more-grid { grid-template-columns: 1fr; }
  /* The whole desktop nav stands down and the drawer takes over. It used to hide only the
     section links and keep the menu, the language switch and the button, which is how the
     header came to overflow a 390px viewport - four survivors competing for 230px. Now the
     bar is a logo and a hamburger at every width below this, so it cannot overflow at all,
     and everything it stopped showing is in the drawer rather than gone. */
  .topbar nav { display: none; }
  .drawer { display: block; }
  /* The header's glass has to go while the drawer exists, and this is not a taste decision.
     An element with a backdrop-filter becomes the containing block for its position: fixed
     descendants, so the drawer's panel - fixed, inset 0, meant to be the height of the screen
     - was laid out inside a 64px header and came out 64px tall: a sheet showing its first
     heading and its foot, with all thirteen links scrolled out of a strip the height of the
     bar it hangs from. Measured, not guessed: panel height 64 with the filter, 844 without,
     on an 844px viewport.
     Removing it below 900px rather than moving the drawer out of the header keeps the markup
     as one <details> - summary and panel have to share an element - and costs only the blur,
     on the widths where there is least behind the bar to blur. */
  .top { -webkit-backdrop-filter: none; backdrop-filter: none;
         background: var(--surface); }
  .lookup { flex-direction: column; }
}

/* A phone is narrower than the 900px block above assumes, and the header did not fit in it.
   Measured at a true 390px viewport (Emulation.setDeviceMetricsOverride, not a clamped
   --window-size, which silently lays out at ~500 and then crops the picture - which is why
   this survived so long): logo 116 + menu 84 + language switch 82 + Acceder 55 + the button
   71 came to 334px of content in the 230px the wrap had left after the logo. The button was
   the item that gave: it wrapped "Auditar mi cartera" onto three lines, stood 71px tall
   inside a 64px header, and finished at x=472 - eighty-two pixels past the right edge of the
   screen. Every marketing and article page in both languages scrolled sideways, and the
   header's own call to action was the part hanging off the screen.

   The button is what goes, rather than the language switch or Acceder, because it is the only
   one of the four whose job is now done twice. Before this page standard every page carried
   its action in the header or nowhere; now every page in both families ends on the shared
   close block with a primary button, and most of them carry the same destination there that
   the header was pointing at. Dropping .langs would strand a visitor in the wrong language
   with no way back (the reason the 900px block above keeps it), and dropping .signin would
   leave a phone with no route to sign in at all - the footer has no sign-in link either.

   The tighter gap is what buys the alignment: at var(--s2) the three survivors still ended at
   379px against a 368px gutter, so the header sat eleven pixels wider than every other
   section on the page. At 10px they land inside it. */
/* The 560px block that used to live here hid the header button, which was the narrowest fix
   available while the bar still had to hold four things. The drawer removed the problem
   rather than the button: below 900px the bar holds a logo and a hamburger, so there is
   nothing left to drop and the button is back - in the drawer's sticky foot, where it has a
   full row to itself instead of 71px of a 230px gutter. */

.coverage-count { font-size: 14px; color: var(--ink-2); max-width: var(--measure);
  margin: calc(var(--s4) * -1 + var(--s2)) 0 var(--s3); padding-left: var(--s2);
  border-left: 2px solid var(--accent); }

/* .ic / .badge / .b-* consolidated into app.css - see the note at the top of this file. */

/* ============ hero: copy + live alert stack ============================== */
.hero-grid { display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(0, .95fr);
  gap: var(--s5); align-items: start; }
.hero-grid .sources { grid-column: 1 / -1; }
.hero-copy .lede, .hero-copy h1 { max-width: none; }

.alertstack { background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-tile); padding: var(--s3);
  box-shadow: inset 0 1px 0 var(--edge), 0 16px 48px -20px var(--glow-2),
              0 2px 8px -2px var(--glow-1);
  margin-top: 34px; }
.as-head { display: flex; align-items: center; gap: 8px; font-size: 12px; font-weight: 650;
  letter-spacing: .06em; text-transform: uppercase; color: var(--muted);
  padding-bottom: var(--s2); border-bottom: 1px solid var(--rule); margin-bottom: var(--s2); }
.as-head .ic { color: var(--accent); }
.as-count { margin-left: auto; background: var(--critical); color: #fff; font-size: 11px;
  border-radius: 999px; padding: 1px 8px; letter-spacing: 0; }
.as-card { border-radius: 10px; padding: 11px 13px; background: var(--plane);
  border: 1px solid var(--border); border-left-width: 3px;
  position: relative; overflow: hidden; }
/* Gaps come from the sibling pair, not a trailing margin cancelled by :last-child. The burst
   appends nodes to this panel, which stopped the last card matching :last-child - its 9px
   margin came back for the length of the explosion and the whole panel grew, then shrank
   again when the debris was cleaned up. Visible as a jump on mobile. Sibling margins cannot
   be disturbed by anything appended after the list. */
.as-card + .as-card { margin-top: 9px; }

/* ---- the panel works its queue (sequenced by landing.js) --------------------------------
   Resolved: a check pops in on the right and the card cools to the "handled" green.
   Vanish: the content dissolves with the atom swarm - opacity only, the box keeps its place,
   so the list never changes height and nothing else on the page moves.
   Entering: the next issue slides into the same slot, decelerating into place.
   Nothing runs under prefers-reduced-motion - the script never starts. */
.as-card { transition: transform .38s cubic-bezier(0.23, 1, 0.32, 1),
                       opacity .3s cubic-bezier(0.23, 1, 0.32, 1),
                       border-color .25s ease; }
.as-card.is-resolved { border-left-color: var(--good); }
/* the check lands where the timestamp sits; the timestamp yields */
.as-card .as-when { transition: opacity .2s ease; }
.as-card.is-resolved .as-when { opacity: 0; }
.as-card.is-resolved::after { content: "\2713"; position: absolute; top: 9px; right: 11px;
  width: 20px; height: 20px; border-radius: 50%; display: grid; place-items: center;
  font-size: 12px; font-weight: 700; color: var(--good);
  border: 1.5px solid color-mix(in srgb, var(--good) 55%, transparent);
  background: color-mix(in srgb, var(--good) 14%, transparent);
  animation: check-pop .3s cubic-bezier(0.23, 1, 0.32, 1) backwards; }
@keyframes check-pop { from { opacity: 0; transform: scale(0.6); } }
/* Detonation: a quick bulge with a flash of light, then blown out - the atoms (spawned the
   same instant by landing.js) carry the debris. forwards holds the card invisible until the
   next issue slides in. */
.as-card.is-vanish { animation: card-detonate .34s cubic-bezier(0.23, 1, 0.32, 1) forwards; }
@keyframes card-detonate {
  0%   { opacity: 1; transform: scale(1); }
  28%  { opacity: 1; transform: scale(1.035);
         box-shadow: 0 0 0 1px color-mix(in srgb, white 35%, transparent),
                     0 0 26px -4px color-mix(in srgb, white 30%, transparent); }
  100% { opacity: 0; transform: scale(0.94); }
}
/* Arrival: the next issue rises into the empty slot, unhurried - a long decelerating slide up
   from below, copy fading in behind the movement. An animation rather than a transition so the
   card can hand off directly from the detonation's held state without a frame of flash. */
.as-card.is-arriving { animation: card-arrive .72s cubic-bezier(0.23, 1, 0.32, 1) backwards; }
@keyframes card-arrive {
  0%   { opacity: 0; transform: translateY(20px); }
  45%  { opacity: .55; }
  100% { opacity: 1; transform: translateY(0); }
}
/* The all-clear tally acknowledges each resolved property: the count line swells and flashes
   green as the number ticks up, timed to the row floating out above it. */
.as-card.is-counting strong { animation: tally-tick .55s cubic-bezier(0.23, 1, 0.32, 1); }
@keyframes tally-tick {
  30% { color: var(--good); transform: scale(1.045); }
  100% { color: inherit; transform: none; }
}
.as-card.good strong { transform-origin: left center; will-change: transform;
  position: relative; }             /* particles position against the count line */
.as-card.good .as-n { color: var(--good); font-variant-numeric: tabular-nums; }

/* The capture swarm: the resolved card dissolves into atoms of its own severity colour
   (--c0, read from the card's edge by landing.js), and each atom flies its own path (--tx/--ty)
   to the tally's digits, turning green on the way and landing small. Parented to the panel -
   the cards clip overflow for the collapse animation, and would eat the atoms. Per-atom delay
   and duration arrive inline; backwards fill holds the atom at its origin colour through its
   delay so late starters don't flash green early. */
.alertstack { position: relative; }
/* The debris lives in its own out-of-flow layer rather than among the cards: particles are
   never siblings of the list, so no structural selector (:last-child, :nth-child, +) can be
   thrown off by an explosion in progress. */
.burst-layer { position: absolute; inset: 0; pointer-events: none; z-index: 3; }
.burst-p { position: absolute; z-index: 3; border-radius: 50%;
  /* Defaults so the tokens are defined in-sheet (the brand guard checks); landing.js sets the
     real per-atom values inline - severity colour, scatter kick and flight vector. */
  --c0: var(--good); --sx: 0px; --sy: 0px; --tx: 0px; --ty: 0px;
  background: var(--c0);
  box-shadow: 0 0 5px var(--c0);
  pointer-events: none; transform: translate(-50%, -50%);
  animation: burst-travel .75s linear both; }
/* Two-legged flight with its own physics per leg (timing functions live IN the keyframes):
   detonation - a hard ease-out kick outward from the card's centre; then the pull - the debris
   accelerates into the digits, turning green on the way and landing small. */
@keyframes burst-travel {
  0%   { opacity: 1; transform: translate(-50%, -50%) scale(1);
         background: var(--c0); box-shadow: 0 0 5px var(--c0);
         animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); }
  34%  { opacity: 1;
         transform: translate(calc(-50% + var(--sx)), calc(-50% + var(--sy))) scale(1);
         background: var(--c0); box-shadow: 0 0 5px var(--c0);
         animation-timing-function: cubic-bezier(0.6, 0, 0.9, 0.6); }
  78%  { background: var(--good); box-shadow: 0 0 7px var(--good); opacity: 1; }
  100% { opacity: 0; background: var(--good);
         transform: translate(calc(-50% + var(--tx)), calc(-50% + var(--ty))) scale(0.2); }
}
.as-card.critical { border-left-color: var(--critical); }
.as-card.warning  { border-left-color: var(--warning); }
.as-card.good     { border-left-color: var(--good); }
.as-top { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
.as-when { margin-left: auto; font-size: 11px; color: var(--muted); }
.as-card strong { display: block; font-size: 14px; line-height: 1.35; }
.as-sub { display: block; font-size: 12px; color: var(--muted); margin-top: 3px; line-height: 1.45; }

/* ============ coverage: live cards + folded pending ====================== */
.cov-live { display: grid; grid-template-columns: repeat(auto-fit, minmax(215px, 1fr));
  gap: var(--s2); margin-bottom: var(--s2); }
/* Glass like .figure/.card; the green "live" edge moves from a solid border-left (which the
   gradient rim would paint over) to an inset shadow bar that sits inside the radius. */
.cov-card { position: relative; border: 1px solid transparent; border-radius: var(--radius);
  padding: var(--s2) var(--s3);
  background: var(--card-glow) padding-box, var(--glass-fill) padding-box, var(--rim) border-box;
  box-shadow: inset 3px 0 0 var(--good),
              inset 0 1px 0 color-mix(in srgb, white 16%, transparent),
              0 12px 32px -20px var(--glow-1); }
.cov-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.cov-head strong { font-size: 15px; }
.cov-codes { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px;
  color: var(--ink-2); margin-top: 7px; }
.cov-reg { font-size: 12px; color: var(--muted); margin-top: 3px; line-height: 1.45; }

.cov-pending { background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); margin-bottom: var(--s3); }
.cov-pending summary { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap;
  padding: 13px var(--s3); cursor: pointer; list-style: none; }
.cov-pending summary::-webkit-details-marker { display: none; }
.cp-left { display: flex; align-items: center; gap: 9px; font-size: 14px; }
.cp-left .ic { color: var(--muted); }
.cp-tags { display: flex; gap: 6px; flex-wrap: wrap; margin-left: auto; }
.cp-tag { font-size: 11px; font-weight: 600; padding: 2px 9px; border-radius: 999px;
  border: 1px solid var(--border); color: var(--ink-2); white-space: nowrap; }
.cp-tag.s-next { color: var(--warning); border-color: color-mix(in srgb, var(--warning) 34%, transparent); }
.cp-chev { display: inline-flex; color: var(--muted); transition: transform .18s ease; }
.cov-pending[open] .cp-chev { transform: rotate(90deg); }
.cov-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(228px, 1fr));
  gap: 8px; padding: 0 var(--s3) var(--s3); }
.cov-chip { display: flex; align-items: center; gap: 9px; padding: 9px 12px;
  background: var(--plane); border: 1px solid var(--border); border-radius: 10px; }
.cc-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--muted); flex: none; }
.cov-chip.s-next .cc-dot { background: var(--warning); }
.cov-chip.s-identified .cc-dot { background: var(--serious); }
/* The why, inside the coverage fold. It sits under the list, INSIDE the panel, so it takes
   the panel's own inline padding rather than a margin - a margin here would have left a
   stripe of panel background beside the text. */
.fold-note { padding: 0 var(--s3) var(--s3); margin: 0; }
.cc-name { font-size: 13.5px; }
.cc-status { margin-left: auto; font-size: 11px; color: var(--muted); text-align: right; }

/* ============ pricing cards ============================================== */
.plan { display: flex; flex-direction: column; }
.plan .notes { flex: 1; }
/* A plan card standing on its own rather than sitting in a grid of them: the founding offer
   under the homepage prices, the free tier under the licence solution, the first-pass audit
   above the product plans. In a grid the gap does the spacing; alone it has to bring its own,
   and all three templates were writing it inline - two as a top margin, one as a bottom one,
   because one of the three sits ABOVE its grid and the other two below. Symmetric here rather
   than three variants: whichever side faces the grid collapses against the grid's own larger
   margin, and the homepage's card is a last-child, where section.wrap > :last-child already
   zeroes the bottom. One class for all three, and nothing moves by more than a pixel. */
.plan-standalone { margin-block: var(--s2); }
/* The one that is being pushed. A left rule in the accent is the lightest way to mark a card
   as the offer without giving it a second fill the grid beside it does not have. */
.plan-standalone.featured { border-left: 3px solid var(--accent); }
/* The descriptive line in a plan card that has no feature list - it does the job .plan .notes
   does above, which is to absorb the slack so the buttons on every card in the row land on one
   line. Not folded into `.plan p.muted.small`: the real plan card has one of those too, ABOVE
   its .notes, and giving both flex:1 would split the slack between them and unalign the row. */
.plan-body { flex: 1; }
/* The body copy in a .spread row - a headline strong on one line, this under it, a button
   pinned to the right. 62ch rather than the measure because it shares the row with the button
   and must give way to it; the small top margin is a caption's gap, not a paragraph's. Three
   templates carried it inline, character for character. */
.spread-body { margin: 6px 0 0; max-width: 62ch; }
.plan-ic { width: 44px; height: 44px; border-radius: var(--r-ctl); display: grid; place-items: center;
  border: 1px solid transparent;
  background:
    radial-gradient(120% 120% at 30% 20%, color-mix(in srgb, var(--accent) 42%, transparent), transparent 60%) padding-box,
    var(--glass-fill) padding-box,
    linear-gradient(135deg, color-mix(in srgb, white 34%, transparent), color-mix(in srgb, var(--accent) 30%, transparent)) border-box;
  box-shadow: inset 0 1px 0 color-mix(in srgb, white 28%, transparent), 0 4px 14px -7px var(--glow-1);
  color: var(--accent-2); margin-bottom: var(--s2); }
.plan-cta { margin-top: var(--s3); text-align: center; }
/* Two products, two columns - a third of the width each would leave a hole where the card
   that does not exist would go. Collapses with the rest of the grids on mobile. */
.more-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--s2);
  margin: var(--s3) 0 0; }
/* Its own query, after the rule: the shared 900px block sits higher up the file, so at equal
   specificity the two columns above used to win and a phone got two clipped cards. */
@media (max-width: 900px) { .more-grid { grid-template-columns: 1fr; } }
/* A whole card that is one link. The inner .btn is a span, so it must not swallow the click
   or read as a second control to a screen reader - the card's own text is the link name. */
.more-card { text-decoration: none; color: inherit; transition: border-color .15s ease; }
/* app.css underlines every a:hover. On a card-sized link that drags a rule under three
   separate blocks of text, so the hover is carried by the border and by weight instead. */
.more-card:hover, .more-card:hover * { text-decoration: none; }
.more-card:hover { border-color: color-mix(in srgb, var(--accent) 55%, var(--border)); }
.more-card:hover .muted { color: var(--ink-2); font-weight: 500; }
.more-card:hover .plan-cta { font-weight: 600; color: var(--ink);
  border-color: color-mix(in srgb, var(--accent) 55%, var(--border)); }
.more-card h3 { margin: 0 0 6px; font-size: 17px; }
.more-card .muted { flex: 1; margin: 0; font-size: 14px; transition: font-weight .12s ease; }
/* Each product gets its own row of sizes, so the heading has to separate them without
   shouting - the products differ in kind, the cards under each differ only in size. */
/* A heading sitting directly on top of a grid. h3's own 8px is the gap between a heading and
   the paragraph it introduces; over a row of cards it is too tight to read as a separation at
   all, and the heading looks like it has slipped down into the first card. */
.grid-h { margin-bottom: var(--s3); }
.product-h { margin: var(--s4) 0 var(--s2); display: flex; align-items: baseline;
  gap: var(--s2); flex-wrap: wrap; }
.product-h .muted { font-weight: 400; }

/* ============ per-section call to action ================================= */
.sec-cta { margin-top: var(--s4); }
.sec-cta .btn { display: inline-flex; align-items: center; gap: 8px; }
.sec-cta .btn .ic { vertical-align: 0; }

@media (max-width: 900px) {
  .hero-grid { grid-template-columns: 1fr; gap: var(--s4); }
  .alertstack { margin-top: var(--s2); }
  .cp-tags { margin-left: 0; }
}

/* ============ hero artwork: the energy label in depth ==================== */
/* Three planes, so a tilt reads as depth rather than a flat picture turning: the big house at
   the back, its A-G bars standing proud of it, the six smaller houses in front. Each SVG also
   draws its own extrusion, so the depth survives prefers-reduced-motion, where nothing moves
   and the scene simply rests at its angle. The pointer tilt (--ela-rx/--ela-ry, hero-art.js)
   sits on a wrapper of its own so it adds to the idle sway instead of fighting it for the one
   transform property. */
.hero.has-art .hero-grid { grid-template-columns: minmax(0, 1.12fr) minmax(0, .88fr);
  align-items: center; }
.hero.has-art h1 { font-size: clamp(36px, 4.3vw, 56px); }
.ela { position: relative; width: 100%; max-width: 470px; justify-self: end;
  perspective: 1300px; perspective-origin: 40% 30%; }
/* A faint green pool under the scene - the good token, not another brand glow. */
.ela::before { content: ""; position: absolute; inset: 12% -8% -10%; z-index: -1;
  pointer-events: none;
  background: radial-gradient(closest-side, color-mix(in srgb, var(--good) 16%, transparent), transparent); }
.ela svg { display: block; width: 100%; height: auto; overflow: visible; }
.ela .ela-defs { position: absolute; width: 0; height: 0; overflow: hidden; }
.ela-tilt, .ela-scene, .ela-main, .ela-row, .ela-mini { transform-style: preserve-3d; }
.ela-tilt { transform: rotateX(var(--art-rx, 0deg)) rotateY(var(--art-ry, 0deg));
  transition: transform .7s cubic-bezier(0.23, 1, 0.32, 1); }
.ela-scene { position: relative; transform: rotateX(9deg) rotateY(-15deg); }
.ela-main { position: relative; }
.ela-house { filter: drop-shadow(0 24px 28px rgb(0 0 0 / .5)); }
.ela .ela-bars { position: absolute; inset: 0; transform: translateZ(48px);
  filter: drop-shadow(0 12px 14px rgb(0 0 0 / .4)); }
.ela-row { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px 16px;
  margin-top: 14px; }
.ela-mini { transform: translateZ(calc(34px + var(--row, 0) * 30px)); }
.ela-mini svg { filter: drop-shadow(0 14px 16px rgb(0 0 0 / .45)); }
.ela text { font-family: var(--font-display); font-weight: 800; }
.ela-big { font-size: 124px; letter-spacing: -.02em; }
.ela-bl { font-size: 17px; }
.ela-hs { stroke-width: 8px; stroke-linejoin: round; }
.ela-bar { transform-box: fill-box; transform-origin: 0 50%; }
.ela-sheen { opacity: 0; }

@media (prefers-reduced-motion: no-preference) {
  .ela-scene { animation: ela-sway 14s ease-in-out infinite; will-change: transform; }
  .ela-house { animation: ela-rise .9s cubic-bezier(0.23, 1, 0.32, 1) both; }
  /* The scale fills in from A down to G, each bar a beat after the one above. */
  .ela-bar { animation: ela-grow .75s cubic-bezier(0.23, 1, 0.32, 1) both;
    animation-delay: calc(.3s + var(--i, 0) * 70ms); }
  /* Two animations on different properties: the entrance moves `translate`, the idle bob
     moves `transform`, so neither cancels the other. Negative bob delays put the six houses
     out of step from the first frame. */
  .ela-mini svg { animation: ela-rise .8s cubic-bezier(0.23, 1, 0.32, 1) both,
                             ela-bob 7s ease-in-out infinite;
    animation-delay: calc(.75s + var(--i, 0) * 90ms), calc(var(--i, 0) * -1.15s); }
  .ela-sheen { animation: ela-sheen 6.5s ease-in-out 1.6s infinite; }
}
@keyframes ela-sway {
  0%, 100% { transform: rotateX(9deg) rotateY(-15deg); }
  50%      { transform: rotateX(5deg) rotateY(-5deg); }
}
@keyframes ela-rise { from { opacity: 0; translate: 0 24px; } }
@keyframes ela-grow { from { opacity: 0; transform: scaleX(0); } }
@keyframes ela-bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-7px); } }
@keyframes ela-sheen {
  0%        { opacity: 0; transform: translateX(0) skewX(-18deg); }
  4%, 24%   { opacity: 1; }
  30%, 100% { opacity: 0; transform: translateX(560px) skewX(-18deg); }
}
/* After the rules above, not in the shared 900px block higher up: at equal or lower
   specificity an earlier query loses the cascade. */
@media (max-width: 900px) {
  .hero.has-art .hero-grid { grid-template-columns: 1fr; }
  .ela { justify-self: center; max-width: 380px; }
}

/* ============ hero artwork: a week of nightly checks ===================== */
/* Seven cards, one per night, climbing back into the picture: tonight nearest and lowest, a
   week ago highest and smallest. Depth doubles as a calendar, which is how this says "down
   since" without a word - the run of drained cards is the answer and the dots count it again.

   The timeline turns (listings-art.js): when the story says the advert answered again,
   tonight's card takes the all-clear in place, then every card steps back one slot, the
   oldest is recycled to the front as the new night, and the dots shift with it. A slot is
   just --i, so that movement is one transition on one transform rather than seven keyframes.

   Rectangles, so plain elements rather than SVG: they take --surface/--border and the status
   tokens like everything else. Under prefers-reduced-motion the script never starts and the
   fan rests on the state the template rendered. */
.lw { position: relative; width: 100%; max-width: 500px; justify-self: end;
  perspective: 1500px; perspective-origin: 64% 46%;
  /* The night's check takes this long, and the verdict waits exactly that long to land. */
  --lw-glint: .85s; }
.lw-tilt, .lw-scene, .lw-stage { transform-style: preserve-3d; }
.lw-tilt { transform: rotateX(var(--art-rx, 0deg)) rotateY(var(--art-ry, 0deg));
  transition: transform .7s cubic-bezier(0.23, 1, 0.32, 1); }
.lw-scene { transform: rotateX(6deg) rotateY(12deg); }
/* Tall: the fan climbs the column beside the copy instead of sitting in a short wide band. */
.lw-stage { position: relative; aspect-ratio: 10 / 11; }
/* One night's advert, portrait like the listing it stands for: picture, two lines, a status.
   Opacity falls with depth, so the week recedes instead of stacking into a wall, and the step
   is wide enough that every night keeps a face - stacked tighter, the six behind tonight
   collapsed into slivers and the week stopped being countable. */
.lw-card { position: absolute; right: 2%; bottom: 14%; width: 42%; aspect-ratio: 4 / 5;
  display: flex; flex-direction: column; gap: 7px; padding: 10px;
  border-radius: var(--r-card); border: 1px solid var(--border); background: var(--surface-2);
  box-shadow: inset 0 1px 0 var(--edge), 0 20px 38px -20px rgb(0 0 0 / .78);
  opacity: calc(1 - .07 * var(--i, 0));
  transform: translate3d(calc(var(--i, 0) * -16%), calc(var(--i, 0) * -11%),
                         calc(var(--i, 0) * -34px));
  transition: transform .9s cubic-bezier(0.23, 1, 0.32, 1), opacity .6s ease,
              border-color .45s ease, background-color .45s ease, box-shadow .45s ease; }
.lw-shot { flex: 1; border-radius: var(--r-ctl); transition: background .45s ease;
  background: linear-gradient(150deg, color-mix(in srgb, var(--accent) 32%, var(--surface)),
                                      color-mix(in srgb, var(--accent) 7%, var(--surface))); }
.lw-line { height: 6px; border-radius: var(--r-pill); transition: background .45s ease;
  background: color-mix(in srgb, var(--ink-3) 42%, transparent); }
.lw-line.is-short { width: 55%; }
.lw-foot { display: flex; align-items: center; gap: 7px; }
.lw-dot { width: 10px; height: 10px; border-radius: 50%; flex: none; background: var(--good);
  transition: background .35s ease; }
.lw-chip { height: 6px; flex: 1; border-radius: var(--r-pill); transition: background .35s ease;
  background: color-mix(in srgb, var(--good) 38%, transparent); }
.lw-card.is-warn .lw-dot { background: var(--warning); }
.lw-card.is-warn .lw-chip { background: color-mix(in srgb, var(--warning) 40%, transparent); }
/* Down: the advert is still out there, it just does not answer - so the card keeps its shape
   and its content goes dead, the picture flattening and the lines dimming, while the status
   marker keeps full strength. */
.lw-card.is-down, .lw-card.is-break {
  background: color-mix(in srgb, var(--surface-2) 82%, black); }
.lw-card.is-down .lw-shot, .lw-card.is-break .lw-shot {
  background: linear-gradient(150deg, color-mix(in srgb, var(--ink-3) 15%, transparent),
                                      color-mix(in srgb, var(--ink-3) 5%, transparent)); }
.lw-card.is-down .lw-line, .lw-card.is-break .lw-line {
  background: color-mix(in srgb, var(--ink-3) 20%, transparent); }
.lw-card.is-down .lw-dot, .lw-card.is-break .lw-dot { background: var(--critical); }
.lw-card.is-down .lw-chip, .lw-card.is-break .lw-chip {
  background: color-mix(in srgb, var(--critical) 40%, transparent); }
/* The night it went down: the only red edge in the stack, and the thing the eye should find. */
.lw-card.is-break { border-color: color-mix(in srgb, var(--critical) 62%, var(--border));
  box-shadow: inset 0 1px 0 var(--edge),
              0 0 0 1px color-mix(in srgb, var(--critical) 34%, transparent),
              0 16px 44px -14px color-mix(in srgb, var(--critical) 55%, transparent); }
/* The pass over tonight's card. It runs once per night, when the check happens - the script
   puts .is-checking on the card that has just taken the front slot - and the verdict below
   waits for it to finish. */
.lw-card::after { content: ""; position: absolute; inset: -1px; border-radius: inherit;
  pointer-events: none; opacity: 0;
  background: linear-gradient(118deg, transparent 36%,
              color-mix(in srgb, white 20%, transparent) 50%, transparent 64%); }
/* Tonight's verdict, and only tonight's, and only once the glint has passed over the card:
   the check happens, then the answer. When the card at the front is green - whether it has been
   green all week or has just come back - it takes the green edge and the tick. Both leave with
   the front slot, so the mark reads as "this is the night that passed" rather than a badge a
   card keeps and carries into the distance. A front card that is down or amber shows neither,
   because nothing passed. */
.lw-card.is-front.is-ok { border-color: color-mix(in srgb, var(--good) 60%, var(--border));
  box-shadow: inset 0 1px 0 var(--edge),
              0 0 0 1px color-mix(in srgb, var(--good) 32%, transparent),
              0 18px 44px -14px color-mix(in srgb, var(--good) 48%, transparent); }
.lw-card.is-front.is-ok::before { content: "\2713"; position: absolute; top: 10px; right: 12px;
  width: 22px; height: 22px; border-radius: 50%; display: grid; place-items: center;
  font-size: 13px; font-weight: 700; color: var(--good); z-index: 2;
  border: 1.5px solid color-mix(in srgb, var(--good) 62%, transparent);
  background: color-mix(in srgb, var(--good) 20%, transparent); }
/* The same week counted again: one dot per night, oldest on the left. It gets a band of
   its own under the fan - level with the cards' own bottom edge the front card sat on top of
   the newest dot, which is the one that matters most. */
.lw-week { position: absolute; left: 0; right: 0; bottom: 1%; display: flex;
  justify-content: center; align-items: center; gap: 13px; transform: translateZ(40px); }
.lw-night { width: 8px; height: 8px; border-radius: 50%; flex: none;
  transition: background .4s ease, width .3s ease, height .3s ease;
  background: color-mix(in srgb, var(--good) 68%, transparent); }
.lw-night.is-warn { background: color-mix(in srgb, var(--warning) 72%, transparent); }
.lw-night.is-down { background: color-mix(in srgb, var(--critical) 68%, transparent); }
.lw-night.is-break { width: 10px; height: 10px; background: var(--critical);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--critical) 20%, transparent); }

@media (prefers-reduced-motion: no-preference) {
  .lw-scene { animation: lw-sway 16s ease-in-out infinite; will-change: transform; }
  .lw-card { animation: lw-in .7s cubic-bezier(0.23, 1, 0.32, 1) both; }
  /* One pass per night, restarted by the script whenever a card is checked. */
  .lw-card.is-checking::after { animation: lw-scan var(--lw-glint, .85s) ease-in-out; }
  /* Both wait out the glint and then land together - backwards fill holds them at the
     neutral card until the light has passed, so the verdict reads as the result of the check
     rather than as something that was already true. */
  .lw-card.is-front.is-ok {
    animation: lw-verdict .45s ease var(--lw-glint, .85s) backwards; }
  .lw-card.is-front.is-ok::before {
    animation: lw-check .45s cubic-bezier(0.23, 1, 0.32, 1) var(--lw-glint, .85s) backwards; }
}
@keyframes lw-sway {
  0%, 100% { transform: rotateX(6deg) rotateY(12deg); }
  50%      { transform: rotateX(3deg) rotateY(3deg); }
}
@keyframes lw-in { from { opacity: 0; scale: .92; } }
@keyframes lw-scan { 0% { opacity: 0; } 35% { opacity: 1; } 100% { opacity: 0; } }
/* The card as it stands before the verdict: the sheet's own border and shadow. */
@keyframes lw-verdict {
  from { border-color: var(--border);
         box-shadow: inset 0 1px 0 var(--edge), 0 20px 38px -20px rgb(0 0 0 / .78); }
}
@keyframes lw-check { from { opacity: 0; transform: scale(.6); } }

@media (max-width: 900px) {
  .lw { justify-self: center; max-width: 360px; }
}

/* ============ public forms: one way in at a time ========================= */
/* Paste, a file, or a feed address - the reader picks one and sees one. Radios inside their
   labels rather than a script: the CSP forbids inline JavaScript, and a form that needs none
   still works for somebody whose JS never arrives. :has() does the reveal, which this sheet
   already relies on for the channel and severity pickers. */
.modes { display: block; }
/* The label over the three source pickers. It is a caption for the radio row under it, not a
   paragraph, so it takes a caption's gap and none of a paragraph's top margin. */
.modes > p:first-child { margin: 0 0 var(--s1); }
.mode-picks { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px;
  margin: 0 0 var(--s2); }
.mode-pick { display: flex; gap: 10px; align-items: flex-start; margin: 0; padding: 12px;
  border: 1px solid var(--border); border-radius: var(--r-ctl); background: var(--plane);
  cursor: pointer; }
.mode-pick:has(input:checked) {
  border-color: color-mix(in srgb, var(--accent) 60%, transparent);
  background: color-mix(in srgb, var(--accent) 12%, transparent); }
.mode-pick strong { display: block; font-size: 14px; }
.mode-pick em { display: block; font-style: normal; font-size: 12px; color: var(--muted);
  line-height: 1.45; margin-top: 2px; }
/* Hidden, not absent: the panel for the unchecked options keeps its fields in the DOM so a
   half-filled textarea survives switching tabs and coming back. */
.mode-panel { display: none; }
.modes:has(#mode-paste:checked) .panel-paste,
.modes:has(#mode-csv:checked) .panel-csv,
.modes:has(#mode-feed:checked) .panel-feed { display: block; }
@media (max-width: 700px) { .mode-picks { grid-template-columns: 1fr; } }

/* ============ homepage hero: the alert panel, in depth ==================== */
/* The same family as the two product-page artworks, dialled down: this panel is real text, so
   the resting angle is a few degrees and the pointer tilt is halved (--art-max-*, read by
   hero-art.js, which falls back to its own larger figures where they are not set). The slab is
   a ::before on the panel's own 3D plane, so the box has a visible thickness along the tilted
   edge. Flattened on phones, where the panel is the content rather than a picture of it. */
.as-3d { perspective: 1500px; perspective-origin: 60% 40%;
  --art-max-ry: 6; --art-max-rx: 4; }
.as-tilt { transform-style: preserve-3d;
  transform: rotateX(var(--art-rx, 0deg)) rotateY(var(--art-ry, 0deg));
  transition: transform .7s cubic-bezier(0.23, 1, 0.32, 1); }
.as-3d .alertstack { transform-style: preserve-3d; transform: rotateX(3deg) rotateY(-7.5deg); }
/* Proud of the panel on every side and further back: at six degrees a same-size face sitting
   18px behind is hidden almost completely - under two pixels of it show - so the rim is what
   makes this read as a solid box rather than a sheet. */
.as-3d .alertstack::before { content: ""; position: absolute; inset: -6px; z-index: -1;
  border-radius: calc(var(--r-tile) + 5px); border: 1px solid var(--border);
  background: color-mix(in srgb, var(--surface) 62%, black);
  transform: translateZ(-34px); }
/* The heading lifts off the face, so the depth is visible without moving a card - the cards
   animate their own transforms as the queue is worked, and a Z offset here would fight them. */
.as-3d .as-head { transform: translateZ(16px); }

@media (prefers-reduced-motion: no-preference) {
  .as-3d .alertstack { animation: as-sway 18s ease-in-out infinite; }
}
@keyframes as-sway {
  0%, 100% { transform: rotateX(3deg) rotateY(-7.5deg); }
  50%      { transform: rotateX(1.2deg) rotateY(-2deg); }
}

@media (max-width: 900px) {
  .as-3d { perspective: none; }
  .as-tilt { transform: none; }
  .as-3d .alertstack { transform: none; animation: none; }
}

/* ============ brand mark ================================================= */
.topbar .logo { display: inline-flex; align-items: center; padding: 0; }
/* The wordmark's "LICENCE" is white, which now sits on the dark ground directly - the
   light-mode rescue plate is gone with the light theme. */
.brandmark { display: block; height: 34px; width: auto; }

/* ============ guides ===================================================== */
.guide, .guide-hub { padding-block: var(--s5) var(--s6); max-width: 900px; }
/* The two funnel pages and the municipality pages run narrower than a guide: they are a form
   and a short argument for filling it in, not a long read, and 900px of form is a form nobody
   believes is short. Was an inline max-width on the <article> in three templates. */
.guide.narrow { max-width: 760px; }
/* A list inside a warning notice, and a resume form inside one. Both were inline margins,
   both are the same thing: a notice is a box of its own, so its children space from its
   padding edge rather than inheriting the article's prose rhythm. */
.guide .notice .notes { margin-top: var(--s1); }
.guide .notice form { margin-top: 10px; }
.crumbs { font-size: 13px; color: var(--muted); margin: 0 0 var(--s2); }
.crumbs span { margin: 0 6px; }
.guide h1 { font-size: clamp(27px, 3.4vw, 38px); line-height: 1.15; letter-spacing: -0.02em;
  margin: 0 0 var(--s3); max-width: 22ch; }
.guide-hub h1 { font-size: clamp(27px, 3.4vw, 38px); margin: 0 0 var(--s2); }

/* The extract block: one direct answer, visually separated so it reads as the answer. */
.answer { display: flex; gap: 12px; align-items: flex-start;
  border: 1px solid transparent; border-radius: var(--radius); padding: var(--s3);
  margin-bottom: var(--s4);
  background: var(--card-glow) padding-box, var(--glass-fill) padding-box, var(--rim) border-box;
  box-shadow: inset 3px 0 0 var(--accent),
              inset 0 1px 0 color-mix(in srgb, white 16%, transparent),
              0 12px 32px -20px var(--glow-1); }
.answer .ic { color: var(--accent); margin-top: 2px; }
.answer p { margin: 0; font-size: 17px; line-height: 1.6; color: var(--ink); }

/* .gstats / .gstat used to live here, one call site (guide.html) and one rule set: the same
   auto-fit minmax(150px) track and the same glass recipe as .tile in app.css, differing only
   in a fixed numeral size and a .src slot for the citation. Two stat components that look the
   same is how a stat ends up styled differently on two pages by accident, so .tile absorbed
   both differences (.tile .n.sm, .tile .src) and these rules are gone. The guide renders
   .tiles / .tile .n.sm / .k / .src now.
   .figure stays, and is NOT the same thing: it is the argued stat, with a coloured bleed
   through the numeral - red for a penalty, .calm for a neutral count - and collapsing it into
   .tile would throw that meaning away to save a rule. */

.guide h2 { font-size: 21px; margin: var(--s5) 0 var(--s2); letter-spacing: -0.01em; }
.guide p { font-size: 15.5px; line-height: 1.72; color: var(--ink-2); margin: 0 0 var(--s2);
  max-width: 72ch; }
ul.glist { margin: 0 0 var(--s3); padding-left: 20px; color: var(--ink-2); font-size: 15px;
  line-height: 1.7; max-width: 72ch; }
ul.glist li { margin: 7px 0; }
.guide blockquote { margin: 0 0 var(--s3); padding: var(--s2) var(--s3); border-left: 3px solid var(--muted);
  background: var(--surface); border-radius: 0 10px 10px 0; font-size: 15px; color: var(--ink-2); }
.guide blockquote cite { display: block; margin-top: 6px; font-size: 12.5px; color: var(--muted);
  font-style: normal; }

.gcta { display: flex; justify-content: space-between; align-items: center; gap: var(--s3);
  flex-wrap: wrap; border: 1px solid transparent;
  border-radius: var(--radius); padding: var(--s3); margin: var(--s5) 0;
  background: var(--card-glow) padding-box, var(--glass-fill) padding-box, var(--rim) border-box;
  box-shadow: inset 0 1px 0 color-mix(in srgb, white 16%, transparent),
              0 12px 32px -20px var(--glow-1); }
.gcta strong { font-size: 16px; }
/* The conversion block's heading is an h2 now, not a <strong>, because it was the only thing
   on a guide page that looked like a heading without being one - so the page's own call to
   action was absent from the document outline while the equivalent block on the homepage sat
   at h2. Promoting it brought `.guide h2`'s margin with it: 56px of dead space above the
   heading, INSIDE the block's own 22px padding, opening a hole in the middle of the highest-
   converting surface on the site. It keeps the <strong>'s size because nothing about the
   block's proportions was wrong - only its semantics were. */
.gcta h2 { margin: 0 0 var(--s1); font-size: 16px; }
.gcta .btn { display: inline-flex; align-items: center; gap: 8px; }
/* The text side takes the slack, so the button lands hard right instead of floating in the
   middle of whatever space the copy left over. Four templates wrote `style="flex:1"` on that
   first child; this is the same declaration, once. */
.gcta > :first-child { flex: 1; }
/* The note under the form or the pitch is the last thing in the block, so its paragraph
   margin would open a strip of empty glass below it, inside the block's own padding. */
.gcta p:last-child { margin-bottom: 0; }

ul.sources-list { list-style: none; padding: 0; margin: 0 0 var(--s2); }
ul.sources-list li { font-size: 14px; color: var(--ink-2); padding: 7px 0;
  border-bottom: 1px solid var(--rule); line-height: 1.55; }
ul.sources-list li:last-child { border-bottom: 0; }

.rel-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: var(--s2); margin-bottom: var(--s4); }
.rel { display: block; border: 1px solid transparent;
  border-radius: var(--radius); padding: var(--s3); color: inherit;
  background: var(--card-glow) padding-box, var(--glass-fill) padding-box, var(--rim) border-box;
  box-shadow: inset 0 1px 0 color-mix(in srgb, white 16%, transparent),
              0 12px 32px -20px var(--glow-1);
  transition: box-shadow .18s ease; }
.rel:hover { text-decoration: none;
  box-shadow: inset 0 1px 0 color-mix(in srgb, white 22%, transparent),
              0 16px 36px -18px var(--glow-2); }
.rel strong { display: block; font-size: 15px; color: var(--ink); margin-bottom: 6px;
  line-height: 1.35; }
.rel span { font-size: 13px; color: var(--muted); line-height: 1.55; }
.guide-hub h2 { margin-top: var(--s5); }

/* ============ help page ================================================== */
figure.shot { margin: var(--s3) 0 var(--s4); }
figure.shot img { display: block; width: 100%; height: auto; border-radius: 10px;
  border: 1px solid var(--border); }
figure.shot figcaption { font-size: 13px; color: var(--muted); line-height: 1.6;
  margin-top: var(--s2); max-width: 72ch; }
.split { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s2); margin: var(--s3) 0 var(--s4); }
.split .card h3 { display: flex; align-items: center; gap: 8px; font-size: 15px; }
.split .card p { font-size: 14px; margin: 0; }
.steps { display: grid; gap: var(--s2); margin: var(--s3) 0 var(--s4); }
.step { border: 1px solid transparent; border-radius: var(--radius); padding: var(--s2) var(--s3);
  background: var(--card-glow) padding-box, var(--glass-fill) padding-box, var(--rim) border-box;
  box-shadow: inset 3px 0 0 var(--accent),
              inset 0 1px 0 color-mix(in srgb, white 16%, transparent),
              0 12px 32px -20px var(--glow-1); }
.step .sn { display: inline-block; font-size: 11px; font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; color: var(--accent); margin-bottom: 6px; }
.step p { font-size: 14.5px; margin: 0 0 6px; }
.step p:last-child { margin-bottom: 0; }
@media (max-width: 900px) { .split { grid-template-columns: 1fr; } }

/* Pebble in a lake: the radial text bleeds breathe in unison - each grows then shrinks from its
   own anchor on one shared rhythm. A radial-gradient is not itself animatable, so the pulse is
   background-size scaling the gradient in and out; background-position pins the growth to each
   bleed's origin so it swells from the centre rather than drifting. Motion only when the viewer
   has not asked it off. */
@keyframes bleed-ripple { 0%, 100% { background-size: 100% 100%; } 50% { background-size: 148% 148%; } }
/* The hero swells half again as far - a bigger stone, a wider ripple - on the same rhythm. */
@keyframes bleed-ripple-lg { 0%, 100% { background-size: 100% 100%; } 50% { background-size: 172% 172%; } }
/* The checker title's ripple swells far enough to wash the whole heading with the tint, then
   contracts all the way back to its centre point. */
@keyframes bleed-cover { 0%, 100% { background-size: 100% 100%; } 50% { background-size: 340% 340%; } }
@media (prefers-reduced-motion: no-preference) {
  .hero h1  { background-position: 50% 20%; animation: bleed-ripple-lg 5.5s ease-in-out infinite; }
  #check h2 { background-position: 50% 82%; animation: bleed-cover 5.5s ease-in-out infinite; }
  .figure .n { background-position: 50% 90%; animation: bleed-ripple 5.5s ease-in-out infinite; }
}


/* ============ the rotating glow, part two ================================
   .glow is defined in app.css and its --rim override needs nothing more: a custom property
   set on the element beats the inherited one whatever else is in the cascade, so the spinning
   rim reaches every glass component on its own.

   Its box-shadow does not, and that is why this is here. .glow is a single class, and so are
   .card, .tile, .figure, .gcta, .rel, .answer and .step - every one of which declares its own
   box-shadow. At equal specificity the later declaration wins, and all of those are later:
   some further down app.css, the rest in this sheet, which loads after it. So the glow's lift
   - the 1px brand halo and the wider, stronger drop that is what makes the element read as THE
   thing to click - was being dropped on every component the glow is actually placed on, while
   the rim went on spinning and the whole thing looked almost right.

   Restated once, here at the end, where nothing comes after it. Deliberately identical to the
   declaration in app.css rather than tokenised: the two must not be able to drift, and a
   reader who finds one is told by both comments that the other exists. .btn.primary (0,2,0)
   and .rel:hover (0,1,1) still outrank this, which is correct - the primary button has its own
   lit treatment and hover is a state, not a rest. */
.glow { box-shadow: inset 0 1px 0 color-mix(in srgb, white 16%, transparent),
                    0 0 0 1px color-mix(in srgb, var(--brand) 18%, transparent),
                    0 14px 40px -16px var(--glow-2); }
