/* Self-hosted, no CDN (GDPR-clean, CSP-safe). One contemporary sans - Manrope - carries the
   whole product; hierarchy comes from weight contrast (light 300 for lede/labels, heavy
   700-800 for headlines and numerals), not from a second family. Two subsets - latin covers
   Spanish + the euro sign; latin-ext covers Polish and the other extended Latin the locales
   use. Variable font, so one file per subset spans every weight (200-800). Filenames are
   stable: the ASSET_V hash (which includes .woff2) bumps the stylesheet when a file changes,
   but the font URL is deliberately unversioned so preload and @font-face request the same URL. */
@font-face { font-family: "Manrope"; font-style: normal; font-weight: 200 800; font-display: swap;
  src: url(/static/fonts/manrope-latin.woff2) format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: "Manrope"; font-style: normal; font-weight: 200 800; font-display: swap;
  src: url(/static/fonts/manrope-latinext.woff2) format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }

:root {
  color-scheme: dark;
  --font-sans: "Manrope", system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-display: "Manrope", system-ui, -apple-system, "Segoe UI", sans-serif;
  /* ONE literal hue in the whole product. Change this single line to trial another brand
     colour: every accent, glow, glass tint, tinted neutral and hover wash is mixed from it,
     so the palette re-skins coherently. tests/test_brand.py enforces that there is exactly
     one --brand and that no other blue hex hides anywhere in either sheet. */
  --brand: #3987e5;
  --accent: var(--brand);
  --accent-2: color-mix(in srgb, var(--brand) 85%, white);
  /* Grounds: near-black carrying a faint brand cast, so --brand re-tints the whole surface
     and not only the accents. */
  --plane: color-mix(in srgb, var(--brand) 8%, #050505);
  --tile: #2E3238;    /* home-screen icon ground + PWA splash; see brand/icon-*.png */
  --surface: color-mix(in srgb, var(--brand) 10%, #101010);
  --surface-2: color-mix(in srgb, var(--brand) 11%, #181818);
  --glass-tint: color-mix(in srgb, var(--brand) 45%, white);
  --ink: color-mix(in srgb, var(--brand) 8%, white);
  --ink-2: color-mix(in srgb, var(--brand) 10%, #b4b4b4);
  --ink-3: var(--muted);
  --muted: color-mix(in srgb, var(--brand) 12%, #8c8c8c);
  --rule: color-mix(in srgb, var(--brand) 12%, #262626);
  --border: color-mix(in srgb, var(--glass-tint) 14%, transparent);
  --edge: color-mix(in srgb, white 20%, transparent);   /* top-lit glass rim */
  /* Semantics keep their own hue families - never mixed from --brand - but brightened for
     the dark ground. These four hexes are the whitelist in tests/test_brand.py. */
  --good: #2fb457; --warning: #fab219; --serious: #ec835a; --critical: #e25454;
  --glow-1: color-mix(in srgb, var(--brand) 25%, transparent);
  --glow-2: color-mix(in srgb, var(--brand) 45%, transparent);
  --ring: color-mix(in srgb, var(--brand) 55%, white);
  /* --- glass kit -------------------------------------------------------------------
     The three layers that make a surface read as lit glass rather than a flat panel, reused
     across cards, buttons, coins, tables and the sidebar so the light is consistent:
       --rim       a gradient BORDER, bright at the top-left, fading, a touch of brand at the
                   far corner - the lit edge that catches the light.
       --glass-fill a translucent fill so a backdrop-blur shows the ground glow THROUGH the
                   surface (this is what creates depth; an opaque fill kills the effect).
       --card-glow an interior radial light pooling from the top-left, bleeding into the fill.
     Applied via background-clip: the fill/glow clip to padding-box, the rim to border-box. */
  --rim: linear-gradient(135deg, color-mix(in srgb, white 32%, transparent), color-mix(in srgb, white 5%, transparent) 42%, color-mix(in srgb, var(--brand) 26%, transparent));
  --glass-fill: linear-gradient(color-mix(in srgb, var(--surface) 74%, transparent), color-mix(in srgb, var(--surface) 55%, transparent));
  --card-glow: radial-gradient(135% 90% at 16% -12%, color-mix(in srgb, var(--brand) 20%, transparent), transparent 55%);
  --blur: blur(16px) saturate(150%);
  /* Spacing + geometry live here (not landing.css) so the app shell, which loads app.css
     alone, resolves them too - the `.getstarted { margin-bottom: var(--s3) }` at the bottom
     of this sheet was silently dropped for exactly that reason. */
  --s1: 8px; --s2: 14px; --s3: 22px; --s4: 34px; --s5: 56px; --s6: 76px;
  --measure: 660px;
  /* Geometry. --r-card is canonical: it names the component it belongs to and it sits in
     the r-* family the rest of the geometry is read from. --radius is the older name for
     the same 14px and roughly a dozen rules in landing.css still write it (.result,
     .cov-card, .answer, .gcta, .rel, .step and the figure family), so the
     components were split across two spellings of one number - and any treatment keyed to
     one of them silently missed every component using the other, which is exactly the kind
     of half-applied change nobody notices until a screenshot is compared. Aliased rather
     than renamed so nothing re-renders: every existing var(--radius) now resolves through
     --r-card and the painted radius is identical. New rules should reach for --r-card. */
  --r-pill: 999px; --r-ctl: 10px; --r-card: 14px; --r-tile: 18px;
  --radius: var(--r-card);
}

/* --- the rotating glow ----------------------------------------------------------------
   A slow brand-coloured light travelling round the rim of the one element on a page that is
   asking for the click. Sits here, against the glass kit above, because it is a fourth member
   of that kit rather than a component of its own.

   It re-skins --rim instead of drawing a pseudo-element, because every glass surface in both
   sheets already paints its border as `var(--rim) border-box` - .card, .tile, .figure, .gcta,
   .rel, .answer, .gs-card, .step and .btn all do. Overriding the one token therefore reaches
   all of them for free, with no per-component rule, and with nothing to fight the
   overflow:hidden several of them carry (which would clip a ::before ring at the corners).
   .gs-card.is-done further down this sheet already proves the pattern: it turns a whole card
   green by re-pointing these same tokens and touching nothing else.

   Six seconds, not the three of the sample this came from. On a decorative sample a quick
   sweep reads as craft; on the block carrying the page's call to action it reads as a
   spinner - as though something were still loading and the button were not yet ready. At 6s
   the light is ambient: the eye finds it without being hurried by it.

   The animation sits inside prefers-reduced-motion: no-preference rather than leaning on the
   global kill-switch below, because the house pattern (landing.css:608, 743, 821, 944) is to
   never START an ambient loop for a visitor who asked for stillness, not to start one and then
   clamp its duration to nothing. The rest state is what earns that: at --spin: 0deg the rim is
   already lit, with its bright point at twelve o'clock. So a reduced-motion visitor - and
   equally a browser with no @property support, where an unregistered angle cannot interpolate
   and the keyframe simply holds at its start - gets a premium static edge rather than a dead
   border. It is the same bargain landing.css:573-576 strikes for the SVG scenes.

   Budget: at most three .glow elements on a page. A conic gradient repaints every frame and
   the cost is per element, not per sheet; tests/test_marketing.py holds the ceiling.

   One half of this rule lives elsewhere. The --rim override below works from anywhere, because
   a custom property set on the element wins over the inherited one outright. The box-shadow
   does not: .glow is one class and so is every glass component it lands on, and all of those
   declare a box-shadow of their own further down this sheet or in landing.css, which loads
   after it. So the lift is restated at the foot of landing.css, where nothing outranks it.
   Change one and you must change the other; the comment there says the same in reverse. */
@property --spin { syntax: "<angle>"; inherits: false; initial-value: 0deg; }
/* Not redundant, and do not delete it, for two separate reasons.

   The first is the rest state. 0deg is where the gradient sits for a reduced-motion visitor
   and for any browser that cannot interpolate an unregistered angle: the bright point at
   twelve o'clock, a lit rim rather than a dead border. @property's initial-value says the same
   thing but only to engines that implement @property, which is precisely the set of engines
   that do not need telling.

   The second is that an @property block declares nothing a plain regex can see. It opens
   `@property --spin {` with no colon after the name, so what
   tests/test_brand.py::test_no_var_references_an_undefined_token finds inside it - matching
   --([a-z0-9-]+)\s*: for its set of DEFINED tokens - is `syntax` and `initial-value`, never
   `spin`. Without a real declaration somewhere, the bare var(--spin) below reads to that test
   as a reference to a token defined in neither sheet, which is the exact bug class it was
   written for. As it happens `--spin: 360deg` inside the keyframe satisfies that regex too, so
   pulling this line alone does not turn that test red today - which is worse, not better:
   the guard would then be resting on a keyframe that anyone is free to rename. That is why
   test_every_registered_property_is_also_declared_in_root pins the :root declaration itself. */
:root { --spin: 0deg; }

.glow {
  --rim: conic-gradient(from var(--spin),
    color-mix(in srgb, var(--brand) 70%, white),
    color-mix(in srgb, var(--brand) 22%, transparent) 22%,
    color-mix(in srgb, white 34%, transparent) 50%,
    color-mix(in srgb, var(--brand) 22%, transparent) 78%,
    color-mix(in srgb, var(--brand) 70%, white));
  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);
}
/* .btn.primary paints its own border-box layer, so re-pointing --rim alone would not reach it:
   the token would change and the button would go on drawing the gradient it names inline. The
   fill is repeated verbatim from .btn.primary below and only the border-box layer is swapped. */
.btn.primary.glow {
  background:
    linear-gradient(180deg, var(--brand), color-mix(in srgb, var(--brand) 80%, black)) padding-box,
    var(--rim) border-box;
}
@media (prefers-reduced-motion: no-preference) {
  .glow { animation: rim-spin 6s linear infinite; }
  @keyframes rim-spin { to { --spin: 360deg; } }
}

* { box-sizing: border-box; }
body {
  margin: 0; background: var(--plane); color: var(--ink);
  font-family: var(--font-sans); line-height: 1.5; -webkit-font-smoothing: antialiased;
}
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

/* One focus ring for everything reachable by keyboard: a brand-tinted outline that reads on
   the dark ground (--ring is ~9.9:1 against the plane). :focus-visible so a mouse click does
   not paint a ring, only keyboard travel does. Nothing in either sheet sets `outline: none`
   without this in place. */
:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; border-radius: 2px; }
/* ...except on a pill. The 2px above is the right default for a text field or a link, and
   wrong for a button: the ring is drawn on the element's own border-radius, so a keyboard
   visitor tabbing onto a .btn got a near-rectangle floating around a fully rounded control.
   Stated here rather than in landing.css so the app shell - which loads this sheet alone -
   is covered too; putting it in the marketing sheet would have fixed the ring on /producto
   and left it broken on every button behind the login. */
.btn:focus-visible { border-radius: var(--r-pill); }

/* Motion kill-switch. Ships ahead of the first animation so a reduced-motion visitor is
   covered the moment any transition or keyframe lands. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    transition-duration: .01ms !important; animation-duration: .01ms !important;
    animation-iteration-count: 1 !important; scroll-behavior: auto !important;
  }
}

/* Layout */
.shell { display: grid; grid-template-columns: 232px 1fr; min-height: 100vh; }
/* Grid items default to min-width:auto, so a wide child (a table, a long path)
   stretches the column and the whole page scrolls sideways. */
.shell > main { min-width: 0; }
nav.side {
  background: color-mix(in srgb, var(--surface) 78%, transparent);
  border-right: 1px solid var(--border);
  padding: 20px 14px; position: sticky; top: 0; height: 100vh; overflow-y: auto;
}
/* One of the few genuinely-floating panels, so it earns a blur; the ground glow reads
   through it. */
@supports ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
  nav.side { -webkit-backdrop-filter: blur(16px) saturate(140%);
             backdrop-filter: blur(16px) saturate(140%); }
}
.logo { font-size: 13px; letter-spacing: .09em; text-transform: uppercase; color: var(--muted);
  padding: 0 10px 4px; }
.agency { font-weight: 600; padding: 0 10px 16px; font-size: 15px; }
nav.side a {
  position: relative; overflow: hidden;
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 8px 10px; border-radius: var(--r-ctl); color: var(--ink-2); font-size: 14px;
  border: 1px solid transparent;   /* reserved so the active pill's rim adds no shift */
}
nav.side a:hover { background: color-mix(in srgb, var(--glass-tint) 7%, transparent);
  color: var(--ink); text-decoration: none; }
/* Active item is a lit glass pill: brand fill, a bright refracting rim, an inner highlight
   and a soft glow - it reads as lit, not pressed in. */
nav.side a.on { color: var(--ink); font-weight: 600;
  background:
    linear-gradient(color-mix(in srgb, var(--brand) 26%, transparent), color-mix(in srgb, var(--brand) 15%, transparent)) padding-box,
    linear-gradient(135deg, color-mix(in srgb, white 26%, transparent), color-mix(in srgb, var(--brand) 32%, transparent)) border-box;
  box-shadow: inset 0 1px 0 color-mix(in srgb, white 18%, transparent),
              0 4px 14px -8px var(--glow-1); }

/* Sign-out is a POST form (CSRF), styled to sit in the nav as if it were a link. Reset the
   button chrome and borrow the surrounding anchor's box so it lines up in every nav it lands
   in - the tenant sidebar, the admin topbar and the admin aside. */
.navform { margin: 0; display: contents; }
.navform button {
  font: inherit; color: inherit; background: none; border: 0; cursor: pointer;
  display: inline-flex; align-items: center; gap: 8px; padding: 8px 10px;
  border-radius: 8px; text-align: left;
}
nav.side .navform button { display: flex; width: 100%; color: var(--ink-2); font-size: 14px; }
nav.side .navform button:hover { background: color-mix(in srgb, var(--glass-tint) 7%, transparent); }
nav.side .group { font-size: 11px; letter-spacing: .08em; text-transform: uppercase;
  color: var(--muted); padding: 18px 10px 6px; }

/* Language switcher. Shared by the marketing topbar and the signed-in sidebar, so it lives
   here rather than in landing.css - base.html loads app.css alone, and every page that loads
   landing.css loads app.css as well. */
.langs { display: inline-flex; gap: 2px; border: 1px solid var(--border); border-radius: 999px;
  padding: 2px; }
.langs a { font-size: 11px; font-weight: 600; letter-spacing: .06em; padding: 3px 9px;
  border-radius: 999px; color: var(--muted); }
.langs a.on { background: var(--plane); color: var(--ink); }
.langs a:hover { color: var(--ink); text-decoration: none; }
/* `nav.side a` is more specific than `.langs a`, so inside the sidebar the switcher would
   otherwise inherit the full-width 14px nav-link box and the lit pill. Restate the small
   chip shape at sidebar specificity. */
nav.side .langs a { display: inline-flex; justify-content: center; width: auto;
  padding: 3px 9px; font-size: 11px; border-radius: 999px; border: 0; color: var(--muted); }
nav.side .langs a.on { background: var(--plane); color: var(--ink); box-shadow: none; }
nav.side .langs a:hover { background: none; color: var(--ink); }
/* Foot of the sidebar: a quiet standing preference, set off from the nav by a rule, using the
   same 10px gutter as the nav links and the sidebar's --s2 rhythm. */
.sidefoot { margin-top: var(--s2); padding: var(--s1) 10px 0; font-size: 12px;
  color: var(--muted); border-top: 1px solid var(--border); }
/* Darkened critical so the 11px white count clears AA (white on plain --critical is 3.73 on
   the brightened dark-ground red; on the darkened fill it is ~5.2). */
.pill { background: color-mix(in srgb, var(--critical) 82%, black); color: #fff;
  border-radius: var(--r-pill); font-size: 11px; padding: 1px 7px; font-weight: 600; }
main { padding: 28px 32px 72px; max-width: 1180px; }

h1 { font-family: var(--font-display); font-weight: 700; font-size: 28px; letter-spacing: -0.021em;
  margin: 0 0 4px; }
h2 { font-family: var(--font-display); font-weight: 700; font-size: 18px; letter-spacing: -0.014em;
  margin: 28px 0 10px; }
.sub { color: var(--ink-2); font-size: 14px; margin: 0 0 4px; }
.muted { color: var(--muted); font-size: 13px; }

/* Cards & tiles */
/* The single .card definition. Marketing keeps only a padding override (.marketing .card
   below in landing.css).
   Full glass: a lit gradient border (--rim), an interior light pooling from the top-left
   (--card-glow) over a translucent fill (--glass-fill), backdrop-blurred so the ground glow
   shows through and the card reads as lit glass with depth rather than a flat panel. */
.card { position: relative;
  border: 1px solid transparent; border-radius: var(--r-card); padding: 16px 18px;
  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); }
@supports ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
  .card { -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur); }
}
/* Head-split card: the icon sits top-left, the label pins top-right, and the detail runs the
   full width below - "label right-top, icon and detail left". Opt-in via .card.split with
   .s-label / .s-detail children (see the preview). */
.card.split { display: grid; grid-template-columns: auto 1fr; gap: 6px 12px;
  align-items: start; grid-template-areas: "icon label" "detail detail"; }
.card.split > .coin, .card.split > .cp-ic { grid-area: icon; }
.card.split > .s-label { grid-area: label; justify-self: end; text-align: right;
  font-weight: 600; font-size: 15px; }
.card.split > .s-detail { grid-area: detail; margin: 0; color: var(--ink-2); font-size: 14px;
  line-height: 1.6; }
/* Stat tile. The number sits ABOVE the label, and that is the fix for a bug this rule carried
   for a long time and that a first attempt at fixing made worse.

   It used to be one row - label left, number right - with the number at a fixed 34px and the
   label free to claim its min-content width first. On /admin the Instance row really does sit
   near the track floor, so each tile held roughly 95px of content box while the mirrored count,
   536<NBSP>691, is seven tabular digits running about 130px. The number ran out through the
   padding.

   The first fix put min-width: 0 on both children so either could shrink. That is right for the
   label, which wraps, and WRONG for the number, which cannot: shrinking a box below the width
   of unwrappable glyphs does not compress them, it spills them. The number then overflowed
   LEFTWARDS out of its own auto track and printed on top of the label - worse than the clipping
   it replaced, and visible on /admin as "536 69" laid over "Registry records mirrored".

   So the two stop competing. Stacked, the number gets the tile's full width and the label wraps
   underneath it with nothing to negotiate against - which is what .figure, the other stat
   component, has always done. .tile was the only one trying to do it in one row. (.gstat was a
   third; it has since been retired into .tile, which is what .tile .src below is for.)

   The track floor moves with it: 122px was drawn for two- and three-digit counts and cannot
   hold a six-figure one at any legible size. 150px lets six tiles sit across the 1180px admin
   main with about 160px of content each, which clears 130px of number at the clamp's ceiling. */
.tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 10px; margin: 18px 0; }
.tile { border: 1px solid transparent; border-radius: var(--r-card); padding: 16px 18px;
  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);
  display: block; }
@supports ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
  .tile { -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur); }
}
/* No min-width here, and no grid placement: in a block tile the number owns the full content
   box, so there is nothing to shrink below and nothing to overflow into. The clamp is what
   keeps it legible when the viewport - and with it the track - is narrow. */
.tile .n { text-align: left; font-family: var(--font-display);
  font-size: clamp(22px, 2.2vw, 34px); font-weight: 800; letter-spacing: -0.02em;
  line-height: 1.05; font-variant-numeric: tabular-nums; }
.tile .k { font-size: 13px; color: var(--ink-2); margin-top: 5px;
  display: flex; align-items: center; gap: 6px; }
/* Where the number came from. This is the one thing .gstat had that .tile did not, and it
   is why a guide page carried a second stat component that was otherwise the same glass,
   the same auto-fit track and the same recipe. Matched to .figure .src in landing.css so a
   citation reads the same whichever stat component is above it. */
.tile .src { display: block; font-size: 12px; color: var(--muted); margin-top: 4px;
  line-height: 1.5; }
/* Text-valued tiles - the registry snapshot date, not a count. Ten characters of date at the
   clamp's ceiling reads as a headline rather than a stat, and a date is the widest value its
   row holds rather than the narrowest. A smaller fixed step, as a class rather than the inline
   style admin_dashboard.html used to carry. */
.tile .n.sm { font-size: 20px; white-space: nowrap; }  /* nowrap because a browser breaks a date at its hyphens: 2026-09-28 came out as
     "2026-09-" over "28". Safe only here - .sm is the short, fixed-size value, and the
     unbounded numbers above it must stay breakable. */
/* /datos opt-in: two columns instead of auto-fit. munipage.html caps its article at an inline
   max-width:760px, and four auto-fit tiles across that leave roughly 144px of content box -
   too little for "109.916" at any size that still reads as a headline figure. Two columns give
   each tile about 375px instead.
   Everything else this block used to carry - display:block, the left-aligned number, the gap
   above the label - is now the BASE tile, so only the two differences are left: the column
   count, and a steeper clamp. The slope is steeper because these tiles are roughly twice the
   width of an auto-fit one, so they can reach the 34px ceiling sooner without overflowing;
   handing them the base clamp would leave them undersized at the width they were drawn for. */
.tiles.pair { grid-template-columns: repeat(2, 1fr); }
.tiles.pair .tile .n { font-size: clamp(22px, 4.2vw, 34px); }
.cols { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; align-items: start; }
.cols.three { grid-template-columns: repeat(3, 1fr); }
/* Mobile. Collapsing the grid to one column was not enough: the sidebar is a full-height
   vertical menu, so stacking it pushed every page a screen and a half down. Below 900px it
   becomes a compact sticky bar with the links on one horizontally scrollable row. */
@media (max-width: 900px) {
  .shell { grid-template-columns: 1fr; }
  .cols, .cols.three { grid-template-columns: 1fr; }

  nav.side {
    position: sticky; top: 0; z-index: 30; height: auto; overflow: visible;
    border-right: 0; border-bottom: 1px solid var(--border);
    padding: 8px 0 0; display: flex; flex-direction: column; gap: 6px;
  }
  nav.side a.logo { margin: 0 12px; padding: 0; align-self: flex-start; }
  nav.side a.logo:hover { background: none; }
  nav.side .brandmark { height: 26px; }
  /* The agency name is context, not navigation - keep it, but out of the scrolling row. */
  .agency { padding: 0 12px; font-size: 14px; }
  /* Group headings and the prototype footnote are orientation for a wide sidebar; on a phone
     they are three extra rows before the page starts. */
  nav.side .group, nav.side .proto { display: none; }
  /* The switcher stays - a phone visitor must still be able to change language - but it rides
     up onto the logo row rather than banding a row of its own: the logo is align-self:flex-start
     so the right-hand half of that row is empty. */
  .sidefoot { order: -1; align-self: flex-end; margin: 0 0 -27px; padding: 0 12px;
    border-top: 0; }

  /* One horizontally scrolling row of links. min-width:0 on both the nav and the scroller
     matters: a flex item defaults to min-width:auto, so without it the row sizes to its
     content and pushes the whole page wider than the viewport instead of scrolling. */
  nav.side { padding-bottom: 0; min-width: 0; -webkit-overflow-scrolling: touch; }
  .nav-scroll {
    display: flex; gap: 4px; overflow-x: auto; padding: 4px 12px 8px;
    min-width: 0; scrollbar-width: none;
  }
  .nav-scroll::-webkit-scrollbar { display: none; }
  .nav-scroll a { flex: 0 0 auto; white-space: nowrap; }
}
@media (min-width: 901px) { .nav-scroll { display: contents; } }

/* Status */
.dot { font-size: 11px; line-height: 1; }
.good .dot, .s-good { color: var(--good); }
.warning .dot, .s-warning { color: var(--warning); }
.serious .dot, .s-serious { color: var(--serious); }
.critical .dot, .s-critical { color: var(--critical); }
.status { display: inline-flex; align-items: center; gap: 7px; font-weight: 600;
  white-space: nowrap; }
/* Inside a notice the chip sits mid-sentence, and an inline-flex box takes its baseline from
   its FIRST item - the 11px glyph - so the bold text rode ~2px off the sentence around it.
   Baseline alignment puts chip text and running text on the same line; standalone uses of
   .status (tables, tiles) keep the centered look above. */
.notice .status { align-items: baseline; }

/* Tables */
/* Glass container (you asked to try it on tables too): lit rim + translucent blurred fill,
   same kit as cards. Rows paint over it; the row-hover wash still reads on top. If this turns
   out too heavy on long tables it reverts to a solid --surface in one line. */
.scroll { overflow-x: auto; border: 1px solid transparent; border-radius: var(--r-card);
  background: var(--glass-fill) padding-box, var(--rim) border-box;
  box-shadow: inset 0 1px 0 color-mix(in srgb, white 14%, transparent),
              0 12px 32px -22px var(--glow-1); }
@supports ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
  .scroll { -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur); }
}
table { border-collapse: collapse; width: 100%; font-size: 14px;
  font-variant-numeric: tabular-nums; }
th { text-align: left; font-size: 12px; letter-spacing: .04em; text-transform: uppercase;
  color: var(--muted); font-weight: 600; padding: 11px 14px;
  border-bottom: 1px solid var(--rule); white-space: nowrap; }
td { padding: 11px 14px; border-bottom: 1px solid var(--rule); vertical-align: top; }
tr:last-child td { border-bottom: 0; }
.code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; }
td:first-child { white-space: nowrap; }
tr.rowlink { cursor: pointer; }
/* Row hover: a brand wash on the ROW (not each cell, so the sweep is one continuous pass
   across the whole width) plus a one-shot light glint. The @keyframes is why the
   prefers-reduced-motion kill-switch near the top of this sheet exists. */
tr.rowlink:hover {
  background-color: color-mix(in srgb, var(--accent) 9%, transparent);
  background-image: linear-gradient(100deg, transparent 44%,
    color-mix(in srgb, white 11%, transparent) 50%, transparent 56%);
  background-repeat: no-repeat; background-size: 220% 100%; background-position: 160% 0;
  animation: sweep .8s ease; }
@keyframes sweep { from { background-position: -60% 0; } to { background-position: 160% 0; } }
.addr { color: var(--ink-2); font-size: 13px; display: block; }

/* Forms & buttons */
label { display: block; font-size: 13px; color: var(--ink-2); margin: 10px 0 4px; }
input, select, textarea { width: 100%; padding: 9px 11px; border-radius: var(--r-ctl); font: inherit;
  font-size: 14px; background: var(--surface-2); color: var(--ink);
  border: 1px solid var(--border); transition: border-color .15s; }
/* Border-only on focus; the keyboard ring is the global :focus-visible outline, so a mouse
   click into a field does not double up outline + border. */
input:focus, select:focus, textarea:focus {
  border-color: color-mix(in srgb, var(--brand) 55%, transparent); }
/* A textarea was left out of the field rule above, so the paste box on the audit and the
   energy-report forms - the main control on both - fell back to the browser default: twenty
   monospace columns with its own border, and its caption stranded beside it instead of above.
   Block, so it sits under its label like every other field, and vertical resize only, because
   dragging one wider tears the card it sits in. */
textarea { display: block; margin-top: 4px; line-height: 1.5; resize: vertical; }
input[type=file] { padding: 6px; }

/* A checkbox or a radio is not a text field. The rule above would stretch one across the form
   and pad it into a block of its own - which is what put the consent box on a line of its own
   with its sentence pushed underneath. Every screen that noticed had patched it locally (the
   cookie rows, the channel cards, the severity list), so any form that forgot the patch had
   the bug. Sized once here instead, and the local rules now only set their own spacing. */
input[type=checkbox], input[type=radio] { width: 16px; height: 16px; flex: none; padding: 0;
  accent-color: var(--accent); }
/* The label that sets one beside its words: a flex row, so a sentence that wraps stays
   indented past the box instead of running back under it, and the box sits on the first line
   rather than centred against the whole paragraph. */
label.inline { display: flex; align-items: flex-start; gap: 9px; line-height: 1.5; }
label.inline input[type=checkbox], label.inline input[type=radio] { margin-top: 2px; }

/* Source column-mapping screen */
input[type=checkbox].inline-check { margin-right: 8px; vertical-align: middle; }
.maptag { font-size: 11px; }
.mapsamp { font-size: 12px; color: var(--muted); }
/* A field we auto-detected: a green rail + tinted border. Toggled off by forms.js when the
   user picks a different column. */
.mapsel.is-auto { border-color: color-mix(in srgb, var(--good) 55%, transparent);
  box-shadow: inset 3px 0 0 var(--good); }
/* Pill buttons.
   Secondary is a glass pill: lit rim + translucent blurred fill + inner highlight.
   Primary is a brand gradient (opaque, so the white label stays >=3.64:1) carrying the light
   itself - a bright lit rim, a strong inner top highlight, and an outer glow. The green/blue
   reads as coming THROUGH the button rather than painted flat. */
.btn { position: relative; overflow: hidden;
  display: inline-flex; align-items: center; justify-content: center;
  padding: 8px 16px; border-radius: var(--r-pill); font-size: 14px;
  font-weight: 600; border: 1px solid transparent; color: var(--ink); cursor: pointer;
  background: var(--glass-fill) padding-box, var(--rim) border-box;
  box-shadow: inset 0 1px 0 color-mix(in srgb, white 14%, transparent);
  transition: box-shadow .18s ease, transform .1s ease; }
@supports ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
  .btn:not(.small) { -webkit-backdrop-filter: blur(10px) saturate(140%);
                     backdrop-filter: blur(10px) saturate(140%); }
}
.btn:hover { text-decoration: none;
  box-shadow: inset 0 1px 0 color-mix(in srgb, white 22%, transparent),
              0 6px 18px -10px var(--glow-1); }
.btn.primary { color: #fff;
  background:
    linear-gradient(180deg, var(--brand), color-mix(in srgb, var(--brand) 80%, black)) padding-box,
    linear-gradient(160deg, color-mix(in srgb, white 62%, transparent),
                    color-mix(in srgb, white 8%, transparent) 46%,
                    color-mix(in srgb, var(--brand) 46%, transparent)) border-box;
  box-shadow: inset 0 1px 0 color-mix(in srgb, white 42%, transparent),
              0 8px 26px -10px var(--glow-2); }
.btn.primary:hover {
  box-shadow: inset 0 1px 0 color-mix(in srgb, white 55%, transparent),
              0 11px 30px -8px var(--glow-2); }
.btn.small { padding: 5px 12px; font-size: 13px; font-weight: 500; }
.btn.danger { color: var(--critical); }
/* Hover glint on controls: a light band crosses, painted OVER the content with a screen blend
   so it lifts the label more than the dark fill - a sweep biased to the text, kept subtle.
   Shares the `sweep` keyframe with table rows; clipped by the element's overflow:hidden, while
   the outer glow (box-shadow) is untouched. */
.btn::after, nav.side a::after {
  content: ""; position: absolute; inset: 0; pointer-events: none; border-radius: inherit;
  background: linear-gradient(100deg, transparent 44%,
    color-mix(in srgb, white 26%, transparent) 50%, transparent 56%);
  background-repeat: no-repeat; background-size: 220% 100%; background-position: 160% 0;
  mix-blend-mode: screen; }
.btn:hover::after, nav.side a:hover::after { animation: sweep .7s ease; }
.row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.spread { display: flex; justify-content: space-between; align-items: flex-end; gap: 16px;
  flex-wrap: wrap; }

/* Notices */
.notice { border-radius: 10px; padding: 11px 14px; font-size: 14px; margin: 14px 0;
  border: 1px solid var(--border); background: var(--surface); }
.notice.warn { border-left: 3px solid var(--warning);
  background: color-mix(in srgb, var(--warning) 8%, var(--surface)); }
.notice.bad { border-left: 3px solid var(--critical);
  background: color-mix(in srgb, var(--critical) 8%, var(--surface)); }
.notice.ok { border-left: 3px solid var(--good);
  background: color-mix(in srgb, var(--good) 8%, var(--surface)); }
.proto { font-size: 12px; color: var(--muted); border: 1px dashed var(--border);
  border-radius: 8px; padding: 8px 11px; margin-top: 14px; }
ul.notes { margin: 0; padding-left: 16px; color: var(--ink-2); font-size: 13px; }
ul.notes li { margin: 2px 0; }
.bar { height: 6px; border-radius: 3px; background: var(--rule); overflow: hidden; margin-top: 6px; }
.bar > span { display: block; height: 100%; background: var(--accent); }
.chips { display: flex; gap: 8px; flex-wrap: wrap; margin: 12px 0; }
.chip { border: 1px solid var(--border); border-radius: var(--r-pill); padding: 4px 12px;
  font-size: 13px; color: var(--ink-2); background: var(--surface-2);
  transition: color .15s, border-color .15s; }
.chip.on { background: var(--accent); border-color: transparent; color: #fff; font-weight: 600; }
.chip:hover { text-decoration: none; color: var(--ink);
  border-color: color-mix(in srgb, var(--glass-tint) 22%, transparent); }

/* Brand mark in the app sidebar. The wordmark is white-on-transparent, which now sits on the
   dark ground directly - the light-mode rescue plate that used to live here is gone with the
   light theme. */
/* The logo is a brand mark, not a nav item, but it is an <a> inside nav.side so it picked up
   the generic link hover. Symmetric padding contains the hover; the gap to the agency name
   below moves to margin, which the hover background does not paint. */
nav.side a.logo { display: block; padding: 6px 10px; margin-bottom: 10px; border-radius: 8px; }
nav.side .brandmark { display: block; height: 30px; width: auto; }

/* ============ notification rules ========================================= */
/* Single home for .ic / .badge / .b-* (landing.css used to carry its own filled copies, so a
   badge looked different in the app than on marketing). Filled is the canonical form; the
   fill percentages re-tune for the dark ground in phase 5. */
.ic { flex: none; vertical-align: -0.22em; }
h2 .ic { color: var(--accent); vertical-align: -0.16em; margin-right: 6px; }
h3 .ic { color: var(--muted); margin-right: 7px; vertical-align: -0.2em; }
.badge { display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px;
  font-weight: 650; letter-spacing: .02em; padding: 2px 9px; border-radius: 999px;
  border: 1px solid transparent; white-space: nowrap; }
.badge .bdot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.b-good     { color: var(--good);     background: color-mix(in srgb, var(--good) 18%, transparent);
              border-color: color-mix(in srgb, var(--good) 45%, transparent); }
.b-warning  { color: var(--warning);  background: color-mix(in srgb, var(--warning) 18%, transparent);
              border-color: color-mix(in srgb, var(--warning) 45%, transparent); }
.b-serious  { color: var(--serious);  background: color-mix(in srgb, var(--serious) 18%, transparent);
              border-color: color-mix(in srgb, var(--serious) 45%, transparent); }
.b-critical { color: var(--critical); background: color-mix(in srgb, var(--critical) 18%, transparent);
              border-color: color-mix(in srgb, var(--critical) 45%, transparent); }

.rules { display: grid; gap: 12px; margin-bottom: 18px; }
.rule { background: var(--surface); border: 1px solid var(--border); border-left: 3px solid var(--accent);
  border-radius: 12px; padding: 15px 18px; }
.rule.is-off { border-left-color: var(--muted); opacity: .72; }
.rule.empty { border-left-color: var(--rule); }
.rule-head { display: flex; justify-content: space-between; align-items: center; gap: 14px;
  flex-wrap: wrap; margin-bottom: 10px; }
.rule-head strong { font-size: 15px; margin-right: 8px; }
.chan-row { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 10px; }
.chan { display: inline-flex; align-items: center; gap: 7px; font-size: 13px; padding: 5px 11px;
  border-radius: 999px; border: 1px solid var(--border); color: var(--muted); }
.chan.on { color: var(--ink); border-color: color-mix(in srgb, var(--accent) 55%, transparent);
  background: color-mix(in srgb, var(--accent) 15%, transparent); }
.chan.off { opacity: .45; text-decoration: line-through; }
.chan em { font-style: normal; color: var(--ink-2); font-size: 12px; }
.rule-meta { display: flex; gap: 18px; flex-wrap: wrap; font-size: 13px; color: var(--ink-2); }
.rule-meta .ic { color: var(--muted); }
.sev.s-critical { color: var(--critical); }
.sev.s-serious { color: var(--serious); }
.sev.s-all { color: var(--good); }

.newrule { background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
  margin-bottom: 28px; }
.newrule > summary { display: flex; align-items: center; gap: 9px; padding: 13px 18px;
  cursor: pointer; list-style: none; }
.newrule > summary::-webkit-details-marker { display: none; }
.newrule > summary .ic { color: var(--accent); }
.newrule .card { border: 0; background: transparent; padding: 0 18px 18px; }
h3.fs, h2.fs { font-family: var(--font-sans); font-weight: 600; font-size: 12px;
  letter-spacing: .1em; text-transform: uppercase; color: var(--muted); margin: 20px 0 6px; }

.chan-picks { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 10px; }
.chan-pick { display: flex; gap: 11px; align-items: flex-start; padding: 13px; margin: 0;
  border: 1px solid var(--border); border-radius: 11px; cursor: pointer; background: var(--plane); }
.chan-pick:has(input:checked) { border-color: color-mix(in srgb, var(--accent) 60%, transparent);
  background: color-mix(in srgb, var(--accent) 13%, transparent); }
.chan-pick input { margin: 2px 0 0; }
/* Glass coin: a lit disc/tile that sits distinct from the card behind it, with a bright
   refracting rim and a hue glow pooling top-left. Set --coin-hue to tint it. Reusable as a
   standalone .coin; the channel-picker icons below are coins carrying their own channel hue. */
.coin, .cp-ic { --coin-hue: var(--brand); display: grid; place-items: center; flex: none;
  border: 1px solid transparent;
  background:
    radial-gradient(120% 120% at 30% 20%, color-mix(in srgb, var(--coin-hue) 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(--coin-hue) 30%, transparent)) border-box;
  box-shadow: inset 0 1px 0 color-mix(in srgb, white 28%, transparent),
              0 4px 14px -7px var(--glow-1); }
/* White icon: the hue lives in the glass (rim + interior glow), so the glyph itself stays
   white rather than doubling the colour. */
.coin, .cp-ic { color: #fff; }
.coin { width: 40px; height: 40px; border-radius: 50%; }
@supports ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
  .coin, .cp-ic { -webkit-backdrop-filter: blur(8px) saturate(140%);
                  backdrop-filter: blur(8px) saturate(140%); }
}
.cp-ic { width: 40px; height: 40px; border-radius: var(--r-ctl); }
.ci-email { --coin-hue: var(--accent); }
.ci-whatsapp { --coin-hue: var(--good); }
.ci-push { --coin-hue: var(--serious); }
.cp-body strong { display: block; font-size: 14px; }
.cp-body em { font-style: normal; font-size: 12px; color: var(--muted); line-height: 1.45;
  display: block; margin-top: 2px; }

.sev-picks { display: grid; gap: 9px; margin-top: 10px; }
.sev-pick { display: flex; gap: 11px; align-items: flex-start; padding: 12px 14px; margin: 0;
  border: 1px solid var(--border); border-left-width: 3px; border-radius: 11px;
  cursor: pointer; background: var(--plane); }
.sev-pick.s-critical { border-left-color: var(--critical); }
.sev-pick.s-serious { border-left-color: var(--serious); }
.sev-pick.s-all { border-left-color: var(--good); }
.sev-pick:has(input:checked) { background: color-mix(in srgb, var(--accent) 13%, transparent); }
.sev-pick input { margin: 3px 0 0; }
.sp-body strong { display: block; font-size: 14px; }
.sp-body em { font-style: normal; font-size: 12.5px; color: var(--muted); line-height: 1.5;
  display: block; margin-top: 2px; }
@media (max-width: 900px) { .chan-picks { grid-template-columns: 1fr; } }

.device { display: flex; gap: 7px; align-items: flex-start; font-size: 12.5px;
  color: var(--ink-2); background: var(--plane); border: 1px solid var(--border);
  border-radius: 9px; padding: 8px 11px; margin: 10px 0 6px; }
.device .ic { color: var(--serious); margin-top: 1px; }
.device strong { color: var(--ink); }
.thisdev { margin-top: 6px !important; color: var(--ink-2) !important;
  display: flex !important; gap: 5px; align-items: center; }
.thisdev .ic { color: var(--serious); }
.thisdev b { color: var(--ink); font-weight: 650; }
/* A channel that is chosen but not yet delivering. Warning-toned rather than error-toned:
   nothing is wrong, and the number and consent really are being kept. */
.chan-pick em.soon { margin-top: 6px !important; display: inline-flex !important;
  align-items: center; gap: 5px; font-style: normal; font-size: 12px; font-weight: 600;
  padding: 2px 8px; border-radius: 999px; color: var(--warning);
  background: color-mix(in srgb, var(--warning) 14%, transparent); }

.notice.why { padding: 16px 18px; }
.notice.why h3 { font-size: 15px; margin: 0 0 8px; display: flex; align-items: center; gap: 8px; }
.notice.why h3 .ic { color: var(--warning); }
.notice.why p { font-size: 13.5px; color: var(--ink-2); line-height: 1.6; margin: 0 0 10px; }
.notice.why ul.notes { font-size: 13.5px; line-height: 1.6; margin-bottom: 10px; }
.notice.why ul.notes li { margin: 6px 0; }
.notice.why p.muted { font-size: 12.5px; margin-bottom: 0; }

/* ============ web push enrolment ========================================= */
.push-state { display: flex; align-items: center; gap: 11px; flex-wrap: wrap;
  background: var(--surface); border: 1px solid var(--border); border-left: 3px solid var(--muted);
  border-radius: 12px; padding: 13px 16px; margin-bottom: 14px; font-size: 14px; }
.push-state.s-on { border-left-color: var(--good); }
.push-state.s-off { border-left-color: var(--warning); }
.push-state.s-blocked { border-left-color: var(--critical); }
.push-state .ps-ic { display: inline-flex; color: var(--muted); }
.push-state.s-on .ps-ic { color: var(--good); }
.push-state.s-blocked .ps-ic { color: var(--critical); }
.push-state .ps-msg { flex: 1; min-width: 240px; color: var(--ink-2); }
.push-state .btn { flex: none; }

/* Devices as cards in a responsive grid rather than full-width rows. A row per device wasted
   most of the width on a wide screen and made two devices look like a long list; auto-fill
   collapses to one column on a phone without a second breakpoint. */
.sub-list { display: grid; gap: 10px; margin-bottom: 14px;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); }
/* Three columns on ONE row: state, device, action. The state used to be a badge stacked
   under the text, which cost a third line per card for one word - as a leading dot it reads
   at a glance down the left edge of the list and the card is a line shorter. */
.sub-row { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 2px 9px;
  align-items: start; background: var(--surface); border: 1px solid var(--border);
  border-radius: 10px; padding: 11px 13px; font-size: 13.5px; }
/* line-height, not margin: the dot then sits on the baseline of the device name whatever
   the card's font size does. */
.sub-row .sub-dot { font-size: 10px; line-height: 1.9; color: var(--good); }
.sub-row.dead .sub-dot { color: var(--warning); }
.sub-row .sub-body { display: flex; flex-direction: column; line-height: 1.35; min-width: 0; }
.sub-row strong { overflow-wrap: anywhere; }
.sub-row em { font-style: normal; font-size: 12px; color: var(--muted); }
.sub-row form { display: flex; }

.push-log { background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
  margin-bottom: 22px; }
.push-log > summary { display: flex; align-items: center; gap: 9px; padding: 12px 16px;
  cursor: pointer; list-style: none; font-size: 14px; }
.push-log > summary::-webkit-details-marker { display: none; }
.push-log .scroll { border: 0; border-top: 1px solid var(--rule); border-radius: 0 0 12px 12px; }
/* Activity entries: headline plus footnote, same leading-dot rhythm as the device cards.
   The detail line is allowed to be long - it wraps inside the card rather than widening it,
   which is what the five-column table could not do. */
.ev-list { border-top: 1px solid var(--rule); }
.ev { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 0 9px;
  padding: 9px 16px; border-bottom: 1px solid var(--rule); font-size: 13.5px; }
.ev:last-child { border-bottom: 0; }
.ev-dot { font-size: 10px; line-height: 1.9; }
.ev-dot.good { color: var(--good); }
.ev-dot.critical { color: var(--critical); }
.ev-body { display: flex; flex-direction: column; line-height: 1.35; min-width: 0; }
.ev-body strong { font-weight: 600; overflow-wrap: anywhere; }
.ev-body em { font-style: normal; font-size: 12px; color: var(--muted);
  overflow-wrap: anywhere; }

/* --- super admin shell ---------------------------------------------------------------
   Sidebar plus content. The admin is the one surface with enough distinct sections that a
   single scrolling page stopped being readable; below 900px the nav collapses to a row of
   chips rather than a hamburger, because four items do not need one. */
.adm { display: grid; grid-template-columns: 216px minmax(0, 1fr); gap: 28px;
  max-width: 1320px; margin: 0 auto; padding: 28px 22px 88px; align-items: start; }
.adm-nav { position: sticky; top: 22px; display: flex; flex-direction: column; gap: 2px; }
.adm-lbl { font-size: 11px; letter-spacing: .09em; text-transform: uppercase;
  color: var(--ink-3, var(--ink-2)); margin: 14px 0 6px; padding-inline: 11px; }
.adm-lbl:first-child { margin-top: 0; }
.adm-nav a { display: flex; align-items: center; gap: 9px; padding: 9px 11px;
  border-radius: 8px; font-size: 14px; color: var(--ink-2); text-decoration: none;
  border: 1px solid transparent; }
.adm-nav a:hover { background: var(--surface); color: var(--ink); }
.adm-nav a.on { background: var(--surface); border-color: var(--border); color: var(--ink);
  font-weight: 600; }
.adm-nav a .ic { flex: none; opacity: .8; }
.adm-nav a.on .ic { color: var(--brand, currentColor); opacity: 1; }
.adm-main { min-width: 0; }
.adm-main > h1 { font-size: 26px; margin: 0 0 6px; }
.adm-main > .sub { margin: 0 0 26px; }
.adm-main h2 { font-size: 16px; margin: 34px 0 12px; display: flex; align-items: center;
  gap: 9px; }
.adm-main .cols h2 { margin-top: 26px; }
.adm-filter { margin: 0 0 18px; display: flex; flex-wrap: wrap; gap: 10px;
  align-items: flex-end; }
.adm-filter label { display: flex; flex-direction: column; gap: 5px; font-size: 12px;
  color: var(--ink-2); }
.adm-filter select, .adm-filter input { background: var(--surface); color: var(--ink);
  border: 1px solid var(--border); border-radius: 8px; padding: 7px 10px; font-size: 13px; }
.adm-filter input[type="search"] { min-width: 230px; }
.adm-filter .btn { margin-bottom: 1px; }
.adm-filter .btn.ghost { background: transparent; }
.adm-main code { font-size: 12.5px; }
/* Paths behind a disclosure: the count is the signal, the list is the evidence, and forty
   of them inline would bury the table. */
details.paths { margin-top: 4px; }
details.paths > summary { cursor: pointer; font-size: 12px; color: var(--ink-2);
  list-style: none; }
details.paths > summary::-webkit-details-marker { display: none; }
details.paths > summary::before { content: "▸ "; }
details.paths[open] > summary::before { content: "▾ "; }
details.paths ul { margin: 6px 0 0; padding-left: 14px; max-height: 210px; overflow-y: auto; }
details.paths li { font-size: 12px; color: var(--ink-2); font-family: ui-monospace,
  SFMono-Regular, Menlo, monospace; list-style: none; }
@media (max-width: 900px) {
  .adm { grid-template-columns: 1fr; gap: 18px; padding-inline: 16px; }
  .adm-nav { position: static; flex-direction: row; flex-wrap: wrap; }
  .adm-lbl { display: none; }
  .adm-nav a { border: 1px solid var(--border); }
}
.adm-main table td, .adm-main table th { white-space: nowrap; }
/* A request path can be arbitrarily long (/.well-known/appspecific/... is 45 characters of
   Chrome probing). Breaking on any character turned one cell into a 13-line column, so the
   path scrolls inside its own cell instead: two lines maximum, the rest reachable by
   dragging. The agency name is chosen in the filter above, so it matters less and may wrap. */
.adm-main table td.path { max-width: 340px; white-space: normal; }
.adm-main table td.path code { display: block; max-width: 100%; overflow-x: auto;
  white-space: nowrap; padding-bottom: 2px; }
/* min-width matters as much as max: with only a max the auto table layout squeezed this
   column until "Properties" broke mid-word across five lines. */
.adm-main table td.agency { min-width: 132px; max-width: 168px; white-space: normal;
  overflow-wrap: break-word; }
.adm-main table td code { white-space: nowrap; }

/* A disabled button here is a state marker, not a dead control: push is already on and the
   row is saying so. Muted rather than greyed to illegibility. */
.btn[disabled] { opacity: .62; cursor: default; pointer-events: none; }

/* Device roster. "this device" is resolved client-side by endpoint hash, so the styling
   hangs off a class the script adds rather than anything the server guessed. */
.sub-row.is-this-device { border-color: color-mix(in srgb, var(--accent) 45%, transparent); }
.sub-row .thisdev-tag { color: var(--accent); font-style: normal; font-weight: 600; }
.sub-row.dead strong { color: var(--ink-2); }
/* Team actions wrap rather than widening the column past the table. */
td .row { gap: 6px; }
/* What each role can do, sitting under the select that grants it. */
.role-notes { margin: 8px 0 0; padding-left: 18px; }
.role-notes li { margin-bottom: 3px; }
.role-notes strong { color: var(--ink-2); }

/* iOS install instructions. Only ever visible on iPhone/iPad outside the Home Screen. */
.ios-install h3 { font-size: 15px; margin: 0 0 8px; display: flex; align-items: center; gap: 8px; }
.ios-install h3 .ic { color: var(--warning); }
.ios-install p { font-size: 13.5px; color: var(--ink-2); line-height: 1.6; margin: 0 0 10px; }
.ios-steps { margin: 0 0 10px; padding-left: 20px; font-size: 13.5px; line-height: 1.8; }
.ios-steps li { color: var(--ink-2); }
.ios-steps strong { color: var(--ink); }

/* Blocked addresses in the usage tables. The chip sits under the device string, and the
   reason is clamped rather than truncated server-side so the full text stays selectable and
   reachable by screen readers - `title` alone is invisible to touch and to keyboard users. */
.adm-main table td .blk { white-space: normal; max-width: 260px; margin-top: 6px; }
.blk-why { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
  overflow: hidden; margin-top: 4px; font-size: 11.5px; line-height: 1.45;
  color: var(--ink-3, var(--ink-2)); }
.blk:hover .blk-why, .blk:focus-within .blk-why { -webkit-line-clamp: unset; overflow: visible; }
/* Tint the whole row, so a blocked address reads as blocked from any column, not only the
   one carrying the chip. Kept low so it never competes with the red used for errors. */
tr.is-blocked > td { background: color-mix(in srgb, var(--critical) 11%, transparent); }
tr.is-blocked > td:first-child code { color: var(--critical); }

/* Email template editor. The shared field rule now covers `textarea`; these override it for
   the editor itself - a monospace face because the body is markup, a much taller box, and the
   flat --plane ground the rest of this screen uses. */
.mailform textarea, .mailform input {
  width: 100%; background: var(--plane); color: var(--ink); border: 1px solid var(--border);
  border-radius: 8px; padding: 9px 10px; font-size: 14px; }
.mailform textarea {
  font: 13px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; resize: vertical;
  min-height: 220px; }
.mailform label { display: block; font-size: 12px; color: var(--muted); margin: 12px 0 5px;
  text-transform: uppercase; letter-spacing: .04em; }
/* White canvas because the message itself is light: a dark frame around a light email makes
   the preview look broken when it is correct. */
/* Tall enough to judge a whole message without scrolling inside a box. The email itself is
   560px wide, so a half-width column showed a letterbox of it and nothing useful. */
.mailprev { width: 100%; height: min(78vh, 900px); min-height: 520px;
  border: 1px solid var(--border); border-radius: 10px; background: #f4f4f2; }
.adm-main table tr.is-current > td { background: color-mix(in srgb, var(--accent) 9%, transparent); }
/* Filter controls on the email admin (template list filter + delivery-log search). These inputs
   live outside .mailform, so without a rule they render as unstyled browser defaults. */
.filterbox, .adm-main input[type="search"], .adm-main select {
  background: var(--plane); color: var(--ink); border: 1px solid var(--border);
  border-radius: 8px; padding: 8px 10px; font-size: 14px; }
.filterbox { width: 100%; max-width: 420px; margin: 0 0 12px; }

/* Group tabs over a long list. The selected tab is marked by weight and an underline as well
   as colour, so it still reads as selected without colour perception. */
.tabrow { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 10px; }
.tabrow .btn { border-bottom: 2px solid transparent; }
.tabrow .btn.is-on { border-bottom-color: var(--accent); color: var(--ink); font-weight: 600; }
.tabrow .btn .muted { margin-left: 6px; font-weight: 400; }
/* The group heading inside the table: a signpost, not a row of data. */
.tplgroup-head th { font-size: 12px; text-transform: uppercase; letter-spacing: .06em;
  color: var(--ink-2); padding-top: 14px; }

/* Sign-up and sign-in screens. */
.authwrap { max-width: 520px; }
.authwrap .card label { display: block; margin: 14px 0 5px; font-size: 13px; color: var(--ink-2); }
.authwrap .card label:first-of-type { margin-top: 0; }
/* The development bypass banner. Sized and coloured to be impossible to stop noticing, because
   a warning people stop seeing is the same as no warning. */
.devbanner { border-left-width: 5px; }
.devpeek { border: 2px dashed var(--critical); background: color-mix(in srgb, var(--critical) 11%, transparent); }
.peekcode { font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; word-break: break-all;
  color: var(--ink); margin: 8px 0 0; }
.peekcode.big { font-size: 30px; letter-spacing: 4px; font-weight: 700; }
.otpinput { font: 700 24px/1.2 ui-monospace, SFMono-Regular, Menlo, monospace;
  letter-spacing: 6px; text-align: center; }

/* Demo account rows on the sign-in page (?demo=true). */
.demo-row { display: flex; align-items: center; justify-content: space-between; gap: 14px;
  padding: 10px 0; border-bottom: 1px solid var(--rule); }
.demo-row:last-of-type { border-bottom: 0; }
.demo-row .addr { display: block; }
/* .rowlink is put here by forms.js, never by the template: without scripting the row stays
   an ordinary row and the button remains the way in. The bleed pulls the hover tint out to
   within 6px of the card edge so it reads as one band rather than a floating stripe. */
.demo-row.rowlink { cursor: pointer; margin: 0 -12px; padding: 10px 12px; border-radius: 8px; }
.demo-row.rowlink:hover, .demo-row.rowlink:focus-within {
  background: color-mix(in srgb, var(--accent) 6%, transparent); }
/* Where there is a real pointer the whole row is the control and the button is duplication,
   so it is clipped rather than removed - a keyboard tab still reaches it and un-clips it,
   which display:none would have made impossible. Touch devices have no hover to discover
   the row with, so they keep the button as it was. */
@media (hover: hover) and (pointer: fine) {
  .demo-row.rowlink > .btn { position: absolute; width: 1px; height: 1px; padding: 0;
    border: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
  .demo-row.rowlink > .btn:focus-visible { position: static; width: auto; height: auto;
    padding: 5px 10px; border: 1px solid var(--border); clip-path: none; }
}

/* --- first-run checklist ----------------------------------------------------- */
.getstarted { margin-bottom: var(--s3); }
/* Three equal cards in a row (wrapping on narrow screens). Each is glass with an icon coin
   top-left and a big translucent step numeral bleeding from the top-right. */
.gs-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: 12px; margin-top: 16px; }
.gs-card { position: relative; overflow: hidden; display: flex; flex-direction: column;
  min-height: 172px;  /* tall enough that the oversized numeral clips ~30% at the bottom */
  border: 1px solid transparent; border-radius: var(--r-tile); padding: 16px 18px 18px;
  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); }
@supports ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
  .gs-card { -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur); }
}
/* The ghost numeral: huge and artistic, its top aligned with the icon coin, painted through the
   glyph with a brand-tint gradient that fades downward so the lower third reads faint - then the
   card's overflow clips that faint tail, leaving ~30% hidden but the digit still legible. Sits
   behind the content (z-index 0); decorative, so aria-hidden in the markup. */
.gs-num { position: absolute; top: 6px; right: 14px; z-index: 0;
  font-family: var(--font-display); font-weight: 800; font-size: 224px; line-height: 0.8;
  letter-spacing: -0.05em; pointer-events: none; user-select: none; color: transparent;
  background-image: linear-gradient(180deg,
    color-mix(in srgb, var(--brand) 50%, transparent) 4%,
    color-mix(in srgb, var(--brand) 20%, transparent) 48%,
    transparent 90%);
  -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; }
.gs-card > .coin { position: relative; z-index: 1; margin-bottom: 14px; }
.gs-title { position: relative; z-index: 1; display: block; font-size: 15px; margin-bottom: 4px; }
.gs-desc { position: relative; z-index: 1; font-size: 13px; color: var(--ink-2);
  line-height: 1.55; margin: 0 0 14px; }
.gs-act { position: relative; z-index: 1; margin-top: auto; }
/* Next undone step: a brighter rim and glow draw the eye, and it carries the only action. */
.gs-card.is-next { box-shadow: inset 0 1px 0 color-mix(in srgb, white 24%, transparent),
              0 16px 40px -18px var(--glow-2); }
/* Done: the whole card goes green. Every tint on the card is mixed from --brand via these
   derived tokens, so re-pointing them at --good re-skins the card green in one place each -
   glass interior glow, rim, and outer glow all follow. Coin turns green with its check; the
   numeral and title follow too. */
.gs-card.is-done { --card-glow: radial-gradient(135% 90% at 16% -12%,
      color-mix(in srgb, var(--good) 20%, transparent), transparent 55%);
  --rim: linear-gradient(135deg, color-mix(in srgb, white 32%, transparent),
      color-mix(in srgb, white 5%, transparent) 42%, color-mix(in srgb, var(--good) 26%, transparent));
  --glow-1: color-mix(in srgb, var(--good) 25%, transparent);
  --glow-2: color-mix(in srgb, var(--good) 45%, transparent); }
.gs-card.is-done > .coin { --coin-hue: var(--good); }
.gs-card.is-done .gs-num { background-image: linear-gradient(180deg,
    color-mix(in srgb, var(--good) 46%, transparent) 4%,
    color-mix(in srgb, var(--good) 18%, transparent) 48%,
    transparent 90%); }
.gs-card.is-done .gs-title { color: var(--muted); }
/* The progress strip the two public forms put in their own place (static/stages.js), and the
   form it stands in for. Neither carries a display of its own today, so the hidden attribute
   already works on both - these two lines are what keeps that true if either ever gains one,
   because a display beats the attribute and the failure is silent: a form that will not hide
   and a strip that was never away. */
.stage-run[hidden], form.stack[hidden] { display: none; }
.stage-run { margin-top: var(--s3); }
/* The heading over the stage cards. Its margins live here rather than inline on the element so
   the gap under it can beat .gs-grid's own 16px top margin - the two collapse, so the larger
   wins, and 16px sat the label right on top of the cards. Scoped to this heading: .gs-grid is
   also the dashboard's first-run checklist, which is not being restyled from here.

   Both classes in the selector on purpose: landing.css styles `.guide h2` and `.guide p`,
   which is (0,1,1) and loads after this file, so a single class here loses the tie and the
   rule silently does nothing. The macro always renders both classes together. */
.gs-head.fs { margin-top: var(--s4); margin-bottom: var(--s3); }

/* The caveat under the stage cards. Was an inline max-width with no top margin, so it sat
   flush against the grid. */
.gs-note.small { max-width: 70ch; margin-top: 12px; }

/* --- the public forms (audit, energy report) ------------------------------------------ */
/* .stack had no rule anywhere, so the card's internal rhythm was whatever each label's own
   margin happened to be: 10px between fields, 4px between the consent line and the button,
   and none at all under it - the small print sat against the button. The list of bullets
   above ran into the card's top edge with 2px between them.
   One rhythm for every child, then more room around the two things that are not fields. */
.guide form.card.stack { margin-top: var(--s3); }
.guide form.card.stack > * + * { margin-top: var(--s2); }
.guide form.card.stack > button { margin-top: var(--s3); }
.guide form.card.stack > p.muted.small { margin-top: var(--s2); margin-bottom: 0; }

/* --- the upsell block at the foot of a guide page ------------------------------------- */
/* It is a <section>, so landing.css gives it the landing rhythm: padding-block var(--s6).
   Inside a guide article that is wrong twice over - 76px of padding, and the padding then
   stops the heading's own var(--s5) margin collapsing through it, so 146px of nothing opened
   up between the stage cards and the next heading. Here it is a block in a column of prose,
   spaced like one. */
.guide .upsell { padding-block: 0; margin-top: var(--s4); }
.guide .upsell > h2 { margin-top: 0; }
/* The buttons were a bare <p> with no rule anywhere, so they sat 2px under the last bullet
   and touched each other on a narrow screen.

   Scoped to .upsell when it was written, and that scope was the bug: producto.html and
   solution.html both close on a bare <p class="actions"> outside any upsell, and so do
   audit_result.html and error.html. All four were asking for this row and getting nothing
   - no flex, no gap, no top margin - because the only rule that defined `actions` required
   an ancestor they do not have. It is a base rule now. Still p.actions and not .actions:
   the admin screens use <div class="actions"> for something else, and widening the
   selector would have restyled those instead of fixing these. */
p.actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: var(--s3); }

/* A dismiss control that looks like a control, not a call to action. */
.linklike { background: none; border: 0; padding: 0; color: var(--muted); font-size: 13px;
  text-decoration: underline; cursor: pointer; }

/* --- done-state rows ---------------------------------------------------------- */
/* The shared collapse for anything whose job is finished: a one-line status row that is the
   summary of a details element holding the full content. Auto-open is the template's job
   (render `open` when the state needs attention); this only styles the closed line so a
   completed setup stops occupying a card. */
details.done > summary { list-style: none; cursor: pointer; display: flex; align-items: center;
  gap: 10px; padding: 10px 14px; background: var(--surface); border: 1px solid var(--border);
  border-radius: 10px; font-size: 14px; }
details.done > summary::-webkit-details-marker { display: none; }
details.done > summary .sum-hint { margin-left: auto; color: var(--muted); font-size: 12px; }
details.done[open] > summary { border-radius: 10px 10px 0 0; border-bottom: 0; }
details.done > .done-body { border: 1px solid var(--border); border-top: 0;
  border-radius: 0 0 10px 10px; padding: 14px; background: var(--surface); }
details.done + details.done { margin-top: 10px; }
/* One-line linked status row for satellite data owned by another page. */
.statline { display: flex; align-items: center; gap: 8px; padding: 8px 0; font-size: 14px;
  color: var(--muted); }
.statline a { color: inherit; }

/* --- shared demo accounts -------------------------------------------------------------
   The demo is read only on the server and edits live in the visitor's own browser, so the
   banner is permanent rather than a flash after a save: the question "where does this go"
   is asked before typing, not after. */
.demo-banner { border-left: 3px solid var(--accent); margin-bottom: var(--s3, 18px); }
.demo-banner .spread { align-items: center; gap: 12px; }
/* Same for the trial countdown: one line of text against a pill button - .spread's default
   flex-end baseline reads as misalignment here, not layout. */
.trial-banner .spread { align-items: center; gap: 12px; }
/* Confirmation printed where the click happened, so it is read as an answer to that Save
   rather than as a page-level status. */
.demo-kept { margin: 10px 0 0; color: var(--ink-2); }
/* The demo's "not saved" notification. Sits under the standing banner rather than floating
   over the page: it answers a click the visitor just made, so it belongs in the flow where
   the reply to that click would be, and it must not cover the form it is talking about. */
.demo-toast { border-left: 3px solid var(--warning); margin-bottom: var(--s3, 18px); }
.demo-toast .spread { align-items: center; gap: 12px; }
.demo-toast[hidden] { display: none; }
/* The field a mismatch warning is actually about, marked on both sides of the comparison.
   Colour alone would not carry it: the tag says "differs" in words, because the whole point
   is that the note above claims a difference the reader could not previously locate. */
tr.differs > th, tr.differs > td {
  background: color-mix(in srgb, var(--serious) 10%, transparent); }
/* .key marks the sample in the legend rather than a flagged row. Same look deliberately -
   it is showing you what to look for - but a distinct class, so "is any row flagged" stays
   answerable by searching the markup for the tag alone. */
.differs-tag { display: inline-block; font-size: 11px; font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; color: var(--serious); border-radius: 999px; padding: 1px 7px;
  border: 1px solid color-mix(in srgb, var(--serious) 45%, transparent); }

/* /admin/visibility: the 14-day triple-bar chart. Three thin bars per day - crawler, human,
   LLM-referred - coloured by meaning, scaled to the window's busiest day. No JS. */
.viz-days th { text-align: left; padding-right: 10px; font-weight: 400; }
.viz-days td { padding: 3px 0; }
.viz-days .bar { margin: 1px 0; min-width: 160px; }
.viz-crawler > span { background: var(--warning); }
.viz-human > span { background: var(--accent); }
.viz-llm > span { background: var(--good); }
