/* =====================================================================
   MESteel v2 — shared stylesheet
   Tokens, base, layout primitives, header/footer/mobile menu and the
   components reused on every page (tier badge, paywall tile, ticker).

   Lifted verbatim from design/homepage.html. Values here are FINAL —
   see design/README.md. Page-specific CSS belongs in the page view,
   never in this file, and never a second copy of the header/footer.
   ===================================================================== */

/* ---------- Tokens ---------- */
:root{
  --ink:#0F172A;
  --blue:#1F3F8F;
  --navy:#0F1F4A;
  --blue-10:#E8EEFA;
  --bg:#F3F6FB;
  --line:#E3E8EF;
  --mute:#64748B;
  --faint:#94A3B8;
  --green:#047857;
  --green-10:#D1FAE5;
  --red:#DC2626;
  --shadow:0 2px 10px rgba(15,31,74,.06);

  /* Membership metals. SILVER = Silver Member (status 2), GOLD = Gold Member
     (status 3). Each is a three-stop set so the badge, the plan card accent
     and the dashboard sidebar can all read from the same metal without
     naming a hex anywhere else.

     **Gold moved down a tier.** It used to be status 2's metal and the
     tokens used to be --gold / --plat. Migration 070 renamed the plans and
     the swap was done here in three passes through a temporary name, because
     a two-pass "gold -> silver, silver -> gold" collapses both tiers onto one
     metal. Anything added here keys off the plan CODE (`full`, `premium`),
     never the display name — the name moved, the code did not.

       --*-hi   the highlight the gradient starts at
       --*      the face colour, and the one a flat 1px border uses
       --*-lo   the shade the gradient ends at
       --*-ink  text on the face. Both are dark on purpose: a metal is a
                light surface, and white on gold fails contrast at 11px.

     Contrast is measured at the DARKEST stop (`--*-lo`), not the face: the
     text sits over the whole gradient, so the darkest point is the worst
     case and the only one that matters.

     That is also a fix. The first gold was #C9A227/#A8841B, and its darkest
     stop was 3.95:1 against #3A2A05 — a FAIL for normal text, which the
     face colour's 5.7:1 hid. Champagne rather than mustard is what was
     asked for, and it makes the badge pass at every point of the gradient
     instead of only the light end.

       gold    (status 3)  worst stop #DCBF62 on #453206   6.82:1   AA
       silver  (status 2)  worst stop #AEB6BD on #2B3138   6.39:1   AA

     Silver is a cooler, darker metal than the platinum it replaces
     (#C2C6CA -> #AEB6BD at the darkest stop): platinum and gold were far
     apart in lightness, silver and gold are the two that now sit beside each
     other on a pricing table and have to be told apart at a glance. It costs
     1.25 of contrast and still clears AA at every stop. */
  --gold:#EFD98E;     --gold-hi:#FAEBB8;    --gold-lo:#DCBF62;    --gold-ink:#453206;
  --silver:#D8DCE0;   --silver-hi:#F0F2F4;  --silver-lo:#AEB6BD;  --silver-ink:#2B3138;

  /* Sell / Buy accents (design/README.md). Three roles each, and every rule
     on the site that means "this is a sell" or "this is a buy" reads one of
     them — there is no second green and no second orange.

       --x      the solid colour: a fill, the tick circle, a strong label
       --x-10   the light tint a pill or chip sits on
       --x-ink  text ON that tint

     `-ink` is a separate token because the solid colour is not always
     readable on its own tint: --buy on --buy-10 measures **4.52:1**, which
     passes AA by two hundredths, where the sell pair is 6.78. #9A3412 brings
     buy to 6.38 and the two sides to the same legibility. */
  --sell:#15803D;   --sell-10:#DCFCE7;   --sell-ink:#065F46;
  --buy:#C2410C;    --buy-10:#FFEDD5;    --buy-ink:#9A3412;
  /* The default for anything themed by data-kind but drawn outside one. */
  --accent:var(--blue); --accent-10:var(--blue-10); --accent-ink:var(--blue);
  --success:#16A34A;
  --error:#DC2626;  --error-10:#FEF2F2;
  --up:#4ADE80;     --dn:#F87171;

  /* Dropdown chevron. Every select in the design uses appearance:none,
     which strips the native arrow without drawing a replacement — leaving
     a dropdown indistinguishable from a text box. Defined once here in
     --mute so it stays on-theme; the register reference already reserves
     24px of right padding for exactly this. */
  --chevron:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%2364748B' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");

  /* Placeholder text. Chrome's own default is around #757575 — dark
     enough to read as a filled-in value — so a hint sits a step lighter
     than --faint. Applied once in the base below. */
  --placeholder:#A8B4C6;
}

/* ---------- Base ---------- */
*{box-sizing:border-box}
/* ═══════════════════════════════════════════════════════════════════════
   THE Z-INDEX SCALE — six layers, defined here and nowhere else.

   ── The thing that is easy to get wrong ────────────────────────────────

   **A z-index only competes inside its nearest stacking-context ancestor.**
   The Country dropdown on /companies carried `z-index:60` and still opened
   UNDERNEATH the gold bar on a company card, which is `z-index:2` — because
   the dropdown sits inside `.hero .head`, which had `z-index:1`. Everything
   in that block is flattened to level 1, so 60 never competed with anything;
   1 lost to 2 and the bar drew over the list.

   So the rule is not "give dropdowns a big number". It is:

     every block that CONTAINS a dropdown sits at --z-raised or above,
     and nothing decorative is ever at or above it.

   ── The layers ─────────────────────────────────────────────────────────

   --z-deco      a wash, a ribbon, a card's metal bar: under the content it
                 decorates, and under everything else
   --z-content   content lifted above a wash in the same box
   --z-raised    a block that holds a dropdown — a filter bar, a hero head.
                 The gap to --z-deco is what the bug above needed
   --z-sticky    the site header, the admin bar, a sticky save row
   --z-dropdown  combos, the account menu, the nav submenus, date pickers
   --z-modal     a modal's backdrop and panel, a full-screen preview
   --z-toast     a floating message, which outranks even a modal because it
                 is what tells you the modal's save failed

   The numbers are far apart on purpose: something squeezed between two
   layers is a thing nobody can place later without re-reading all of this.
   ═══════════════════════════════════════════════════════════════════════ */
:root{
  --z-deco:1;
  --z-content:2;
  --z-raised:10;
  --z-sticky:100;
  --z-dropdown:1000;
  --z-modal:2000;
  --z-toast:3000;
}
/* Alpine hides x-show elements only once it boots; x-cloak covers the gap. */
[x-cloak]{display:none!important}
/* One rule for every form on the site. opacity:1 because Firefox dims
   placeholders on top of whatever colour it is given. */
::placeholder{color:var(--placeholder);opacity:1}
body{margin:0;font-family:Manrope,system-ui,sans-serif;color:var(--ink);background:#fff;-webkit-font-smoothing:antialiased}
a{color:var(--blue);text-decoration:none}
a:hover{color:var(--navy)}
a{transition:color .15s ease,background-color .15s ease,border-color .15s ease,box-shadow .15s ease,transform .15s ease}
h1,h2,h3{margin:0;letter-spacing:-.02em;font-weight:800}
img{max-width:100%}

/* ---------- Layout primitives ---------- */
.wrap{max-width:1280px;margin:0 auto;padding:0 48px}
.sec{padding-top:48px}
.sec-h{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:20px}
.sec-h h2{font-size:30px}
.sec-h a{font-size:14px;font-weight:700;display:inline-flex;gap:4px}
.sec-h a:hover{color:var(--navy);transform:translateX(3px)}
.sec-h .hr{display:flex;align-items:center;gap:12px}
.grad{height:6px;border-radius:3px;margin:-8px 0 16px;background:linear-gradient(90deg,#F4CB2A 0,#E9762B 30%,#9FB4CF 32%,#C2CEDC 58%,#B5D0EA 60%,#CBDFF0 70%,#C5CBD2 72%,#D8DBDE 100%)}
.grid4{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}
.grid6{display:grid;grid-template-columns:repeat(6,1fr);gap:12px}
/* A GRID ITEM DEFAULTS TO `min-width:auto`, SO `1fr` IS NOT A CEILING.
   A track cannot shrink below its content's min-content width, so one
   long activity name on one tile pushed the whole six-track grid wider
   than the row it sits in and the PAGE scrolled sideways — measured at
   1024 as 11px, with the last tile ending 11px past the viewport.

   Pre-existing, and found while measuring the carousel: the same 11px is
   on the previous commit, which was checked by stashing. It is the trap
   this project has now recorded for `.hm-news`, `.pr-scroll`, `.pt-view`
   and `.co-t` — a flex or grid item has to be told it may be narrower
   than its contents. The names then wrap, which is what the two-line
   tile is shaped for. */
.grid4 > *,
.grid6 > *{min-width:0}

/* ---------- Buttons ---------- */
.btn{display:inline-flex;align-items:center;justify-content:center;font-weight:700;font-size:14px;border-radius:999px;padding:10px 18px;cursor:pointer;border:0;font-family:inherit}
.btn,button{transition:color .15s ease,background-color .15s ease,border-color .15s ease,box-shadow .15s ease,transform .15s ease}
.btn:hover,button:not(:disabled):hover{transform:translateY(-1px)}
.btn:active,button:not(:disabled):active{transform:translateY(0)}
.btn-blue{background:var(--blue);color:#fff;box-shadow:0 6px 18px rgba(31,63,143,.3)}
.btn-blue,.btn-blue:hover{color:#fff}
.btn-blue:hover{background:var(--navy);box-shadow:0 8px 22px rgba(31,63,143,.4)}
/* Selling is green, buying is orange — everywhere, so a member can
   tell the two apart before reading the label. */
.btn-sell{background:var(--sell);box-shadow:0 6px 18px rgba(21,128,61,.28)}
.btn-buy{background:var(--buy);box-shadow:0 6px 18px rgba(194,65,12,.28)}
.btn-sell,.btn-buy,.btn-sell:hover,.btn-buy:hover{color:#fff}
.btn-sell:hover,.btn-buy:hover{filter:brightness(.92)}
.btn-ghost{background:none;color:#334155;font-weight:600;padding:9px 14px}
.btn-line{background:#fff;border:1px solid var(--line);color:#334155}
/* --accent, not --blue: inside a [data-kind] these tint to that side, and
   outside one --accent IS --blue, so nothing else on the site moves. */
.btn-line:hover,.btn-ghost:hover{background:var(--accent-10);color:var(--accent-ink)}
.btn-line:focus-visible,.btn-ghost:focus-visible{outline:none;
  box-shadow:0 0 0 2px #fff,0 0 0 4px var(--accent)}
/* The small size the pill rows use. Lived only in picker-page.css, where the
   one stylesheet that needed it could not share it. */
.btn.sm{font-size:12.5px;padding:7px 13px;min-height:34px}

/* ---------- Segmented two-option toggle ----------
   Thin outline, the active half lightly filled in the side's accent. Used for
   the picker's Any / All match choice; .seg in picker.css is the register
   wizard's own, solid-blue and on a grey track, and is left alone. */
.seg2{display:inline-flex;border:1px solid var(--line);border-radius:999px;
  background:#fff;padding:2px;gap:2px}
.seg2 button{border:0;background:none;border-radius:999px;padding:0 13px;
  min-height:30px;font:inherit;font-size:12.5px;font-weight:700;color:var(--mute);
  cursor:pointer;white-space:nowrap}
.seg2 button:hover{background:var(--bg);transform:none}
.seg2 button[aria-pressed="true"]{background:var(--accent-10);color:var(--accent-ink)}
.seg2 button:focus-visible{outline:none;box-shadow:0 0 0 2px #fff,0 0 0 4px var(--accent)}

/* ---------- Plan-choice buttons wear their plan's metal ----------
   "Renew Silver Member", "Upgrade to Gold Member", "Join …" — the button
   that buys a plan, in that plan's metal, so the card's top bar, the badge a
   member will wear and the button that gets them there are one colour.

   Keyed on `data-plan`, the plan CODE, read off the card. Not the plan name:
   the code survives a rename, which is the whole reason 054 kept it. Both an
   ancestor and the button itself are matched, so it works wherever the
   attribute sits.

   `.btn-metal` is an explicit opt-in rather than styling every button inside
   a `[data-plan]` card. That is what keeps the payment step's
   "Pay $X securely by card" brand blue — it is the one unmissable button on
   the page and it should not change colour per plan.

   Contrast is measured at the DARKEST stop, the only figure that means
   anything for text over a gradient:

     gold   (status 3)  rest  6.82:1   hover  8.75:1    AA
     silver (status 2)  rest  6.39:1   hover  9.53:1    AA
     disabled  6.15:1 (#475569 on #E2E8F0)     AA

   Hover shifts the gradient lighter, so it gets brighter AND higher contrast
   at the same time. WCAG exempts a disabled control, but this one reads
   "Raising invoice…" while it is disabled, which is information — so it is
   held to AA too, and --mute would have been 3.86:1. */
.btn-metal{border:0}

[data-plan="full"] .btn-metal,.btn-metal[data-plan="full"]{
  background:linear-gradient(135deg,var(--silver-hi),var(--silver) 55%,var(--silver-lo));
  color:var(--silver-ink);
  box-shadow:inset 0 0 0 1px rgba(43,49,56,.26),0 6px 18px rgba(43,49,56,.16)}
[data-plan="full"] .btn-metal:hover:not(:disabled),
.btn-metal[data-plan="full"]:hover:not(:disabled){
  background:linear-gradient(135deg,var(--silver-hi),var(--silver-hi) 40%,var(--silver));
  box-shadow:inset 0 0 0 1px rgba(43,49,56,.26),0 9px 24px rgba(43,49,56,.24)}
[data-plan="full"] .btn-metal:focus-visible,
.btn-metal[data-plan="full"]:focus-visible{outline:none;
  box-shadow:0 0 0 3px #fff,0 0 0 6px var(--silver-ink),0 6px 18px rgba(43,49,56,.16)}

[data-plan="premium"] .btn-metal,.btn-metal[data-plan="premium"]{
  background:linear-gradient(135deg,var(--gold-hi),var(--gold) 45%,var(--gold-lo));
  color:var(--gold-ink);
  box-shadow:inset 0 0 0 1px rgba(69,50,6,.30),0 6px 18px rgba(69,50,6,.18)}
[data-plan="premium"] .btn-metal:hover:not(:disabled),
.btn-metal[data-plan="premium"]:hover:not(:disabled){
  background:linear-gradient(135deg,var(--gold-hi),var(--gold-hi) 40%,var(--gold));
  box-shadow:inset 0 0 0 1px rgba(69,50,6,.30),0 9px 24px rgba(69,50,6,.26)}
[data-plan="premium"] .btn-metal:focus-visible,
.btn-metal[data-plan="premium"]:focus-visible{outline:none;
  box-shadow:0 0 0 3px #fff,0 0 0 6px var(--gold-ink),0 6px 18px rgba(69,50,6,.18)}

/* One disabled treatment for every metal — a button that cannot be pressed
   should not advertise a plan. Last, so it beats the per-metal rules. */
.btn-metal:disabled{background:#E2E8F0;color:#475569;
  box-shadow:inset 0 0 0 1px rgba(71,85,105,.18);cursor:progress}

/* The spinner, in the metal's own ink rather than white — these buttons are
   light. Same geometry and timing as the pay button's. */
.btn-metal .bspin{width:16px;height:16px;flex:none;border-radius:50%;
  border:2.5px solid currentColor;border-top-color:transparent;
  animation:metalSpin .7s linear infinite;opacity:.75}
@media (prefers-reduced-motion:reduce){.btn-metal .bspin{animation-duration:2.4s}}
/* Its own keyframes rather than pay.css's `paySpin`: the homepage has a metal
   button and does not load pay.css, and an animation naming keyframes that
   are not there simply does not run. */
@keyframes metalSpin{to{transform:rotate(360deg)}}
.btn-metal .bp{display:inline-flex;align-items:center;gap:9px;justify-content:center}

/* Forced colours drops the gradient, which is the only thing telling the two
   metals apart, so give each a border and let the label carry it. */
@media (forced-colors:active){
  .btn-metal{border:1px solid currentColor;background:none}
}

/* ---------- Header / nav ---------- */
.nav{display:flex;align-items:center;gap:32px;height:76px}
/* width:auto is required — the <img> carries width/height attributes (so the
   browser can reserve space and avoid layout shift), and those act as
   presentational hints. Without this the logo renders 196px wide at 34px
   tall instead of 133px, stretching the wordmark. */
.nav img{height:34px;width:auto;border-radius:5px}
.nav nav{display:flex;gap:26px;font-size:14px;font-weight:600;color:#334155;flex-wrap:nowrap;white-space:nowrap}
.nav nav a{color:#334155;padding:8px 12px;border-radius:999px;margin:0 -4px}
.nav nav a:hover{background:var(--blue-10);color:var(--blue)}
/* The page you are on. Colour alone was the whole signal and it is the
   same blue the hover uses, so a moment after the pointer moved there was
   nothing left saying which page this was.

   The rule is `--navy`, the theme's own token, plus a short bar under the
   label — drawn with `::after` and not `text-decoration`, so it sits clear
   of the descenders and keeps the pill's padding. `width` is the label's,
   because the anchor is wider than its text and an underline the width of
   the padding reads as a box. */
.nav nav a.on{color:var(--navy);font-weight:800;position:relative}
.nav nav a.on::after{content:"";position:absolute;left:12px;right:12px;
  bottom:1px;height:2.5px;border-radius:2px;background:var(--navy)}
/* The 1024-1150 band tightens the pill to 7px, so the bar follows it. */
@media (min-width:1024px) and (max-width:1150px){
  .nav nav a.on::after{left:7px;right:7px}
}
.nav .right{margin-left:auto;display:flex;gap:10px;align-items:center;min-width:0}
/* Real company names run long ("HIGHWAY STAR GENERAL TRADING LLC"), and an
   unconstrained button pushed the main nav onto two lines. Truncate instead;
   the full name stays in the title attribute. */
.btn-account{max-width:170px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block;line-height:38px;padding-top:0;padding-bottom:0}
@media (max-width:1279px){.btn-account{max-width:120px}}
/* 1024-1150px is the band between the mobile menu (<=1023) and the 1280
   design width. The reference overflows badly here (scrollWidth 1275 at
   1024); tightening the nav gap keeps ours within the viewport. */
@media (min-width:1024px) and (max-width:1150px){
  .nav nav{gap:12px}
  .nav nav a{padding:8px 7px;margin:0}
  .nav{gap:12px}
}
.nav nav .mcta{display:none}
/* Overlaid header (homepage hero); other pages use .site-header--solid */
.site-header{position:absolute;left:0;right:0;z-index:var(--z-sticky)}
.site-header--solid{position:relative;background:#fff;border-bottom:1px solid var(--line)}
.burger{display:none;width:44px;height:44px;border-radius:12px;border:1px solid var(--line);background:#fff;font-size:18px;cursor:pointer;align-items:center;justify-content:center;color:var(--ink);flex:none}
/* The page behind an open burger panel. `overflow:hidden` alone leaves iOS
   Safari free to rubber-band the document when a flick starts on the
   panel and runs past its end; `overscroll-behavior` is what refuses that
   chain. The panel sets `contain` on itself as well — both halves, because
   either one alone leaves one direction of the gesture working. */
body.menu-open{overflow:hidden;overscroll-behavior:none}

/* ---------- Shared components ---------- */
.card{background:#fff;border:1px solid var(--line);border-radius:16px;box-shadow:var(--shadow)}
/* ── THE LIFT, AND IT IS ONE DEFINITION ───────────────────────────────
   A card that rises a little under the pointer: the directory card, a
   tile, the homepage's featured companies and — opting in by name —
   anything wearing `.lift`.

   THE VALUES ARE TOKENS so a second card cannot be given "the same"
   lift with numbers typed out beside it. That is how two controls meant
   to be one control come to differ by four pixels, which this project
   has recorded for the toggle switch and for the selectable pill.

   `.lift` EXISTS because the four selectors above it are anchors and
   the contact cards are not: a `.ct-card` holds two buttons, so it
   cannot itself be a link, and a selector list that grows by one every
   time a card is added is a list somebody forgets. */
:root{
  --lift-move:scale(1.02);
  --lift-shadow:0 12px 28px rgba(15,31,74,.16);
  --lift-line:#B7C4E4;
  --lift-ease:transform .18s ease,box-shadow .18s ease,border-color .18s ease;
}
a.card,a.tile,.card a.tile,a.co,.lift{transition:var(--lift-ease)}
/* FOCUS INSIDE IT COUNTS. A keyboard user tabbing to the Edit button
   has to see the same thing the pointer shows, and the focus lands on a
   control INSIDE the card rather than on the card — so `:focus-within`
   and not `:focus-visible`, which would never match the card itself. */
a.card:hover,a.tile:hover,a.co:hover,.lift:hover,.lift:focus-within{
  transform:var(--lift-move);box-shadow:var(--lift-shadow);
  border-color:var(--lift-line)}

/* NO MOVEMENT FOR SOMEBODY WHO ASKED FOR NONE — and the shadow stays,
   because it is what says the card is under the pointer. Overriding the
   token rather than the rule means every caller is covered at once,
   including ones written later. */
@media (prefers-reduced-motion:reduce){
  :root{--lift-move:none;--lift-ease:box-shadow .18s ease,border-color .18s ease}
}

.eyebrow{display:inline-flex;align-items:center;gap:8px;font-size:12px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--blue);background:var(--blue-10);padding:6px 12px;border-radius:999px}
.eyebrow i{width:6px;height:6px;border-radius:50%;background:var(--blue)}

.tile{padding:14px 16px;display:flex;align-items:center;gap:12px;color:inherit}
.tile .ic{width:44px;height:44px;border-radius:12px;display:grid;place-items:center;flex:none}
.tile .ic svg{width:26px;height:26px;fill:none;stroke:var(--navy);stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
.tile.hot .ic svg{stroke:#3A2A0A}
.tile b{display:block;font-size:14px;line-height:1.2}
.tile small{display:block;font-size:12px;color:var(--mute);margin-top:2px}

/* Membership badge — Silver Member / Gold Member / Brief.

   One shape everywhere: this same `.pill` carries the badge on a directory
   card, a homepage card, a profile heading and a saved-companies row, so the
   metals are defined once here and nothing re-states the radius or the
   weight. The class is the display bucket from Roles::tier() — `full`,
   `premium`, `brief` — not the plan name, which is in the element's text.

   Silver and Gold are metals rather than flat chips because that is what
   tells them apart at a glance in a list; the 1px inset ring is what keeps
   silver from disappearing into a white card. */
.pill{font-size:11px;font-weight:800;padding:5px 10px;border-radius:999px;margin-left:auto}
/* Each pill carries a 1px inset ring in its own ink: both metals are light
   enough that the badge needs an edge to stay a distinct object on a white
   card. The ring moved with the metal in the 070 swap. */
.pill.full{background:linear-gradient(135deg,var(--silver-hi),var(--silver) 55%,var(--silver-lo));
           color:var(--silver-ink);box-shadow:inset 0 0 0 1px rgba(43,49,56,.26)}
.pill.premium{background:linear-gradient(135deg,var(--gold-hi),var(--gold) 45%,var(--gold-lo));
              color:var(--gold-ink);box-shadow:inset 0 0 0 1px rgba(69,50,6,.30)}
.pill.brief{color:var(--mute);background:#F1F5F9}

/* ── The metal bar on a company card ────────────────────────────────────────
   The same 4px bar the plan cards wear on /membership/upgrade, the same
   gradient, on a card for a company that holds that plan: silver for
   status 2, gold for status 3.

   `.co-metal` is the opt-in and `data-plan` carries the plan CODE, exactly as
   on a plan card — so the two read the same and a rename cannot break either.
   Only the two metal tiers have a rule: Free, Brief and suppressed cards get
   no bar because nothing matches them, not because a bar is painted and then
   hidden.

   The server decides. PricingService::statusMetal() returns null when the
   Settings toggle is off, so the attribute is simply absent and the card is
   byte-for-byte what it was before — no seam, no stray radius, nothing to
   un-paint.

   `overflow:hidden` rather than a matching radius: these cards have three
   different radii (20px in the directory, 16px on the homepage, 14px on a
   saved row) and clipping follows whichever the card has. `inset:0 0 auto`
   means no height is added, so no card moves.

   One stylesheet because the bar is identical everywhere; each page keeps its
   own card sizing. */
/* `isolation:isolate` makes the card its own stacking context, so the bar
   above is bounded by the card whatever number it carries. Belt and
   braces: the z-index is already --z-deco, and this is what stops a
   future edit to it reaching the page again. */
.co-metal[data-plan="full"],.co-metal[data-plan="premium"]{
  position:relative;overflow:hidden;isolation:isolate}
.co-metal[data-plan="full"]::before,.co-metal[data-plan="premium"]::before{
  content:"";position:absolute;inset:0 0 auto;height:4px;
  z-index:var(--z-deco);pointer-events:none}
.co-metal[data-plan="full"]::before{
  background:linear-gradient(90deg,var(--silver-lo),var(--silver-hi) 50%,var(--silver-lo))}
.co-metal[data-plan="premium"]::before{
  background:linear-gradient(90deg,var(--gold-lo),var(--gold-hi) 50%,var(--gold-lo))}

/* Forced colours drops a gradient, so the bar would be a flat block of system
   colour saying nothing. It goes, and the pill — which carries the plan's NAME
   in text — is the identifier, as it is everywhere else. */
@media (forced-colors:active){
  .co-metal[data-plan="full"]::before,.co-metal[data-plan="premium"]::before{display:none}
}

/* ── "Check your spam folder" — partials/spam-note.php ──────────────────────
   A muted helper line, deliberately NOT a banner and not an error colour:
   nothing has gone wrong, it is advice for the one person in twenty whose
   mail went to Junk. In mesteel.css because it appears on the register
   wizard, /forgot-password, the payment step and the dashboard, which load
   four different page stylesheets between them. */
.spam-note{display:flex;align-items:flex-start;gap:8px;margin:10px 0 0;
  font-size:12.5px;line-height:1.55;color:var(--mute);text-wrap:pretty}
.spam-note.tight{margin-top:4px}
.spam-note b{font-weight:700;color:#475569}
.spam-note .sn-ic{width:15px;height:15px;flex:none;margin-top:2px;
  fill:none;stroke:var(--faint);stroke-width:1.8;
  stroke-linecap:round;stroke-linejoin:round}

/* Non-membership pill states, in the shared stylesheet so a page does not
   have to borrow a membership class to get a colour. /admin/review did
   exactly that — `pending ? 'brief' : 'full'` — which was harmless while
   `.full` was plain blue and became wrong the moment it became a metal:
   "All clear" rendered as a membership badge. */
.pill.ok{color:var(--green);background:var(--green-10)}
.pill.wait{color:#92400E;background:#FEF3C7}
/* The third state the pill was missing: finished, withdrawn, over — not
   good and not waiting. `.badge.off` has had these two colours all along;
   this is the same pair on the pill, and NOT called `off`, because
   `.sc-tags .pill.off` is a scoped RED and two meanings on one name is how
   a status comes out the wrong colour on the next page that uses it. */
.pill.quiet{color:var(--mute);background:#F1F5F9}
/* Something a person ended. Not `.wait`, which means "waiting for
   somebody", and not a membership class — see the note on /admin/review
   borrowing `.full` for a colour. */
.pill.bad{color:var(--red);background:#FEF2F2}

/* Forced-colours and print: a gradient is dropped by the first and wastes
   ink in the second, and in both cases the metal is the only thing telling
   Silver from Gold. Give each a border and let the text carry it. */
@media (forced-colors:active),print{
  .pill.full,.pill.premium{background:none;color:inherit;box-shadow:none;border:1px solid currentColor}
}

/* Sell / Buy tag */
.tag{font-size:11px;font-weight:800;padding:4px 9px;border-radius:999px}
/* One colour language site-wide: GREEN is selling / an offer, ORANGE is
   buying / an inquiry. The tokens existed but the tag used blue for buy,
   which put the same colour on "wanted" as on every neutral chip.

   The ink token, not the solid one: --buy on --buy-10 is 4.52:1 and --sell on
   --sell-10 is 4.57:1, both AA by a hair at 11px/800. The -ink pair takes them
   to 6.38 and 7.00 at the same hue. */
.tag.sell{color:var(--sell-ink);background:var(--sell-10)}
.tag.buy{color:var(--buy-ink);background:var(--buy-10)}

/* Anything inside a themed block reads --accent instead of naming a
   colour, so a card, a button and a header stay in step. */
[data-kind="offer"],[data-kind="sell"]{--accent:var(--sell);--accent-10:var(--sell-10);
  --accent-ink:var(--sell-ink)}
[data-kind="inquiry"],[data-kind="buy"]{--accent:var(--buy);--accent-10:var(--buy-10);
  --accent-ink:var(--buy-ink)}

.avatar{width:48px;height:48px;border-radius:14px;background:var(--blue);color:#fff;font-weight:800;display:grid;place-items:center;flex:none;overflow:hidden}
/* With a logo the tile becomes a white frame around it, so a mark with
   its own colours is not laid over the brand blue. */
.avatar.img{background:#fff;box-shadow:inset 0 0 0 1px var(--line)}
.avatar img{width:100%;height:100%;object-fit:contain}
.avatar.brief{background:#F1F5F9;color:var(--mute)}

/* Paywall tile — shown to non-members in place of contact data */
.unlock{margin-top:auto;background:var(--blue-10);border-radius:14px;padding:14px 16px;font-size:13px;display:flex;justify-content:space-between;align-items:center;color:var(--blue)}

/* Price ticker */
.ticker{margin-top:-20px;position:relative;display:flex;gap:28px;align-items:center;font-size:13px;font-weight:700;font-variant-numeric:tabular-nums;background:var(--navy);color:#fff;border-radius:999px;padding:13px 26px;box-shadow:0 12px 30px rgba(15,31,74,.25)}
.ticker .lbl{color:#93A9E8;letter-spacing:.1em;font-size:11px}
.ticker .up{color:var(--up)}
.ticker .dn{color:var(--dn)}
.ticker .all{margin-left:auto;color:#93A9E8;font-weight:600}
.ticker .all:hover{color:#fff;transform:translateX(3px)}

/* Ad slot placeholder */
/* ---------- The pill ----------
   A checkbox that reads as a choice rather than a form control: the
   register wizard, the dashboard pickers and the back office all use
   it, so it lives here rather than in any one of their stylesheets.

   Markup is a <button data-on> with a <span class="tick"> inside —
   not an <input>, because the tick is drawn rather than native. */
/* The pill, verbatim from the reference's [data-chip]. Blue for an
   activity; the sell / buy accents on steps 5 and 6. */
/* The side's colour is --accent, which [data-kind] sets — on the pill itself
   (the dashboard binds it per pill) or on any ancestor (the picker page sets
   it once on its wrapper). Custom properties inherit, so one declaration
   serves both and there is no per-kind copy of the rule to keep in step.
   Outside any data-kind it resolves to blue, which is what Activities wants. */
.pill-opt{display:inline-flex;align-items:center;gap:6px;min-height:40px;white-space:nowrap;padding:0 14px;border-radius:999px;border:1px solid #DDE4F0;background:#fff;color:#334155;font:inherit;font-size:13.5px;font-weight:700;cursor:pointer}
.pill-opt:hover{border-color:var(--accent);transform:none}
/* flex:none, or the tick is a flex item that shrinks: a long label
   squeezes it horizontally and a border-radius:50% circle comes out an
   ellipse. The mobile rules already carried this; the base rule did
   not. */
.pill-opt .tick{display:none;flex:none;width:16px;height:16px;border-radius:50%;
  background:var(--accent);color:#fff;font-size:10px;font-weight:800;
  place-items:center;margin-left:-4px}
.pill-opt[data-on="true"]{background:var(--accent-10);border-color:var(--accent-10);
  color:var(--accent-ink)}
.pill-opt[data-on="true"] .tick{display:grid}

/* Partially selected — a group with some of its products ticked. The same
   circle with a dash, which is what indeterminate looks like, on the tint but
   keeping a visible border so it does not read as "on". */
.pill-opt[data-state="some"]{background:var(--accent-10);border-color:var(--accent);
  color:var(--accent-ink)}
.pill-opt[data-state="some"] .tick{display:grid}

/* A real focus ring. This is the thing a styled native checkbox loses, and
   these pills ARE the checkboxes on /who-buys and /who-sells. */
.pill-opt:focus-visible{outline:none;box-shadow:0 0 0 2px #fff,0 0 0 4px var(--accent)}

@media (max-width:639px){
  /* The reference keeps its chips nowrap at every width, so a name like
     "Merchant Bars (rounds, flats, squares, tees, hexagons)" pushes the
     page sideways on a phone. Let a long one wrap instead. */
  .pill-opt{white-space:normal;text-align:left;max-width:100%;line-height:1.35;
            padding-top:8px;padding-bottom:8px}
  .pill-opt .tick{flex:none}
}

/* ---------- The toggle switch — `.switch` ----------------------------------
   The ONE on/off switch site-wide: a grey pill track with a white round knob,
   navy and the knob to the right when it is on.

   ── Why it is here, and what it replaced ──────────────────────────────────

   There were FOUR stylings of it, and no two agreed:

     .set-toggle    admin.css, 38x22 track / 16px knob, `outline` focus ring
     .ar-sw         dashboard.css, 42x24 / 18px, a doubled box-shadow ring
     .acct-switch   account.css — not a switch at all: a native checkbox
                    tinted with `accent-color`
     (none)         /account's auto-renewal, which is what started this

   Four pixels and a different focus ring between two controls that are meant
   to be the same control, and a third that was a different control
   altogether. This is the fifth component this file has recorded
   learning that a shared component's styles have to travel with it — and the
   first where the cost was not a page missing the rules but two pages quietly
   disagreeing about the size of them.

   One definition, at the LARGER of the two sizes: 24px of track is a better
   touch target than 22, and nothing on the site was built around 22. The
   Settings toggles grow 4px.

   ── A SWITCH, not a checkbox, and the difference is not decoration ────────

   A switch means the change takes effect the moment it moves. A checkbox
   means it is collected and submitted with everything else. So this is for
   "Renew automatically", "Show archived" and a Settings row that saves — and
   deliberately NOT for `is_premium` on an article draft, `remember` on the
   sign-in form, or `is_remote` on a job posting, which are form fields and
   whose plain boxes are right. `role="switch"` says the same thing to a
   screen reader, which is why the markup carries it.

   ── The markup ───────────────────────────────────────────────────────────

     <label class="switch">
       <input type="checkbox" role="switch" x-model="x" x-bind:aria-checked="x">
       <span class="sw" aria-hidden="true"></span>
       <span class="t"><b>Label</b><small>Help</small></span>
     </label>

   A real `<input type=checkbox>` underneath, moved off-screen rather than
   `display:none` — hidden that way it leaves the tab order and Space stops
   working, and the knob is then a decoration nobody can reach. The whole
   label is the hit target, so the text is part of the control.
   `aria-hidden` on the track, because it is the picture of a state the input
   already announces.
   --------------------------------------------------------------------------- */
/* `.switch` is (0,1,0), so a container that styles its own children can
   outrank it — `.fld > span{display:block}` in dashboard.css is (0,1,1) and
   does, which turned the track into an inline element whose width and height
   are ignored: a **0x0** switch beside a label that still read correctly.
   A `.switch` placed inside a field shell has to restate `display`, and
   admin.css's Ads rule says so where it does it. Raising the specificity here
   would not help — (0,1,1) against (0,1,1) is decided by load order, and
   mesteel.css loads first. */
/* ── The password eye ────────────────────────────────────────────────────
   One control for every `type="password"` field on the site, injected by
   `mesteel.js`. In mesteel.css and not login.css, which is where the old
   `.pw-toggle` lived — that is why the eye existed on two of the eight
   screens that have a password field and on none of the other six. A
   component's styles travel with the component.

   The wrapper is `display:block` so the input keeps the width its field
   shell gave it; `.fld`, `.f` and the admin grid all set `width:100%` on
   the input itself, which is unaffected by being wrapped. */
.pw-wrap{position:relative;display:block}
.pw-wrap>input{width:100%;padding-right:44px}
.pw-eye{position:absolute;right:6px;top:50%;transform:translateY(-50%);
  width:34px;height:34px;display:grid;place-items:center;padding:0;
  border:0;background:transparent;color:var(--mute);cursor:pointer;border-radius:8px}
/* THE HOVER MUST RESTATE `translateY(-50%)`. mesteel.css lifts every
   button by a pixel on hover — `button:not(:disabled):hover{transform:
   translateY(-1px)}` — and `transform` is ONE property, not two, so that
   rule REPLACES the centring instead of adding to it: the eye dropped
   about 16px the moment the pointer touched it. The old `.pw-toggle`
   carried the same note; it is the kind of thing a rewrite loses. */
.pw-eye:hover,
.pw-eye:focus-visible{transform:translateY(-50%)}
.pw-eye:hover{color:var(--ink);background:var(--bg)}
.pw-eye:focus-visible{outline:2px solid var(--blue);outline-offset:2px;color:var(--ink)}
.pw-eye svg{display:block}

.switch{display:inline-flex;align-items:flex-start;gap:10px;cursor:pointer}

/* Off-screen, not display:none. See above. */
.switch input{position:absolute;opacity:0;width:0;height:0}

.switch .sw{flex:none;width:42px;height:24px;border-radius:999px;background:#CBD5E1;
  position:relative;transition:background-color .15s ease;margin-top:1px}
.switch .sw::after{content:'';position:absolute;top:3px;left:3px;width:18px;height:18px;
  border-radius:50%;background:#fff;transition:transform .15s ease;
  box-shadow:0 1px 3px rgba(15,31,74,.3)}

.switch input:checked + .sw{background:var(--blue)}
.switch input:checked + .sw::after{transform:translateX(18px)}

/* A doubled ring rather than `outline`, which on a 999px radius draws a
   rounded rectangle a pixel outside a pill and looks like a rendering
   fault. The inner white stop is what keeps it visible on the navy track. */
.switch input:focus-visible + .sw{box-shadow:0 0 0 3px #fff,0 0 0 5px var(--blue)}

/* ── Saving ──
   The track dims and the cursor says why. `cursor:progress` on the LABEL as
   well, because the text is half the hit target and a pointer that changes
   over the knob and not over the words reads as the words being dead rather
   than the control being busy.

   The switch does NOT move until the server confirms. A toggle that jumps
   and then jumps back is a setting somebody believes they have changed — and
   on automatic renewal that belief is about their card being charged. */
.switch input:disabled + .sw{opacity:.5}
.switch:has(input:disabled){cursor:progress}
.switch input:disabled ~ .t{opacity:.7}

/* The text block. `min-width:0` so a long label wraps rather than pushing
   the row wider than its card. */
.switch .t{min-width:0}
.switch .t b{display:block;font-size:13.5px}
.switch .t small{display:block;margin-top:2px;font-size:11.5px;line-height:1.5;
  color:var(--mute);font-weight:600}

/* `.l` is the short On / Off word some callers put beside the track instead
   of a label block. The min-width stops the row reflowing as the word
   changes length. */
.switch .l{font-size:12.5px;font-weight:700;color:var(--mute);min-width:3.4em;
  align-self:center}

/* `.panel` is the grey rounded field the Settings grid wants, where every
   other control is a boxed cell and a bare switch reads as a stray one.
   Everywhere else the switch sits in a card that is already the panel. */
.switch.panel{background:var(--bg);border-radius:12px;padding:12px 14px;
  display:flex;align-items:flex-start}
.switch.panel:hover{background:var(--blue-10)}

.ad{border:1.5px dashed #B7C4E4;background:repeating-linear-gradient(135deg,#F3F6FB 0 12px,#EAF0FA 12px 24px);border-radius:14px;display:grid;place-items:center;color:var(--mute);font-size:12px;font-weight:700;letter-spacing:.08em;text-transform:uppercase}

/* Horizontal scroller arrows (mobile tiles) */
.arrows{display:none;gap:8px;flex:none}
.arrows button{width:44px;height:44px;border-radius:50%;border:1px solid var(--line);background:#fff;color:var(--ink);font-size:18px;cursor:pointer;box-shadow:var(--shadow);display:grid;place-items:center;font-family:inherit}
.arrows button:active{background:var(--bg)}

/* Searchable dropdown (combobox) — mesteel.combo() in mesteel.js.
   The field shell is the host page's own (.f on register, .field on the
   directory); everything from the chevron inwards lives here so both
   pages get the same control. */
.combo{position:relative;--chev-inset:18px}
.combo-row{display:flex;align-items:center;gap:8px;padding-right:calc(var(--chev-inset) + 28px)}
.combo-input{flex:1;min-width:0;cursor:pointer}
.combo-input::placeholder{font-weight:600}
.combo.is-open .combo-input{cursor:text}
/* Drawn rather than set as a background image, so it can flip when open. */
/* Inset by the field's own 18px, so the caret lines up with the text
   rather than sitting against the border. */
.combo-chev{position:absolute;right:var(--chev-inset);top:50%;transform:translateY(-50%);width:22px;height:22px;
            display:grid;place-items:center;color:var(--mute);pointer-events:none;
            transition:transform .18s ease,color .18s ease}
/* Stays slate when open — the reference keeps it grey and lets the blue
   ring carry the state. */
.combo.is-open .combo-chev{transform:translateY(-50%) rotate(180deg)}
/* Broad and shallow: a caret off the 24px icon grid is a third of this
   width and reads as a full stop next to 16px type. */
.combo-chev svg{width:22px;height:11px;stroke:currentColor;stroke-width:2.2;fill:none;
                stroke-linecap:round;stroke-linejoin:round}

/* At least as wide as its field, but free to grow for a long option —
   "Membership, then A-Z" hangs off a 200px control, and the directory's
   176px country field would otherwise wrap "Turks & Caicos Is". */
.combo-list{position:absolute;left:0;right:auto;width:max-content;min-width:100%;
            max-width:min(420px,calc(100vw - 32px));
            top:calc(100% + 10px);z-index:var(--z-dropdown);background:#fff;
            border-radius:16px;padding:8px;max-height:296px;overflow-y:auto;overscroll-behavior:contain;
            box-shadow:0 24px 60px rgba(15,31,74,.18),0 0 0 1px rgba(15,31,74,.06);
            animation:comboIn .16s ease-out}
@keyframes comboIn{from{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:none}}
.combo-list::-webkit-scrollbar{width:10px}
.combo-list::-webkit-scrollbar-track{background:transparent}
.combo-list::-webkit-scrollbar-thumb{background:#CBD5E1;border-radius:999px;border:3px solid #fff}
/* The dial-code field is 112px wide; its panel is not. */
.combo-list.wide{right:auto;min-width:280px}
/* Anchored right — set in the markup for a toolbar control, and added
   by openList() when a panel would otherwise run off the viewport. */
.combo-list.combo-right{left:auto;right:0}

/* PINNED TO THE VIEWPORT.
 *
 * `position:absolute` is clipped by any ancestor that scrolls, and
 * `.table-wrap` is `overflow-x:auto` — which per spec makes the other axis
 * clip too, so an inline combo in a table row showed one option and the rest
 * was cut off by the container. On a two-row table that is the whole menu.
 *
 * openList() measures the control and sets left/top/bottom/min-width/
 * max-height inline, so everything positional is overridden here rather than
 * fought with: `min-width:100%` in particular would resolve against the
 * viewport once this is fixed, and `top:calc(100% + 10px)` means nothing. */
.combo-list.combo-pinned{position:fixed;top:auto;left:auto;right:auto;bottom:auto;
                         min-width:0;margin:0}
/* Opening upward: the shadow and the entry animation come from below. */
.combo-list.combo-up{animation:comboUp .16s ease-out}
@keyframes comboUp{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}

.combo-opt{display:flex;align-items:center;gap:12px;width:100%;min-height:46px;padding:10px 12px;white-space:nowrap;
           border:0;border-radius:12px;background:none;font:inherit;font-size:15.5px;font-weight:800;
           color:var(--ink);text-align:left;cursor:pointer;letter-spacing:-.01em}
.combo-opt:hover{transform:none}
.combo-opt:hover,.combo-opt.on{background:var(--bg)}
.combo-opt[aria-selected="true"]{background:var(--blue-10)}
/* The unmatched remainder recedes, so the typed run reads as the match. */
.combo-opt .pre,.combo-opt .post{color:var(--mute);font-weight:600}
.combo-opt[aria-selected="true"] .pre,.combo-opt[aria-selected="true"] .post{color:var(--ink);font-weight:700}
.combo-opt .sub{color:var(--mute);font-weight:600;font-size:13.5px}
.combo-opt .tick{margin-left:auto;flex:none;color:var(--blue);font-size:15px;font-weight:800}
.combo-none{padding:18px 12px;color:var(--mute);font-size:13.5px;font-weight:600}

/* Bare shell — no box, for a control that sits inline in a toolbar
   (the directory's Show / Sort). The label runs alongside the value. */
.combo-bare{display:inline-flex;align-items:center;gap:6px;--chev-inset:0px}
.combo-bare .lbl{font-size:inherit;color:var(--mute);white-space:nowrap}
.combo-bare .combo-row{padding-right:22px}
.combo-bare .combo-input{font:inherit;font-weight:800;color:var(--ink);border:0;background:none;
                         outline:0;padding:0;width:var(--combo-w,140px);cursor:pointer}
.combo-bare .combo-chev{width:16px;height:16px}
.combo-bare .combo-chev svg{width:16px;height:8px;stroke-width:2.6}
.combo-bare.is-open .combo-input{color:var(--blue)}

/* ── The flag, one component ────────────────────────────────────────────────
   `.flag` is the whole behaviour: the ISO2 code sits UNDER the image, so a
   country whose SVG is missing degrades to a readable grey chip instead of a
   broken-image gap, and the <img>'s own onerror hides only the image.

   It used to exist twice — here as `.combo-flag`, with the fallback, and as a
   bare `<img class="flag">` in four views without one. Those four are
   partials/flag.php now and wear this class; the combo keeps `.combo-flag`
   alongside it for its own size.

   The SIZE is deliberately not set here. Every caller sizes it for its
   context — 18x13 in a card and on a profile, 16x12 in a saved-companies row,
   22x16 in a dropdown — and those rules live with the pages that own them.

   With the complete flag-icons set vendored (271 files, every ISO code the
   data uses) the chip should never be seen. It stays because "should" is not
   "is": a country added later with no artwork would otherwise be a hole. */
.flag{position:relative;display:inline-grid;place-items:center;overflow:hidden;
      background:#EEF2F9;color:var(--faint);font-size:7.5px;font-weight:800;line-height:1}
.flag .flag-code{position:relative;z-index:0}
.flag img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;z-index:var(--z-content)}
.flag.blank{visibility:hidden}

/* The dropdown's size and radius only; everything else comes from .flag. */
.combo-flag{width:22px;height:16px;flex:none;border-radius:3px;font-size:8px;
            box-shadow:0 0 0 1px rgba(15,31,74,.12)}

/* ---------- Footer ---------- */
/* Scoped to the site footer. As a bare `footer` selector this painted
   every <footer> element on the site navy — the invoice document has
   one of its own, and its grey small print ended up on a dark band.
   The descendant rules below stay as they are; they only ever match
   inside this one. */
footer.site-footer{background:linear-gradient(180deg,var(--blue),var(--navy));
  color:#fff;margin-top:24px}
footer .wrap{padding:56px 48px 28px;display:block;color:#fff}
/* ── FIVE COLUMNS ON ONE ROW ──
   The brand, then the four link groups: Market, Directory,
   Membership, Talk to us. It was four tracks holding five children,
   so the fifth wrapped to a second row and had to be placed by hand.
   A track each means auto-flow puts them where they belong and the
   placement rule goes.

   THE GAP NARROWS WITH THE VIEWPORT, 20px at 1024 to 40px at 1440,
   because a fifth track takes its width out of the other four: at a
   flat 40 the columns are 137px at 1024 and "Send a suggestion" is
   about 125 of them. `clamp(20px, 4.807vw - 29.23px, 40px)` is those
   two points solved — 20px over 416px of viewport is 4.807vw with a
   -29.23px intercept — and the ends hold, so 1920 gets the 1440
   figure and nothing below 1024 is touched.

   THE BRAND KEEPS THE WIDTH IT HAD. `2fr` against four `1fr` leaves
   it 341px at 1440, which is the 339 it measured as a four-track
   grid — a fifth track otherwise takes 60px out of it and its
   description wraps a line further. The link columns are then 171px
   at 1440 and 141px at 1024, against a longest label ("Send a
   suggestion") that measures 128 and 121 at those two widths. */
footer .ftop{display:grid;grid-template-columns:2fr repeat(4,1fr);gap:clamp(20px,4.807vw - 29.23px,40px);padding-bottom:40px;border-bottom:1px solid rgba(255,255,255,.15)}
footer .brand img{height:44px;width:auto}
footer .brand p{margin:16px 0 0;font-size:14px;line-height:1.6;color:#B8C4E6;max-width:320px}
footer .brand .social{display:flex;gap:8px;margin-top:20px}
footer .social a{width:44px;height:44px;border-radius:12px;display:grid;place-items:center;background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.15);color:#fff}
footer .social a:hover{background:#fff;color:var(--blue)}
footer .social svg{width:20px;height:20px;fill:currentColor}
footer .col h4{margin:0 0 14px;font-size:12px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:#93A9E8}
footer .col nav{display:flex;flex-direction:column;gap:0}
footer .col nav a{color:#DCE3F5;font-size:14.5px;font-weight:600;padding:6px 0;display:inline-block}
footer .col nav a:hover{color:#fff}
footer .fbot{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:12px 24px;padding-top:22px;font-size:12.5px;color:#93A9E8}
footer .fbot nav{display:flex;gap:18px;flex-wrap:wrap}
footer .fbot nav a{color:#B8C4E6}
footer .fbot nav a:hover{color:#fff}

/* ---------- Responsive: tablet (<=1023px) ---------- */
@media (max-width:1023px){
  .wrap{padding:0 24px}
  .nav{gap:16px}
  .nav nav{display:none;position:fixed;top:76px;left:0;right:0;bottom:0;background:#fff;flex-direction:column;gap:0;padding:8px 24px 32px;font-size:18px;overflow:auto;z-index:var(--z-dropdown);border-top:1px solid var(--line)}
  .nav nav.open{display:flex}
  .nav nav a{padding:16px 0;border-bottom:1px solid #F1F5F9;min-height:44px;display:flex;align-items:center}
  /* ── A parent with children, in the burger ──
     It becomes a ROW: the label on the left going to its own page, the
     chevron on the right opening the children underneath. Stacking the
     children in place rather than flying a card out is the only thing that
     works on a phone — a panel positioned against the parent would sit over
     the rest of the menu with no room to go. */
  .nav nav .nav-group{display:grid;grid-template-columns:1fr auto;
    align-items:center;width:100%;position:static}
  .nav nav .nav-group > a{border-bottom:0}
  .nav nav .nav-caret{padding:16px 8px;min-height:44px;min-width:44px;
    justify-content:center;opacity:1}
  .nav nav .nav-caret svg{width:14px;height:9px}

  /* `position:static` on the group above is what lets this be a plain block
     in the flow instead of a floating card. */
  .nav nav .nav-panel{position:static;grid-column:1 / -1;
    box-shadow:none;border:0;border-radius:0;padding:0 0 8px 14px;
    background:none;min-width:0}
  .nav nav .nav-panel:before{display:none}
  .nav nav .nav-panel a{padding:12px 0;font-size:16px;min-height:44px;
    display:flex;align-items:center;border-radius:8px;padding-left:10px}

  /* The row above the children keeps its rule; the group draws its own so
     the children sit inside it rather than below the line. */
  .nav nav .nav-group{border-bottom:1px solid #F1F5F9}
  /* In the burger panel the rows already carry a bottom rule, so a second
     bar under the label would read as one of those. A navy bar down the
     LEFT instead — the same colour, in the one direction nothing else on
     this panel uses. */
  .nav nav a.on{padding-left:12px}
  .nav nav a.on::after{left:0;right:auto;top:8px;bottom:8px;
    width:3px;height:auto;border-radius:3px}
  .nav nav .mcta{display:flex;gap:10px;margin-top:20px}
  .nav nav .mcta a{flex:1;border:0;padding:14px;justify-content:center;min-height:48px;font-size:15px}
  .nav nav .mcta .btn-ghost{background:var(--bg);color:var(--ink)}
  /* `.nav nav a` (0,2,1) outranks `.btn-blue` (0,1,0), so the reference
     renders the mobile Join CTA as slate-on-blue (~2.5:1). Restore white
     text per design/README.md "keep text readable — never black". */
  .nav nav .mcta .btn-blue{color:#fff}
  .burger{display:inline-flex}
  .nav .right{gap:8px}
  .nav .right .btn-ghost{display:none}
  .btn{min-height:44px}
  .grid4{grid-template-columns:repeat(2,1fr)}
  .grid6{grid-template-columns:repeat(3,1fr)}
  .ticker{flex-wrap:wrap;border-radius:20px;gap:12px 20px}
  .ticker .all{margin-left:0;width:100%}
  .scroller .arrows{display:none}
  .unlock{min-height:48px}
  footer .wrap{padding:44px 24px 24px}
  footer .ftop{grid-template-columns:1fr 1fr;gap:32px 24px}
  footer .brand{grid-column:1/-1}
  footer .fbot{flex-direction:column;text-align:center;justify-content:center}
  footer .fbot nav{justify-content:center}
  footer .col nav a{padding:10px 0}
}

/* ---------- Responsive: mobile (<=639px) ---------- */
@media (max-width:639px){
  .wrap{padding:0 16px}
  .nav .right .btn-blue{padding:10px 14px;font-size:13px}

  /* THE SIGNED-IN HEADER OVERFLOWED AT 360 and was 12px over at 344,
     on every page — the logo, the account chip and the burger do not
     fit on one phone row once the chip carries a name as well as an
     avatar. Measured against clientWidth: 362px of content in a 361px
     viewport, so a page-wide sideways scroll for one pixel.

     The name and the chevron go, not the chip: the avatar is the
     target and the menu it opens repeats the company name and the
     email in its own header, so nothing is lost. `max-width` alone
     would not do it — `.nm` already ellipsises, and an ellipsis still
     occupies the width it was given. */
  .acct .nm,.acct .cv{display:none}
  .acct > button{padding:4px;gap:0}
  .ad{height:auto!important;min-height:64px;padding:14px;text-align:center}
  .sec{padding-top:36px}
  .sec-h{flex-direction:column;align-items:flex-start;gap:4px}
  .sec-h h2{font-size:24px}
  .scroller{position:relative}
  .scroller .sec-h{flex-direction:row;align-items:center;justify-content:space-between;gap:12px}
  .scroller .sec-h .hr a{display:none}
  .scroller .grid4,.scroller .grid6{display:flex;gap:10px;overflow-x:auto;scroll-snap-type:x mandatory;scrollbar-width:none;-webkit-overflow-scrolling:touch;padding:2px 0 6px;margin:0 -16px;padding-left:16px;padding-right:16px;scroll-padding-left:16px}
  .scroller .grid4::-webkit-scrollbar,.scroller .grid6::-webkit-scrollbar{display:none}
  .scroller .tile{flex:0 0 200px;scroll-snap-align:start;min-height:72px}
  .scroller .arrows{display:flex}
  .ticker{flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none;-webkit-overflow-scrolling:touch;white-space:nowrap;gap:20px;border-radius:999px;font-size:12px;padding:12px 18px}
  .ticker::-webkit-scrollbar{display:none}
  .ticker>*{flex:none}
  .ticker .all{width:auto;margin-left:8px;padding-right:6px}
  .co{flex-wrap:wrap}
  .co .pill{margin-left:auto}
  footer .wrap{padding:36px 16px 20px}
  /* TWO COLUMNS ON A PHONE, not one: the four link groups are short
     lists of short words, and stacked singly they were four screens of
     scrolling under a logo. The brand stays full width above them (the
     1023 block says so) and the legal line full width below. */
  footer .ftop{grid-template-columns:1fr 1fr;gap:26px 18px}
}

/* ---------- Account menu (header, signed in) ----------
   design/dashboard.html keeps this in the header; it is the only one in
   the app, so pages must not draw a second. */
.acct{position:relative;flex:none}
.acct > button{display:inline-flex;align-items:center;gap:10px;border:1px solid var(--line);background:#fff;border-radius:999px;padding:4px 12px 4px 4px;min-height:40px;color:var(--ink);font:inherit;cursor:pointer;max-width:230px}
.acct > button:hover{border-color:var(--blue);transform:none}
.acct .ini{width:32px;height:32px;border-radius:10px;background:var(--blue);color:#fff;font-weight:800;font-size:12px;display:grid;place-items:center;flex:none;overflow:hidden}
/* With a logo the tile is a frame, not a coloured badge. */
.acct .ini.img{background:#fff;box-shadow:inset 0 0 0 1px var(--line)}
.acct .ini img{width:100%;height:100%;object-fit:contain}
.acct .nm{font-size:13.5px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.acct .cv{font-size:10px;color:var(--mute);flex:none}
/* ---------- A menu item with a submenu ----------
   The parent is a LINK and the chevron is a BUTTON beside it. One element
   cannot both navigate and open a panel, and on a phone the parent's label
   has to keep going to its own page — /prices is the published reporting and
   /marketplace is both lists, so a parent that only opened a menu would take
   two pages off the site. */
/* `gap:1px` and no side padding on the caret below: the two chevrons
   added 28px to a bar that had 4px of slack at 1024, and the links
   themselves are not negotiable. */
.nav-group{position:relative;display:inline-flex;align-items:center;gap:1px}

.nav-caret{appearance:none;border:0;background:none;padding:4px 0;margin:0;
  display:inline-flex;align-items:center;cursor:pointer;color:inherit;
  opacity:.7;transition:opacity .15s ease,transform .15s ease}
.nav-caret:hover{opacity:1}
.nav-caret:focus-visible{outline:2px solid currentColor;outline-offset:2px;border-radius:4px}
.nav-group:has(.nav-panel:not([style*="display: none"])) .nav-caret{transform:rotate(180deg);opacity:1}

/* --z-dropdown, like every other panel that floats. The header itself is
   --z-sticky, and this is inside it, so it needs nothing further to clear
   the page — but it does need to clear the hero below the header, which is
   what a bare `z-index:1` here would have failed at. */
.nav-panel{position:absolute;top:calc(100% + 10px);left:0;min-width:208px;
  background:#fff;border:1px solid var(--line);border-radius:14px;
  box-shadow:0 20px 50px rgba(15,31,74,.18);padding:6px;
  z-index:var(--z-dropdown);display:flex;flex-direction:column}

/* The gap between the parent and the panel is hoverable, or the panel shuts
   as the pointer crosses it. It is a pseudo-element rather than padding so
   the card's own edge stays where it looks. */
.nav-panel:before{content:"";position:absolute;left:0;right:0;top:-12px;height:12px}

.nav-panel a{display:block;padding:9px 12px;border-radius:9px;
  font-size:14.5px;font-weight:600;color:var(--ink);white-space:nowrap;
  border:0;text-decoration:none}
.nav-panel a:hover{background:var(--bg)}
.nav-panel a:focus-visible{outline:2px solid var(--blue);outline-offset:-2px}

/* The current page inside the panel. `.on` in the bar is an underline on a
   translucent header; in a white card it is a tint, because an underline on
   one row of a list reads as a link rather than as "you are here". */
.nav-panel a.on{background:var(--blue-10);color:var(--blue)}
.acct-menu{position:absolute;right:0;top:calc(100% + 8px);width:280px;background:#fff;border:1px solid var(--line);border-radius:18px;box-shadow:0 20px 50px rgba(15,31,74,.18);padding:8px;z-index:var(--z-dropdown)}
.acct-menu .who{display:flex;gap:12px;align-items:center;padding:10px 12px 12px;border-bottom:1px solid #F1F5F9;margin-bottom:6px}
.acct-menu .who .ini{width:40px;height:40px;border-radius:12px;background:var(--blue-10);color:var(--blue)}
/* Scoped to the text block. Unscoped, ".acct-menu .who span" also matched
   the initials chip and its display:block beat the grid centring on
   ".acct .ini", pushing "ME" into the corner of its square. */
.acct-menu .who > div{min-width:0}
.acct-menu .who > div b{display:block;font-size:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.acct-menu .who > div span{display:block;font-size:12px;color:var(--mute);font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.acct-menu a,.acct-menu button{display:flex;align-items:center;gap:12px;width:100%;border:0;background:none;text-align:left;font:inherit;font-size:13.5px;font-weight:700;color:var(--ink);padding:10px 12px;border-radius:12px;cursor:pointer;min-height:42px;text-decoration:none}
.acct-menu a:hover,.acct-menu button:hover{background:var(--bg);color:var(--ink);transform:none}
.acct-menu .danger,.acct-menu .danger:hover{color:var(--red)}
/* Fixed box with its own line-height so a text glyph and an emoji sit
   on the same centre line. */
.acct-menu .ic{width:20px;height:20px;flex:none;color:var(--faint);font-size:13px;line-height:20px;text-align:center;font-style:normal}

/* Soft page ground — the dashboard sits on --bg, not white. */
body.bg-soft{background:var(--bg)}

/* =====================================================================
   Page hero — the photo band on /news, /prices, /events and /jobs.

   Shared rather than per-page because these four are the same component,
   and because the directory learned the hard way that a second copy of a
   hero drifts. The photograph and its crop come in as custom properties
   from partials/page-hero.php, so a page swaps its own image in without
   a rule here changing.
   ===================================================================== */
.ed-hero{--hero-img:url(../img/hero-warehouse.jpg);--hero-pos:center 65%;
         position:relative;height:300px;background:var(--hero-img) var(--hero-pos)/cover}
/* The wash keeps the left half readable while the photograph survives on
   the right; the bottom fade hands over to the page's own background. */
.ed-hero:before{content:"";position:absolute;inset:0;z-index:0;
  background:linear-gradient(90deg,rgba(255,255,255,.95) 0%,rgba(255,255,255,.78) 42%,rgba(255,255,255,.14) 100%)}
.ed-hero:after{content:"";position:absolute;left:0;right:0;bottom:0;height:90px;
  background:linear-gradient(180deg,rgba(244,247,252,0),var(--bg))}
.ed-hero > *{position:relative;z-index:var(--z-content)}

.ed-hero .head{position:absolute;left:0;right:0;bottom:26px}
.ed-hero .kick{font-size:12px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--blue)}
.ed-hero .line{display:flex;align-items:baseline;gap:16px;flex-wrap:wrap;margin-top:10px}
.ed-hero h1{font-size:44px;line-height:1;letter-spacing:-.03em;margin:0}
.ed-hero .meta{font-size:13px;font-weight:700;color:var(--mute);background:rgba(255,255,255,.72);
               border-radius:999px;padding:5px 12px}
.ed-hero p{margin:12px 0 0;font-size:15px;line-height:1.55;color:#475569;max-width:60ch;
           text-shadow:0 1px 0 rgba(255,255,255,.6)}

/* THE WASH IS BUILT FOR A WIDE BAND, and under about 1100px it is not one.

   The stops above fade to 14% white at the right edge, which is right at
   1440: the line under the heading is 60ch inside the washed left half and
   the photograph survives beside it. Narrower, that line reaches across the
   whole band and into the part with almost no wash on it.

   Measured on the rendered page with the text hidden, taking the darkest
   ground inside the line's own rectangle: #475569 on #89898D is 2.17:1 at
   360 and 3.07:1 at 768, against 5.39 at 1440. A fail on /news, /prices,
   /events and /jobs, and it has been there since the band was built — the
   heading above it is 12-14:1 and was never in question, which is why
   nothing showed it.

   1100 rather than the 767 below it because 768 fails too: that width uses
   the DESKTOP rule, where the head is absolutely positioned in a 300px band
   and the paragraph is 556px wide against a gradient that is spent by 322.
   After: 5.3-5.5 at every width. */
@media (max-width:1100px){
  .ed-hero:before{background:linear-gradient(90deg,rgba(255,255,255,.97) 0%,
    rgba(255,255,255,.90) 55%,rgba(255,255,255,.70) 100%)}
}

@media (max-width:767px){
  .ed-hero{height:auto;padding:104px 0 26px}
  .ed-hero:after{display:none}
  .ed-hero .head{position:relative;bottom:auto}
  .ed-hero h1{font-size:31px}
  .ed-hero p{font-size:14.5px}
}

/* ---------- The detail page's photo band ----------
   One article, one event, one vacancy, one offer. It is the SECTION's hero —
   the photograph /news, /events, /jobs or /marketplace opens with, from the
   same Settings row — and nothing is written on it: the <h1> is at the top of
   the item where it has always been, and drawing it here too would give the
   page two of them. partials/detail-hero.php reads the section off the path.

   IT USED TO BE THE ITEM'S OWN PICTURE, BLURRED, and the blur was not
   decoration: a featured image is whatever the newsroom attached — a tall
   event poster, a mill logo, a square headshot — and `cover` on a 260px strip
   turns a poster into three enormous letters. Blurring made any aspect ratio
   read as a band of colour taken from the story.

   A section hero needs none of that. It is a landscape photograph chosen for
   a band this shape, so it is shown as it is, and the item's own picture goes
   where it can be looked at — the top of the article body. That also means
   every page in a section opens the same way, which a per-item picture never
   could: 4,516 of 5,208 articles have no featured image at all.

   SHORTER than the 300px landing band — about two thirds — so the thing
   somebody followed a link to starts higher up the page. */
.art-hero{--hero-img:url(../img/hero-warehouse.jpg);--hero-pos:center 50%;
  height:200px;position:relative;overflow:hidden;
  background:var(--navy) var(--hero-img) var(--hero-pos)/cover}

/* The wash. Strong at the top because the header is drawn over it and the
   logo has to stay readable against a picture nobody chose for contrast. */
.art-hero:before{content:"";position:absolute;inset:0;z-index:var(--z-deco);
  background:linear-gradient(180deg,rgba(255,255,255,.96) 0,rgba(255,255,255,.62) 40%,
    rgba(255,255,255,.35) 100%)}
.art-page.has-hero{padding-top:26px}

/* The header is ~76px and drawn over the band, so this is the floor rather
   than a preference: much under 150 and the logo sits on the page below. */
@media (max-width:767px){ .art-hero{height:150px} }

/* =====================================================================
   `stack-sm` — a table whose rows become cards below 560px.

   OPTED INTO, never applied to `table.inv` at large: Companies and
   Accounting are read as grids of numbers and a stack of them would be
   worse. Admin → Events and Advertising use it, and so does the reader's
   own invoice list on /account — which is why it is here rather than in
   admin.css, where it started. That page loads dashboard.css and its own
   sheet, so the rules simply never arrived and the Actions column sat
   off the right-hand edge.

   `thead` goes, so each cell carries its own label in `data-l`. Without
   one a stack reads "16-18 May 2027 / Published / 1 Jun 2027" and the
   two dates are not tellable apart.
   ===================================================================== */
@media (max-width:560px){
/* ---------- A ROW BECOMES A CARD ----------
   Events and Advertising only, and opted into with `stack-sm` rather than
   applied to `table.inv`: Companies, Accounting and the rest are read as
   grids of numbers and a stack of them would be worse, not better.

   THE PICTURE IS WHY. A <td> is a column wherever the row is drawn, so
   inside a table the thumbnail can only ever have the width the column
   algorithm leaves it - about 110px of a 460px minimum at 360, with the
   title wrapping one word per line beside it. Taking the row out of the
   table layout is the only way the poster gets the width of the card.

   It also fixes what was already wrong here: at 360 the table was 460px
   wide inside a horizontally scrolling box, so State, Expires and every
   button were off the right-hand edge and reachable only by dragging the
   table sideways.

   `thead` goes, so each cell carries its own label in `data-l`. Without
   one a stack reads "16-18 May 2027 / Published / 1 Jun 2027", and the
   two dates are not tellable apart. */
table.stack-sm{display:block}
table.inv.stack-sm,table.stack-sm{min-width:0}
table.stack-sm thead{display:none}
table.stack-sm tbody,table.stack-sm tr,table.stack-sm td{display:block;width:auto}
table.stack-sm tr{padding:16px 0;border-top:1px solid #E2E8F0}
table.stack-sm tbody tr:first-child{border-top:0;padding-top:4px}
table.stack-sm td{border:0;padding:3px 0;white-space:normal}
table.stack-sm td:empty{display:none}
table.stack-sm td[data-l]:before{content:attr(data-l);
  font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;
  color:var(--mute);margin-right:8px}
/* The right-aligned columns are a table idea; in a stack they read as a
   label and its value, left to right like everything else. */
table.stack-sm td.r{text-align:left;padding-right:0}
table.stack-sm td.nowrap{white-space:normal}
/* The buttons get their own line and wrap onto a second if they must. */
table.stack-sm td.r:last-child{display:flex;flex-wrap:wrap;gap:6px;padding-top:10px}
table.stack-sm td.r:last-child:before{width:100%}
}

/* A slot that has something in it stops being a dashed placeholder. */
.ad[data-filled="true"]{border:0;background:none;padding:0;height:auto!important;min-height:0}
.ad-link{display:block;line-height:0}
.ad-link img{display:block;max-width:100%;height:auto;border-radius:10px}

/* ── The placeholder in an unbooked slot ─────────────────────────────
   A SALES SLOT, NOT A GAP. It has to read as "this space is for sale"
   and never as an advert somebody paid for, so it keeps the dashed
   outline and the muted uppercase "Advertisement" the empty box always
   had, and adds the one thing it was missing: somewhere to go.

   `.ad[data-filled="true"]` above already strips the border, the
   background and the padding the moment a banner lands, and ads.js
   replaces the box's contents outright — so this cannot appear beside a
   real advert. */
.ad-ph{display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:7px;padding:10px;text-align:center;line-height:1.35}
.ad-ph-t{font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;
  color:var(--mute)}

/* The link is the only non-muted thing in the box, and it is a LINK
   rather than a button: a button here would read as the page's own call
   to action rather than as a note about the space. */
.ad-ph-l{font-size:12.5px;font-weight:600;letter-spacing:0;text-transform:none;
  color:var(--blue);text-decoration:none;border-bottom:1px solid transparent}
.ad-ph-l:hover{border-bottom-color:currentColor}
.ad-ph-l:focus-visible{outline:2px solid var(--blue);outline-offset:3px;border-radius:3px}

/* A filled slot has no placeholder left to show, and a 300×250 box that
   is 90px tall in the markup must not stretch to its own min-height once
   the banner is in. Both are the `[data-filled]` rule above; this is the
   half it could not know about, because the height now comes from an
   inline `min-height` the partial writes. */
.ad[data-filled="true"]{min-height:0!important}

/* ---------- Password strength ----------
   The bar from design/register.html: a thin track under the field with a
   word beside it. Shown only where a password is being chosen — on a
   sign-in form it would tell an attacker how close they are getting. */
.pw-meter{display:flex;align-items:center;gap:10px;margin-top:8px}
.pw-track{flex:1;height:4px;border-radius:2px;background:#F1F5F9;
  overflow:hidden;display:block}
.pw-track i{display:block;height:100%;border-radius:2px;width:0;
  transition:width .3s ease,background-color .3s ease}
.pw-label{font-size:11px;font-weight:700;min-width:40px;text-align:right}

/* ---------- Attached files ----------
   One grid for all four listing types — an offer, an inquiry, a vacancy
   and a candidate — because a spec sheet looks the same whatever it is
   hanging off, and three near-copies of this would drift apart.

   A photo is drawn as itself; everything else gets a paper glyph with
   its extension on it, cut from one element so there is no icon font and
   no SVG to keep in step between the PHP and the Alpine pages. Whether a
   tile is a link at all is Core\FileKind's decision, not the palette's. */
.doc-grid{display:grid;gap:12px;
  grid-template-columns:repeat(auto-fill,minmax(148px,1fr))}
.ch + .doc-grid,.hint + .doc-grid{margin-top:12px}
.doc-tile{position:relative;padding:8px;background:var(--bg);border-radius:14px;
  transition:background-color .15s ease,var(--lift-ease)}
/* THE TINT IS THIS COMPONENT'S; the movement and the shadow are the
   shared lift's. A `.lift` class is not used here because the tile
   already has a modifier saying whether it opens at all — a locked
   one must not rise — and two classes meaning the same thing is how
   they come to disagree. */
.doc-tile.can-open:hover,.doc-tile.can-open:focus-within{
  background:var(--blue-10);transform:var(--lift-move);
  box-shadow:var(--lift-shadow)}
/* The whole tile is the hit target, not just the filename. */
.doc-open{display:flex;flex-direction:column;gap:8px;color:inherit}
a.doc-open:hover{color:inherit}
a.doc-open:focus-visible{outline:2px solid var(--blue);outline-offset:3px;
  border-radius:10px}
/* Over the thumbnail, so removing a file never costs a grid column. */
.doc-del{position:absolute;top:12px;right:12px;border:0;cursor:pointer;
  font:800 10.5px/1 Manrope,system-ui,sans-serif;color:#fff;letter-spacing:.04em;
  background:rgba(15,31,74,.72);border-radius:999px;padding:5px 9px}
.doc-del:hover{background:var(--red)}

.doc-thumb{aspect-ratio:4/3;border-radius:10px;background:#fff;
  display:grid;place-items:center;overflow:hidden}
.doc-thumb img{width:100%;height:100%;object-fit:cover;display:block}

.doc-meta{display:flex;flex-direction:column;gap:2px;min-width:0}
.doc-meta b{font-size:12.5px;font-weight:700;overflow:hidden;
  text-overflow:ellipsis;white-space:nowrap}
.doc-meta span{font-size:11px;font-weight:600;color:var(--faint)}

/* The paper glyph. The notch in the clip-path is the folded corner, and
   ::after is a plain rectangle that the same clip cuts in half — so the
   fold is one shape, not two elements that have to line up. */
.ft-icon{position:relative;width:46px;height:58px;background:var(--fk,#475569);
  clip-path:polygon(0 0,68% 0,100% 22%,100% 100%,0 100%);
  display:flex;align-items:flex-end;justify-content:center;padding-bottom:8px}
.ft-icon:after{content:'';position:absolute;top:0;right:0;
  width:32%;height:22%;background:rgba(255,255,255,.42)}
.ft-icon b{font-size:10px;font-weight:800;color:#fff;letter-spacing:.05em}

/* Conventional file colours, each dark enough for white 10px bold on it.
   .ft-host is for anywhere else that hosts a glyph without being a full
   tile — the dashboard gallery drops one into its own thumb. */
.doc-tile[data-kind="pdf"],  .ft-host[data-kind="pdf"]  {--fk:#B91C1C}
.doc-tile[data-kind="sheet"],.ft-host[data-kind="sheet"]{--fk:#047857}
.doc-tile[data-kind="doc"],  .ft-host[data-kind="doc"]  {--fk:#1F3F8F}
.doc-tile[data-kind="image"],.ft-host[data-kind="image"]{--fk:#6D28D9}
/* Fills the thumb it sits in, so the whole square is the hit target. */
.ft-host{display:grid;place-items:center;width:100%;height:100%}
.ft-host:hover{background:var(--blue-10)}

/* Not a member: the list is public, the bytes are not, so the tile says
   so instead of offering a link that would only 403. */
.doc-tile.locked{cursor:default}
.doc-tile.locked .doc-thumb{background:repeating-linear-gradient(
  45deg,#fff,#fff 6px,#F8FAFC 6px,#F8FAFC 12px)}
.doc-lock{font-size:11px;font-weight:800;color:var(--mute)}
.doc-note{margin:10px 0 0;font-size:12.5px;color:var(--mute);font-weight:600}
.doc-note a{font-weight:700}

/* ---------- A file on its way up ----------
   The same tile as an attached file, with the transfer drawn over its
   thumbnail. A queued image shows itself (from a blob URL, so nothing
   has to reach the server first); everything else shows its glyph.

   The ring is one element: a conic-gradient sweeps to --p percent and an
   inset pseudo-element punches the middle out. No second arc to keep in
   register, and no SVG. */
.doc-tile.queued .doc-thumb{position:relative}
.doc-prog{position:absolute;inset:0;display:grid;place-items:center;
  background:rgba(15,31,74,.55);border-radius:10px}
.doc-prog .ring{position:relative;width:46px;height:46px;border-radius:50%;
  display:grid;place-items:center;
  background:conic-gradient(#fff calc(var(--p,0) * 1%),rgba(255,255,255,.28) 0)}
.doc-prog .ring:before{content:'';position:absolute;inset:4px;
  border-radius:50%;background:var(--navy)}
.doc-prog b{position:relative;font-size:10.5px;font-weight:800;color:#fff;
  font-variant-numeric:tabular-nums}

/* No total to divide by — show motion rather than a number that would
   be invented. */
.doc-prog.busy .ring{background:conic-gradient(#fff 0 25%,rgba(255,255,255,.28) 0);
  animation:doc-spin .9s linear infinite}
@keyframes doc-spin{to{transform:rotate(360deg)}}
@media (prefers-reduced-motion:reduce){
  .doc-prog.busy .ring{animation-duration:2.4s}
}

/* Done, and refused. Both stay on screen: a file that vanished on
   success leaves no trace it went, and one that vanished on failure
   takes its reason with it. */
.doc-tile.done .doc-thumb:after{content:'✓';position:absolute;right:6px;bottom:6px;
  width:22px;height:22px;border-radius:50%;background:var(--green);color:#fff;
  font-size:12px;font-weight:800;display:grid;place-items:center}
.doc-tile.bad{background:var(--error-10)}
.doc-tile.bad .doc-thumb{outline:2px solid var(--red);outline-offset:-2px}
.doc-tile.bad .doc-meta span{color:var(--red)}

/* ── Banners ──────────────────────────────────────────────────────────
   Moved here from register.css, which is the only page that loaded it —
   so the same markup was styled on /register and bare everywhere else.
   Eight views use .banner, including the dashboard's error line and the
   back office; on five of them it had no styles at all, which meant an
   error message that did not look like one.

   login.css keeps nothing of its own now: its copy was identical but
   scoped to .auth, and two copies of a rule is how they come to differ. */
/* ---------- locked-note ----------
   "Why can I not see this": the company profile's blurred name, and the
   subscriber prices. Here and not in profile.css, which is where it started
   and where it stayed when the partial was reused — so the price pages, which
   do not load that stylesheet, rendered the lock icon as an unsized black
   block the width of the column. A shared partial's styles belong with the
   partial.

   Not an error colour: nothing has gone wrong, somebody simply cannot see
   this yet. */
.locked-note{display:flex;align-items:center;gap:8px;margin:10px 0 0;
  padding:7px 12px;border-radius:999px;background:rgba(255,255,255,.82);
  width:fit-content;max-width:100%;font-size:13px;font-weight:600;color:#475569}
/* `:not(.btn)` is load-bearing. This rule is (0,1,1) and `.btn-blue` is
   (0,1,0), so without it the link colour wins on a button inside the
   note — and "See plans" rendered as blue text on a blue fill: a solid
   pill with nothing written on it. The inline links in the pill form
   still want it. */
.locked-note a:not(.btn){color:var(--blue);font-weight:700}
.locked-note small{font-size:12.5px;font-weight:600;color:var(--mute);margin-left:4px}
.locked-note small a:not(.btn){font-weight:700}
/* The icon MUST carry its own size and fill: an inline SVG with neither is a
   filled shape scaled to its container. */
.locked-note .ln-ic{width:15px;height:15px;flex:none;fill:none;stroke:var(--mute);
  stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round}

/* The wider form: a sentence and two buttons, for a page with room for it.
   Stacks under 480px, where two buttons and a sentence on one line is three
   lines of wrapping.

   ON THE THEME'S NAVY. It was a white panel with a grey border on a white
   page, which is the one place a paywall must not be invisible: the reader
   has just been stopped mid-article or handed a table of dashes and has to
   find the sentence explaining it. `--navy` is the token the primary button
   already uses, so the box and that button are the same colour by
   definition rather than by two hexes that agree today.

   White on it measures 15.97:1 and the gold icon 11.39:1, both far past AA;
   the gradient only deepens the surface, so the end of it is better still
   (17.67 and 12.61). Nothing here is left to look right. */
.locked-note.wide{display:block;width:auto;border-radius:14px;padding:24px 28px;
  background:linear-gradient(135deg,var(--navy) 0%,#0A1736 100%);
  border:0;box-shadow:0 10px 30px rgba(15,31,74,.28);
  color:#fff;font-size:15.5px;font-weight:500;line-height:1.55}
.locked-note.wide .ln-row{display:flex;align-items:flex-start;gap:11px;margin:0}

/* Gold, which is a token this site already has and a tier this sentence
   already names. Bigger than the pill's 15px because the box is bigger and
   it is the only thing on it that is not a word. `flex:none` and its own
   size, as the base rule's comment says: an inline SVG with neither is a
   filled shape scaled to its container. */
.locked-note.wide .ln-ic{width:21px;height:21px;flex:none;margin-top:1px;
  stroke:var(--gold);stroke-width:1.8}

.locked-note.wide .ln-acts{display:flex;gap:10px;flex-wrap:wrap;margin-top:18px}

/* The buttons are INVERTED here, and they have to be: `.btn-blue` is a navy
   fill, which on a navy box is a button-shaped hole. Overridden by
   descendant selector rather than by changing the classes in the partial —
   (0,4,0) against `.btn-blue`'s (0,1,0), so no `!important` and the pill
   form two rules up is untouched. */
.locked-note.wide .ln-acts .btn-blue{background:#fff;color:var(--navy);
  font-weight:800;box-shadow:none}
.locked-note.wide .ln-acts .btn-blue:hover{background:var(--blue-10);
  color:var(--navy);box-shadow:none}

/* Sign in is the quieter of the two: it is only ever shown to somebody who
   is not signed in, and for most of them the answer is still See plans. */
.locked-note.wide .ln-acts .btn-line{background:transparent;color:#fff;
  border:1px solid rgba(255,255,255,.6)}
.locked-note.wide .ln-acts .btn-line:hover{background:rgba(255,255,255,.1);
  color:#fff;border-color:rgba(255,255,255,.8)}

/* A white ring, because the site's default focus ring is the brand blue and
   that is this box's own background — a focus ring you cannot see is the
   same as none. `outline` rather than the `box-shadow` pair the other
   buttons use: there is no light surface here to draw the inner gap on. */
.locked-note.wide .ln-acts .btn:focus-visible{outline:2px solid #fff;
  outline-offset:2px;box-shadow:none}

@media (max-width:480px){
  .locked-note.wide{padding:20px}
  .locked-note.wide .ln-acts .btn{width:100%;text-align:center}
}
.banner{display:flex;gap:10px;align-items:flex-start;border-radius:12px;
  padding:12px 14px;font-size:13.5px;font-weight:600;line-height:1.45}
.banner i{flex:none;width:18px;height:18px;border-radius:50%;color:#fff;
  font-size:11px;font-weight:800;display:grid;place-items:center;font-style:normal}
.banner.err{background:#FEF2F2;color:#991B1B}
.banner.err i{background:#DC2626}
.banner.info{background:var(--blue-10);color:var(--navy);align-items:center;flex-wrap:wrap}
.banner.info i{background:var(--blue)}
.banner.ok{background:var(--green-10);color:#065F46}
.banner.ok i{background:var(--green)}
/* An action beside the message — the status change's Undo. `auto` on the
   left margin pushes it to the far end, and the banner is already a flex
   row, so nothing else moves. align-items on .banner is flex-start, so
   the button needs its own centring against the text's first line. */
.banner .banner-act{margin-left:auto;flex:none;align-self:center}

/* Money owed. Amber rather than red: an unpaid invoice is a thing to do,
   not a thing that has gone wrong, and the member is not in trouble. */
.banner.pay{background:#FFFBEB;color:#92400E;align-items:center;flex-wrap:wrap}
.banner.pay i{background:#D97706}
/* A two-line banner: a heading and the detail under it. Not scoped to
   .pay — the review notice uses the same shape. */
.banner .pb-txt{flex:1 1 260px;min-width:0;display:flex;flex-direction:column;gap:2px}
.banner .pb-txt b{font-size:14px}
.banner .pb-txt small{font-size:12px;font-weight:600;line-height:1.5;opacity:.9}
.banner.pay .btn{flex:none;min-height:40px}

/* ---------- The combo's multi-select chips ----------
   Under the field, not inside it: the input is the search box, and chips
   growing inside push the caret around and make a four-item selection two
   lines tall before anything is typed. Styles live here with the component,
   so any page that turns `multi` on gets them. */
.combo-chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
.combo-chip{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--line);
  background:#fff;border-radius:999px;padding:4px 8px 4px 9px;font:inherit;
  font-size:12.5px;font-weight:600;color:var(--ink);cursor:pointer;max-width:100%}
.combo-chip:hover{border-color:#FCA5A5;background:#FEF2F2}
.combo-chip:focus-visible{outline:2px solid var(--blue);outline-offset:2px}
.combo-chip > span:not(.flag){overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.combo-chip i{font-style:normal;font-weight:800;color:var(--mute);line-height:1}
.combo-chip:hover i{color:var(--red)}
.combo-chip .flag{width:16px;height:12px;flex:none}

/* ---------- The analytics cookie banner ----------
   A bar at the foot of every public page, until the visitor answers it. The
   styles are HERE and not in a per-page sheet for the reason this project
   has now recorded six times: a component's styles travel with the
   component, and this one is on /news, /prices, a profile, the dashboard
   and a 404 — five stylesheets that have nothing else in common.

   `--z-toast` and not `--z-sticky`: it has to clear the site header, a
   modal's backdrop and the combo panels, because it is the one thing on the
   page that must be answerable wherever the reader happens to be. That is
   the same argument the scale's own comment makes for a toast.

   `position:fixed` with `inset:auto 0 0`, so it is out of the page's flow
   entirely: a bar that took part in the layout would move the footer of
   every page by its own height, and then move it back the moment somebody
   pressed Accept. */
.ck-bar{position:fixed;inset:auto 0 0;z-index:var(--z-toast);
  display:flex;align-items:center;gap:18px;flex-wrap:wrap;
  padding:14px clamp(16px,4vw,48px);
  background:#0F1F4A;color:#E4EAFA;
  box-shadow:0 -8px 24px rgba(15,31,74,.22)}
.ck-bar[hidden]{display:none}
.ck-txt{flex:1;min-width:240px;margin:0;font-size:13.5px;line-height:1.5}
/* The link has to restate its colour: `a{color:var(--blue)}` is navy on a
   navy bar, which is the (0,1,1)-beats-(0,1,0) trap this project has met
   five times — on the ticker's own hover most recently. `.ck-bar a` is
   (0,1,1) and sits later in this sheet, so it wins on both counts. */
.ck-bar a{color:#fff;text-decoration:underline;white-space:nowrap}
.ck-bar a:hover{color:#fff}
.ck-btns{display:flex;gap:10px;flex:none}
/* The two buttons are the site's own, sized down. Decline is an outline on
   navy, so it needs its own border and text colour — `.btn-line` is drawn
   for a white ground. */
.ck-bar .btn{min-height:40px;padding:9px 18px;font-size:13.5px}
.ck-bar .btn-line{background:transparent;border:1px solid rgba(255,255,255,.4);color:#fff}
.ck-bar .btn-line:hover{background:rgba(255,255,255,.12);border-color:#fff;color:#fff}
.ck-bar .btn-blue{background:#fff;color:var(--navy);box-shadow:none}
.ck-bar .btn-blue:hover{background:#E4EAFA;color:var(--navy)}
.ck-bar .btn:focus-visible{outline:2px solid #fff;outline-offset:2px}

@media (max-width:639px){
  /* Stacked: at 360 the sentence, Decline and Accept on one row leave each
     button about 70px. The buttons go full width and in reading order. */
  .ck-bar{flex-direction:column;align-items:stretch;gap:12px;padding:14px 16px}
  .ck-btns{flex-direction:row}
  .ck-btns .btn{flex:1}
}

/* ══ The modal shell ══════════════════════════════════════════════════
   MOVED HERE FROM admin.css, because it stopped being admin's.

   Four things wear it now: the back office's dozen dialogues, the
   contributor submission flow, the campaign preview, and the Contact /
   Suggestion modal — which is in the PUBLIC layout and therefore on
   every page of the site, including the ones that load neither
   admin.css nor dashboard.css.

   That is the fifth time this project has moved a component's styles
   to where the component is, after `stack-sm`, `locked-note`, the
   `.switch` and the cookie banner. The rule it keeps re-learning: a
   shared partial's styles travel with the partial, and a page that
   does not load them renders it as unstyled markup.

   `position:fixed` with the panel as a flex column and only the MIDDLE
   scrolling, so the title bar and the buttons stay reachable on a long
   form — a modal whose footer is below the fold is one you cannot
   submit without scrolling through somebody else's content. */
.modal-back{position:fixed;inset:0;z-index:var(--z-modal);background:rgba(15,31,74,.52);
  display:flex;align-items:flex-start;justify-content:center;
  padding:24px 16px;overflow-y:auto}

.modal-pan{background:#fff;border-radius:18px;width:100%;max-width:860px;
  max-height:calc(100vh - 48px);display:flex;flex-direction:column;
  box-shadow:0 24px 70px rgba(15,31,74,.34);overflow:hidden}

.modal-h{display:flex;align-items:center;gap:12px;padding:18px 22px;
  border-bottom:1px solid var(--line);flex:none}
.modal-h h2{margin:0;font-size:19px;font-weight:800;flex:1}

.modal-x{appearance:none;border:0;background:none;cursor:pointer;
  width:34px;height:34px;border-radius:10px;font-size:26px;line-height:1;
  color:var(--mute)}
.modal-x:hover{background:var(--bg);color:var(--ink)}
.modal-x:focus-visible{outline:2px solid var(--blue);outline-offset:2px}

/* The only scrolling part. `min-height:0` because a flex item's default
   min-height is auto, which would let the content push the panel past
   `max-height` instead of scrolling inside it. */
.modal-body{padding:20px 22px;overflow-y:auto;min-height:0;flex:1}

.modal-f{display:flex;align-items:center;gap:10px;padding:14px 22px;
  border-top:1px solid var(--line);background:var(--bg);flex:none}
.flex-sp{flex:1}

@media (max-width:640px){
  .modal-back{padding:0}
  .modal-pan{max-width:none;border-radius:0;max-height:100vh;min-height:100vh}
  .modal-f .btn{flex:1;justify-content:center}
}

/* ══ The field shell ══════════════════════════════════════════════════
   MOVED HERE FROM dashboard.css, for the reason above it.

   `.fld` is worn by the dashboard, /account, the contributor submission
   flow, the back office and now the site-wide Contact modal. It was in
   ONE page's stylesheet, which is why /contributor/register once
   rendered `label.fld` at 1215px and scrolled sideways by 889px at 360,
   and why /news-prices shipped without it.

   `.fld small.err` and `.fld.is-bad` stay in admin.css: those are the
   red state, which only the forms that validate inline use, and they
   are additive. */
/* Same field shell as the register wizard, on the softer dashboard ground. */
.fld{--fld-bg:var(--bg);display:block;background:var(--fld-bg);border-radius:14px;padding:12px 16px}
.fld:focus-within{box-shadow:0 0 0 2px var(--blue)}
.fld > span{display:block;font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--mute)}
/* A hint under the input is ALSO a direct child span, so it was picking up
   the label style above and rendering the sentence in uppercase letter-
   spaced grey. The label is the first span; a `.fine` one is prose. */
.fld > span.fine{text-transform:none;letter-spacing:normal;font-weight:500;
  font-size:12px;line-height:1.5;margin-top:5px}
.fld input,.fld textarea{width:100%;border:0;background:none;font:inherit;font-size:16px;font-weight:700;margin-top:3px;color:var(--ink);outline:0;padding:0}
.fld textarea{font-weight:600;font-size:15px;resize:vertical;min-height:84px;line-height:1.5}
.fld input[readonly]{color:var(--mute);cursor:not-allowed}

/* ── A FIELD CHROME HAS FILLED IN ─────────────────────────────────────
   Chrome paints its own pale blue over an autofilled field and draws a
   box round it, and NO `background` can refuse either: the autofill
   colour is painted in the UA's own layer, above anything the page
   declares. That is the "extra border and blue background" reported on
   the contributor form — one cause, two symptoms, and neither of them a
   rule in this stylesheet.

   Three declarations, and each covers something the others cannot:

     the inset shadow   fills the box with the shell's own ground
     text-fill-color    the only property that reaches the text Chrome
                        has recoloured, which `color` does not
     the transition     Chrome ANIMATES the blue in, and on the frames
                        before the shadow composites it is visible. A
                        delay longer than any session means that
                        animation never arrives.

   THE COLOUR IS `--fld-bg` AND NOT A LITERAL, because the shell is grey
   on a page and white inside a `.row-card`: one hard-coded value makes
   an autofilled field the one box on the card that is the wrong shade. */
.fld input:-webkit-autofill,
.fld input:-webkit-autofill:hover,
.fld input:-webkit-autofill:focus,
.fld input:-webkit-autofill:active,
.fld textarea:-webkit-autofill{
  -webkit-box-shadow:0 0 0 100px var(--fld-bg) inset;
  box-shadow:0 0 0 100px var(--fld-bg) inset;
  -webkit-text-fill-color:var(--ink);
  caret-color:var(--ink);
  border:0;
  transition:background-color 9999s ease-in-out 0s}

/* ── AND THE SAME, SCOPED TO A MODAL ──
   (0,2,1) against (0,1,1), so it wins wherever the sheet loads. The
   contact and contributor modals are on EVERY public page, including the
   ones that ship a stylesheet of their own after this one — and the trap
   this project records five times is that at equal specificity the later
   sheet decides. The field's own look is restated for the same reason:
   no inner border, no ground of its own, the focus ring on the CARD. */
.modal-back .fld input,
.modal-back .fld textarea{border:0;background:none;outline:0}
.modal-back .fld input:-webkit-autofill,
.modal-back .fld input:-webkit-autofill:hover,
.modal-back .fld input:-webkit-autofill:focus,
.modal-back .fld input:-webkit-autofill:active,
.modal-back .fld textarea:-webkit-autofill{
  -webkit-box-shadow:0 0 0 100px var(--fld-bg) inset;
  box-shadow:0 0 0 100px var(--fld-bg) inset;
  -webkit-text-fill-color:var(--ink);
  caret-color:var(--ink);
  border:0;
  transition:background-color 9999s ease-in-out 0s}

/* ══ Contact us / Send a suggestion ═══════════════════════════════════
   The modal is in the PUBLIC layout, so it is on every page of the site
   — including the ones that load neither admin.css nor dashboard.css nor
   submit.css. Everything it needs is therefore here, beside the modal
   shell and the field shell it shares with the back office.

   What it does NOT redefine: `.modal-*` and `.fld` (above), `.seg2` (the
   one-of-N toggle), `.btn` and the flag. Scoped under `.ct-modal` where a
   name is generic, so nothing here can reach a page that has its own. */

/* The two-option choice, at the top of the form. */
.ct-modal .ct-kind{width:100%;margin:0 0 14px}
.ct-modal .ct-kind button{flex:1}

/* The lede under it, and the one under the tick on the thank-you step. */
.ct-modal .lede{margin:0 0 16px;font-size:14px;line-height:1.6;color:var(--mute)}

/* Two fields to a row above 560, one below — the same shape the
   contributor form uses, with its own name so neither can move the
   other. */
.ct-modal .sub-row{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));
  gap:10px;margin-bottom:10px}
.ct-modal .fld.full,
.ct-modal .combo{margin-bottom:10px}
.ct-modal .fld.full{grid-column:1/-1}

/* A file input is the one control here that cannot wear `.fld`'s own
   typography: 16px bold on a native file picker is a button with a
   serif-sized label beside it. */
.ct-modal input[type=file]{font-size:13px;font-weight:600;margin-top:4px}

/* The red state. `.fld small.err` and `.fld.is-bad` are admin.css's, and
   admin.css is not on a public page — so the pair is restated under
   `.ct-modal`, which is (0,2,1) and (0,2,0) and therefore wins wherever
   both sheets load rather than depending on their order. */
.ct-modal .err{display:block;margin-top:4px;font-size:12px;color:var(--red);
  font-weight:600}
.ct-modal .fld.is-bad,
.ct-modal .combo.is-bad{box-shadow:0 0 0 2px var(--red)}

/* ── The error summary, beside the button ──
   Inside the footer, above the buttons, where the press happened. A
   message under a field eleven rows above the fold of a scrolling modal
   leaves the button reading as broken — the fault the contributor forms
   record in full. */
.ct-modal .modal-f{flex-wrap:wrap}
.ct-modal .err-sum{flex:1 0 100%;background:#FEF2F2;border:1px solid #FECACA;
  border-radius:12px;padding:10px 14px;margin:0 0 10px;
  font-size:13px;line-height:1.5}
.ct-modal .err-sum p{margin:0 0 4px;color:#991B1B}
.ct-modal .err-sum ul{margin:0;padding-left:18px}
.ct-modal .err-sum a{color:#991B1B;text-decoration:underline}
.ct-modal .err-sum a:hover{color:var(--red)}

/* ── The thank-you step ── */
.ct-modal .ct-done{text-align:center;padding:14px 0 6px}
.ct-modal .ct-tick{display:grid;place-items:center;width:52px;height:52px;
  margin:0 auto 14px;border-radius:50%;background:var(--sell-10);color:var(--sell-ink)}
.ct-modal .ct-tick svg{width:26px;height:26px}
.ct-modal .ct-done .lede{margin-bottom:8px;color:var(--ink);font-weight:600}

/* ── The honeypot ──
   OFF-SCREEN, never `display:none` and never `visibility:hidden`: both
   are the first thing a form-filler checks for. The same shape the
   contributor registration's own honeypot uses. */
.hp-field{position:absolute;left:-9999px;top:auto;width:1px;height:1px;
  overflow:hidden}

@media (max-width:560px){
  .ct-modal .sub-row{grid-template-columns:minmax(0,1fr)}
}
