
/* ---------- fonts ---------------------------------------------------------
   Self-hosted so no visitor data reaches a third-party font CDN. Files are
   fetched into public/fonts by scripts/fetch-assets.mjs; if they are absent the
   stack falls back to system faces and the layout still holds. */
@font-face {
  font-family: 'Plus Jakarta Sans';
  src: url('/fonts/plus-jakarta-sans-latin.woff2') format('woff2');
  font-weight: 400 800;
  font-display: swap;
  font-style: normal;
}
@font-face {
  font-family: 'DM Sans';
  src: url('/fonts/dm-sans-latin.woff2') format('woff2');
  font-weight: 400 700;
  font-display: swap;
  font-style: normal;
}

:root {
  --bg: #F0F4F9;
  --text: #1C2C45;
  --text-strong: #1C2D45;
  --muted: #5A7396;
  --nav: #2E4D7A;
  --primary: #4E6FA3;
  --primary-hover: #3E5C89;
  --pill: #7B9EC7;
  --border: #C5D5E8;
  --card: #FFFFFF;
  --footer-text: #B8CEE2;
  /* The one warm colour in the palette. Featured is the only state that earns
     it — it is the paid promotion, and it has to win against blue and green.
     Used for the star glyph, the featured card's border, and nothing else.
     Saturated, so it reads as an accent against white rather than a surface.
     Held at the same hue as the band it borders — a warmer, oranger gold beside
     a pure-yellow band reads as two colours rather than one. */
  --featured: #A88600;
  /* The featured card's title band. A tint of the accent rather than the accent
     itself: this site is white cards on pale blue, and a saturated band was
     heavy against that whatever its hue. Being pale also settles the contrast —
     the navy title reads at 13:1 and the blue star at 4.9:1, where on the old
     mid-tone amber neither dark nor white text could pass at all.

     This value cannot double as the accent: at 1.05:1 against a white card a
     border in it would be invisible, which is why the two are separate. */
  --featured-band: #FFFF99;
  /* The two review states that have a glyph. Purple is the only hue in the
     admin not already spoken for — blue is Published, green is Listed, amber is
     Featured — so Validated cannot be mistaken for a paid or visibility state.
     Verified reuses the green already carrying it on the Verified chip. */
  --validated: #6D4AA8;
  --verified: #1F5A3A;
  /* Archived. Brown rather than grey: archiving is a decision someone made,
     not an absence of one, so it earns a colour. Kept clear of the --featured
     amber so a filled star and an archive box never read as the same state. */
  --archived: #8A5A2B;

  --radius: 10px;
  --radius-card: 12px;
  --radius-pill: 9999px;

  --shadow-sm: 0 1px 2px 0 rgba(0,0,0,.03), 0 1px 3px 0 rgba(0,0,0,.06);
  --shadow-md: 0 4px 6px -1px rgba(0,0,0,.05), 0 2px 4px -2px rgba(0,0,0,.05);
  --shadow-lg: 0 10px 15px -3px rgba(0,0,0,.1), 0 4px 6px -4px rgba(0,0,0,.1);

  --s-xs: 4px; --s-sm: 8px; --s-md: 16px; --s-lg: 24px;
  --s-xl: 32px; --s-2xl: 48px; --s-3xl: 64px;

  --container: 1200px;

  --font-display: 'Plus Jakarta Sans', ui-sans-serif, system-ui, sans-serif;
  --font-body: 'DM Sans', ui-sans-serif, system-ui, sans-serif;
}

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 24px;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4 { font-family: var(--font-display); margin: 0; }

h1 {
  font-size: 36px; line-height: 1.1; font-weight: 700;
  letter-spacing: -1px; color: #fff; margin-bottom: var(--s-md);
}
h2 {
  font-size: 28px; line-height: 1.15; font-weight: 700;
  letter-spacing: -0.7px; color: var(--nav); margin-bottom: var(--s-sm);
}
h3 {
  font-size: 18px; line-height: 28px; font-weight: 600;
  letter-spacing: -0.45px; color: var(--text-strong); margin-bottom: var(--s-sm);
}
h4 { font-size: 16px; font-weight: 600; color: #fff; margin-bottom: var(--s-sm); }

p { margin: 0 0 var(--s-md); }
a { color: var(--primary); }

:focus-visible {
  outline: 3px solid var(--primary);
  outline-offset: 2px;
  border-radius: 4px;
}

.container {
  margin-inline: auto;
  max-width: var(--container);
  padding-inline: var(--s-lg);
}

.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  background: #fff; color: var(--nav); padding: var(--s-sm) var(--s-md);
}
.skip-link:focus { left: var(--s-md); top: var(--s-sm); }

/* ---------- nav ---------------------------------------------------------- */
.nav {
  position: sticky; top: 0; z-index: 50; width: 100%;
  background: var(--nav);
}
.nav__inner {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s-md);
  margin-inline: auto; max-width: var(--container);
  padding: 12px var(--s-lg);
}
.nav__brand {
  display: inline-flex; align-items: center; gap: var(--s-sm);
  color: #fff; font-family: var(--font-display); font-size: 20px;
  letter-spacing: -0.5px; font-weight: 600; text-decoration: none;
}
.nav__links { display: flex; align-items: center; gap: var(--s-lg); }
.nav__link {
  color: rgba(255,255,255,.9); text-decoration: none;
  font-size: 14px; font-weight: 500;
}
.nav__link:hover { color: #fff; text-decoration: underline; }

.btn {
  display: inline-flex; align-items: center; gap: var(--s-sm);
  background: var(--primary); color: #fff; border: 0;
  border-radius: var(--radius); text-decoration: none; cursor: pointer;
  font-family: var(--font-body); font-size: 14px; font-weight: 500;
  padding: var(--s-sm) 20px;
  transition: background-color .15s ease;
}
.btn:hover { background: var(--primary-hover); }
.btn--lg {
  font-size: 16px; font-weight: 600; padding: 12px var(--s-xl);
  box-shadow: var(--shadow-lg);
}
.btn--ghost {
  background: transparent; color: var(--primary);
  border: 1px solid var(--border);
}
.btn--ghost:hover { background: #fff; }
.btn--danger { background: #B3261E; }
.btn--danger:hover { background: #8C1D18; }

/* ---------- hero --------------------------------------------------------- */
.hero {
  position: relative; display: flex; align-items: center; justify-content: center;
  overflow: hidden; min-height: 380px; padding-block: var(--s-3xl);
}
.hero__image {
  position: absolute; inset: 0;
  background-size: cover; background-position: center;
  /* WebP is ~25% smaller than the JPEG. Browsers that do not understand
     image-set() ignore the whole declaration, so the plain url() below stays
     as the fallback and must come first. */
  background-image: url('/img/hero.jpg');
  background-image: image-set(url('/img/hero.webp') type('image/webp'),
                              url('/img/hero.jpg') type('image/jpeg'));
}
.hero__scrim {
  position: absolute; inset: 0;
  background: linear-gradient(to right,
    rgba(28,45,69,.85) 0%, rgba(46,77,122,.7) 50%, rgba(78,111,163,.5) 100%);
}
.hero__content {
  position: relative; z-index: 10; text-align: center;
  margin-inline: auto; max-width: var(--container); padding-inline: var(--s-lg);
}
.hero__sub {
  color: rgba(255,255,255,.92); font-size: 18px; line-height: 28px;
  max-width: 620px; margin: 0 auto var(--s-lg);
}

/* ---------- sections ----------------------------------------------------- */
.section { padding-block: var(--s-3xl); }
.section--tight { padding-block: var(--s-2xl); }
.section__lede { color: var(--muted); max-width: 680px; }

.pills { display: flex; flex-wrap: wrap; gap: var(--s-sm); margin: var(--s-lg) 0; }
.pill {
  display: inline-block; border-radius: var(--radius-pill);
  padding: 6px var(--s-md); font-size: 14px; font-weight: 500;
  text-decoration: none; background: var(--pill); color: var(--text-strong);
  transition: background-color .15s ease, color .15s ease;
}
.pill:hover { background: var(--primary); color: #fff; }
.pill[aria-current="page"] { background: var(--primary); color: #fff; }

.count { color: var(--muted); font-size: 14px; margin-bottom: var(--s-md); }

.grid {
  display: grid; gap: var(--s-lg);
  grid-template-columns: 1fr;
}
@media (min-width: 640px) { .grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1024px) { .grid { grid-template-columns: repeat(3, 1fr); } }

.card {
  position: relative; display: block; background: var(--card);
  border: 1px solid var(--border); border-radius: var(--radius-card);
  padding: var(--s-lg); box-shadow: var(--shadow-sm);
  text-decoration: none; color: inherit;
  transition: box-shadow .2s ease, transform .2s ease;
}
.card:hover { box-shadow: var(--shadow-md); }
.card:hover h3 { color: var(--primary); }
/* Featured cards: butter band, butter edge, no badge. The styling carries the
   meaning, so the word "Featured" came off the card.

   The band is pulled to the card's edges with negative margins rather than the
   card losing its padding, which would have meant restyling every card to
   change one of them. Its height comes from the title it holds — around a
   quarter of a typical card, and it stays proportionate when a name wraps to
   two lines instead of being pinned to a percentage that would clip.

   The border is the band colour, on request, to see it in place. Be aware it is
   1.05:1 against the white card, so the edge is effectively not drawn. The
   hairline under the band is the same colour on request, which means it too is
   invisible — against the band it is the same value exactly. So a featured card
   is now distinguished by the band alone: no frame, no divider. */
.card--featured { border-color: var(--featured-band); box-shadow: var(--shadow-md); }
.card--featured .card__head {
  margin: calc(var(--s-lg) * -1) calc(var(--s-lg) * -1) var(--s-md);
  padding: var(--s-md) var(--s-lg);
  background: var(--featured-band);
  border-bottom: 1px solid var(--featured-band);
  border-radius: calc(var(--radius-card) - 1px) calc(var(--radius-card) - 1px) 0 0;
}
/* Size and weight stay ordinary — the pale band gives the title 13:1, so none
   of the enlarging the old saturated band forced is needed.

   The colours are inverted against a standard card: blue at rest, navy on
   hover, which is the reverse of .card:hover h3 below. Standard cards keep
   navy-then-blue, so the two behave oppositely on purpose. */
.card--featured .card__head h3 {
  display: flex; align-items: center; gap: var(--s-sm); margin: 0;
  color: var(--primary);
}
.card--featured:hover .card__head h3 { color: var(--text-strong); }
.card__star { display: inline-flex; flex: 0 0 auto; color: var(--primary); }
.card__star svg { display: block; }

.card__meta {
  display: flex; align-items: center; gap: 6px;
  color: var(--muted); font-size: 14px; margin-bottom: var(--s-xs);
}
/* Phone and site on one line under the location. Muted and unlinked: they are
   there to qualify the card, not to be actioned from it. */
.card__contact {
  display: flex; flex-wrap: wrap; gap: 4px var(--s-md);
  color: var(--muted); font-size: 14px; margin-top: var(--s-xs);
}
.card__site { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
.card__desc { color: var(--muted); font-size: 14px; line-height: 22px; margin: var(--s-sm) 0 0; }
.card__tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: var(--s-sm); }
.tag {
  display: inline-block; border-radius: var(--radius-pill);
  padding: 2px 10px; font-size: 12px; font-weight: 500;
  background: var(--pill); color: var(--text-strong); text-decoration: none;
}

.badge {
  display: inline-block; border-radius: var(--radius-pill);
  padding: 2px 10px; font-size: 12px; font-weight: 600;
  background: var(--primary); color: #fff; margin-bottom: var(--s-sm);
}

/* ---------- detail page -------------------------------------------------- */
.crumbs { font-size: 14px; color: var(--muted); margin-bottom: var(--s-md); }
.crumbs a { color: var(--muted); }
.crumbs span { margin-inline: 6px; }

.panel {
  background: var(--card); border: 1px solid var(--border);
  border-radius: var(--radius-card); padding: var(--s-xl);
  box-shadow: var(--shadow-sm);
}
/* The admin filter bar, in the theme's navigation blue — the same --nav the
   admin bar at the top of the page uses, so the two pieces of chrome bracket
   the content between them.

   An earlier pass used --bg here. That is the theme's page blue, but at 94%
   lightness it reads as white on a white page and separated nothing. This is a
   real fill, so everything sitting on it has to be re-lit: labels and the reset
   button go white, and the focus ring switches off --primary, which is itself a
   blue and would sink into this one. The inputs stay #fff and now read as
   raised out of the bar rather than dissolved into it.

   A modifier, not a change to .panel: that class carries every card on the
   public site too. */
.panel--filters { background: var(--nav); border-color: var(--nav); }
.panel--filters .field__label { color: #fff; }
.panel--filters .btn--ghost { color: #fff; border-color: rgba(255,255,255,.45); }
.panel--filters .btn--ghost:hover { background: rgba(255,255,255,.14); color: #fff; }
/* --primary on --nav is blue on blue. White keeps the ring visible for anyone
   tabbing through the filters. */
.panel--filters :focus-visible { outline-color: #fff; }
.detail__title { color: var(--nav); }
.detail__facts { list-style: none; margin: var(--s-lg) 0 0; padding: 0; }
.detail__facts li {
  display: flex; gap: var(--s-md); padding: 10px 0;
  border-top: 1px solid var(--border); font-size: 15px;
}
.detail__facts dt, .detail__facts .k {
  color: var(--muted); min-width: 120px; font-size: 14px;
}
.prose { max-width: 68ch; }
.prose h2 { margin-top: var(--s-xl); }

/* ---------- forms -------------------------------------------------------- */
/* Bounded sections inside the two-column form — Status, Categories, Delete.
   On a fieldset the border must be set explicitly: the UA default is a 2px
   groove that reads as a dark box and matches nothing else on the page. The
   legend is pulled back to the padding edge so it sits like a heading rather
   than straddling the border. */
.boxed {
  border: 1px solid var(--border); border-radius: var(--radius-card);
  background: var(--card); padding: var(--s-md);
  margin: 0 0 var(--s-md);
}
.boxed > legend { padding: 0; margin-bottom: var(--s-sm); }
.boxed > h3 { font-size: 15px; margin-bottom: var(--s-sm); }
.boxed > :last-child { margin-bottom: 0; }
.boxed__option { display: block; font-size: 14px; margin-bottom: 6px; }

/* Seam between the directory's properties and the business's own details.
   A UA hr is a shaded 3D bevel; this is one hairline in the border colour,
   matching the boxes it separates. */
.rule { border: 0; border-top: 1px solid var(--border); margin: var(--s-xl) 0; }

/* The UA rule for [hidden] is display:none, but it loses to any author rule
   that sets display — .field below among them — so an element carrying the
   attribute stayed on screen. Restored here, ahead of everything that could
   override it. */
[hidden] { display: none !important; }

.field { display: block; margin-bottom: var(--s-md); }

/* A section heading inside a boxed fieldset, for the sections a visitor is
   choosing between rather than merely filling in. */
.boxed__legend {
  font-family: var(--font-display); font-size: 17px; font-weight: 600;
  letter-spacing: -0.3px; color: var(--text-strong);
  padding: 0; margin-bottom: var(--s-sm);
}
/* Required marker. The asterisk is decoration — the input's own required
   attribute is what assistive technology announces. */
.req { color: #B3261E; margin-left: 2px; }
.req-note { font-size: 13px; color: var(--muted); }

/* ---------- billing panel ------------------------------------------------
   One product on two billing cycles, shown as one segmented control rather
   than two plan cards: the cards in the reference are two different products,
   and duplicating a benefits list into both would imply a difference that does
   not exist here.

   The control is rendered once and stays visible across both steps, so there
   is literally one interval input on the page. Two synchronised toggles is
   where a visitor ends up buying a year of one thing and a month of another —
   and the two subscriptions have to share an interval to be alignable. */
/* Two separate ovals rather than one segmented strip. A strip relies on a
   subtle fill to say which half is live; two buttons where the selected one is
   filled in the same blue as the submit button cannot be misread. */
.seg { display: flex; gap: var(--s-sm); }
.seg__opt {
  flex: 1; display: flex; align-items: center; justify-content: center; gap: 6px;
  padding: 10px 16px; cursor: pointer;
  border: 1px solid var(--border); border-radius: var(--radius-pill);
  background: var(--card); color: var(--text);
  font-size: 15px; font-weight: 600;
  transition: background-color .12s ease, color .12s ease, border-color .12s ease;
}
.seg__opt:hover { border-color: var(--primary); }
/* Driven by a class from the script AND by :has(), so the selected state
   survives either being unavailable. */
.seg__opt--on, .seg__opt:has(input:checked) {
  background: var(--primary); border-color: var(--primary); color: #fff;
}
.seg__opt:has(input:focus-visible) { outline: 3px solid var(--nav); outline-offset: 2px; }
.seg__opt input { position: absolute; width: 1px; height: 1px; opacity: 0; }

.price-lead { display: flex; align-items: baseline; gap: 6px; margin-top: var(--s-lg); }
.price-lead__amount {
  font-family: var(--font-display); font-size: 42px; font-weight: 700;
  letter-spacing: -1.6px; color: var(--nav); line-height: 1;
}
.price-lead__per { font-size: 15px; color: var(--muted); }
/* The struck figure is only ever a real number: the undiscounted rate for the
   same period. Never an invented former price. */
.price-was { margin-top: 6px; font-size: 16px; color: var(--muted); }
.price-was s { text-decoration-thickness: 1px; margin-right: 6px; }
.price-note { margin-top: 6px; font-size: 13px; color: var(--muted); }
.price-recap {
  margin-top: var(--s-md); padding: 10px var(--s-md);
  background: var(--bg); border-radius: var(--radius); font-size: 14px; color: var(--text);
}

/* The billing breakdown. Both products are itemised rather than rolled into a
   single figure: the customer is agreeing to two things, and a combined total
   hides which half of it is the placement they could drop later. The renewal
   line is not optional either — the discount runs once, so the ongoing price
   is higher than the one being agreed to, and it says so. */
.bill { margin-top: var(--s-md); border-top: 1px solid var(--border); }
.bill__row {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: var(--s-md); padding: 8px 0; font-size: 15px;
  border-bottom: 1px solid var(--border);
}
.bill__row span:last-child { white-space: nowrap; font-variant-numeric: tabular-nums; }
.bill__row--off { color: var(--muted); font-size: 14px; }
.bill__row--total { font-weight: 700; font-size: 17px; border-bottom: 0; }
.bill__renew { margin-top: 6px; font-size: 13px; color: var(--muted); }

.includes { list-style: none; margin: var(--s-lg) 0 0; padding: 0; }
.includes li {
  position: relative; padding-left: 24px; margin-bottom: 8px;
  font-size: 14px; line-height: 21px; color: var(--text);
}
.includes li::before {
  content: ''; position: absolute; left: 0; top: 6px;
  width: 12px; height: 7px; border-left: 2px solid var(--primary);
  border-bottom: 2px solid var(--primary); transform: rotate(-45deg);
}
.includes--warm li::before { border-color: var(--featured); }

.step-head { font-size: 13px; font-weight: 600; color: var(--muted); letter-spacing: .3px; }
.step-back {
  background: none; border: 0; padding: 0; cursor: pointer;
  color: var(--primary); font-family: var(--font-body); font-size: 14px;
}
.step-back:hover { text-decoration: underline; }
.field__label {
  display: block; font-size: 14px; font-weight: 500;
  color: var(--text); margin-bottom: 6px;
}
.field__hint { display: block; font-size: 13px; color: var(--muted); margin-top: 4px; }
.input, .select, .textarea {
  width: 100%; background: #fff; color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius);
  padding: 10px 12px; font-family: var(--font-body); font-size: 15px;
}
.textarea { min-height: 110px; resize: vertical; }
.input:focus, .select:focus, .textarea:focus { border-color: var(--primary); }

.notice {
  border-radius: var(--radius); padding: 12px var(--s-md);
  font-size: 14px; margin-bottom: var(--s-lg);
  border: 1px solid var(--border); background: #fff;
}
.notice--ok { border-color: #2E7D51; color: #1B5E3A; background: #EEF7F1; }
.notice--warn { border-color: #A8741A; color: #7A5312; background: #FDF6E8; }
.notice--err { border-color: #B3261E; color: #8C1D18; background: #FDECEA; }

.price {
  font-family: var(--font-display); font-size: 44px; font-weight: 700;
  letter-spacing: -1px; color: var(--nav); line-height: 1;
}
.price small { display: block; font-family: var(--font-body); font-size: 14px;
  font-weight: 400; color: var(--muted); letter-spacing: 0; margin-top: 6px; }

.benefits { list-style: none; margin: var(--s-lg) 0; padding: 0; }
.benefits li {
  position: relative; padding-left: 26px; margin-bottom: 10px;
  font-size: 15px; color: var(--text);
}
.benefits li::before {
  content: ''; position: absolute; left: 0; top: 7px;
  width: 14px; height: 8px; border-left: 2px solid var(--primary);
  border-bottom: 2px solid var(--primary); transform: rotate(-45deg);
}

.split { display: grid; gap: var(--s-xl); }
@media (min-width: 900px) { .split { grid-template-columns: 1fr 1fr; align-items: start; } }

/* ---------- faq ---------------------------------------------------------- */
.faq { border-top: 1px solid var(--border); }
.faq details { border-bottom: 1px solid var(--border); padding: var(--s-md) 0; }
.faq summary {
  cursor: pointer; font-family: var(--font-display); font-weight: 600;
  font-size: 17px; color: var(--text-strong); letter-spacing: -0.3px;
  list-style: none;
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after { content: ' +'; color: var(--primary); font-weight: 700; }
.faq details[open] summary::after { content: ' –'; }
.faq p { margin: var(--s-sm) 0 0; color: var(--muted); max-width: 68ch; }

/* ---------- footer ------------------------------------------------------- */
.footer { background: var(--nav); color: var(--footer-text); margin-top: var(--s-3xl); }
.footer__inner {
  margin-inline: auto; max-width: var(--container);
  padding: var(--s-2xl) var(--s-lg);
}
.footer__grid { display: grid; gap: var(--s-xl); }
@media (min-width: 768px) { .footer__grid { grid-template-columns: 1fr 1fr; } }
.footer__blurb { font-size: 14px; line-height: 22px; max-width: 480px; margin: 12px 0 0; }
.footer__right { }
@media (min-width: 768px) { .footer__right { text-align: right; } }
.footer__meta { font-size: 14px; margin-bottom: 4px; }
.footer a { color: var(--footer-text); }
.footer a:hover { color: #fff; }
.footer__nav { display: flex; flex-wrap: wrap; gap: var(--s-md); margin-top: var(--s-md); font-size: 14px; }
@media (min-width: 768px) { .footer__nav { justify-content: flex-end; } }
.footer__legal {
  margin-top: 40px; padding-top: var(--s-lg);
  border-top: 1px solid rgba(255,255,255,.15); font-size: 13px;
}

/* ---------- admin -------------------------------------------------------- */
.adm { background: #fff; min-height: 100vh; }
.adm__bar { background: var(--nav); color: #fff; }
.adm__bar .nav__inner { padding-block: 10px; }
.adm__tabs { display: flex; gap: var(--s-md); flex-wrap: wrap; }
.adm__tab { color: rgba(255,255,255,.85); font-size: 14px; text-decoration: none; }
.adm__tab[aria-current="page"] { color: #fff; font-weight: 600; text-decoration: underline; }
.adm__who { font-size: 13px; color: rgba(255,255,255,.75); }

.table { width: 100%; border-collapse: collapse; font-size: 14px; }
.table th, .table td {
  text-align: left; padding: 10px 12px; border-bottom: 1px solid var(--border);
  vertical-align: top;
}
.table th { color: var(--muted); font-weight: 500; font-size: 13px; }
.table tr:hover td { background: var(--bg); }
/* Dates and their headers must not break mid-value. The width is what the
   content needs and no more — the Categories column absorbs the difference,
   since chips wrap gracefully and an ISO date does not. */
.table .nowrap { white-space: nowrap; width: 1%; }
/* Row title as link: inherits the cell's colour so the table does not turn into
   a wall of blue, and declares itself on hover and keyboard focus. */
.table a.rowlink { color: inherit; text-decoration: none; }
.table a.rowlink:hover { color: var(--primary); text-decoration: underline; }
.row-actions { display: flex; gap: var(--s-sm); flex-wrap: wrap; }

.chip {
  display: inline-block; font-size: 12px; padding: 2px 8px;
  border-radius: var(--radius-pill); background: var(--bg);
  border: 1px solid var(--border); color: var(--muted);
}
.chip--on { background: var(--primary); border-color: var(--primary); color: #fff; }

/* Review-status chips. Tinted rather than solid, so they read as an annotation
   next to the solid --primary "Published" chip rather than competing with it —
   review state gates nothing, and a chip as loud as the visibility one would
   imply otherwise. Each carries its own word, so colour is never the only
   signal. */
/* Status as plain coloured text in a table cell. Same hues as the chips and
   the legend, without the pill — every row has a status, so a pill on each one
   marked nothing out and only made the column noisy. */
.reviewtext            { font-size: 14px; }
.reviewtext--good      { color: #1F5A3A; }
.reviewtext--info      { color: #4F3583; }
.reviewtext--attention { color: #7A4D0C; }
.reviewtext--bad       { color: #8C1D18; }
.reviewtext--neutral   { color: var(--muted); }

.chip--good      { background: #E8F3ED; border-color: #9BC7AF; color: #1F5A3A; }
.chip--info      { background: #E9EFF7; border-color: #A9BFDC; color: #2E4D7A; }
.chip--attention { background: #FBF0DF; border-color: #E0BE86; color: #7A4D0C; }
.chip--bad       { background: #FDECEA; border-color: #E4A9A3; color: #8C1D18; }
/* Archived is inactive, not a verdict — it keeps the plain chip's grey so it
   recedes rather than competing with Rejected two rows below it. */
.chip--neutral   { background: var(--bg); border-color: var(--border); color: var(--muted); }

.stack > * + * { margin-top: var(--s-md); }
.muted { color: var(--muted); }
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; }
.right { text-align: right; }
.inline-form { display: inline; }

@media (min-width: 768px) {
  h1 { font-size: 48px; letter-spacing: -1.2px; }
  h2 { font-size: 36px; letter-spacing: -0.9px; line-height: 40px; }
}
@media (min-width: 1024px) {
  h1 { font-size: 60px; line-height: 60px; letter-spacing: -1.5px; }
}

/* ---------- status toggles (admin) ----------------------------------------
   Checkbox-backed so the control is a real form field: keyboard reachable,
   announced as a checkbox, and submitted without JavaScript. Green reads as
   on, red as off, matching the existing notice and danger colours.        */

.toggles { display: grid; gap: var(--s-lg); margin-top: var(--s-sm); }
.toggle-row { display: grid; gap: var(--s-sm); }

/* Two lines: title and switch on the first, the sentence explaining it on the
   second. Reading order matches how the control is used — what it is called,
   whether it is on, then what it means — rather than running all three
   together on one line, which is what made this hard to scan.

   The head is width-capped so the three switches line up with each other
   instead of drifting to the far edge of the column, where they would sit
   against the expiry field and read as belonging to it. */
.toggle { display: grid; gap: 4px; cursor: pointer; }
.toggle__head {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s-md); max-width: 300px;
}
.toggle__input { position: absolute; opacity: 0; width: 0; height: 0; }
.toggle__track {
  position: relative; flex: 0 0 auto;
  width: 52px; height: 30px; border-radius: var(--radius-pill);
  background: #B3261E; border: 1px solid rgba(0,0,0,.12);
  transition: background .15s ease;
}
.toggle__knob {
  position: absolute; top: 3px; left: 3px;
  width: 22px; height: 22px; border-radius: 50%;
  background: #fff; box-shadow: var(--shadow-sm);
  transition: transform .15s ease;
}
.toggle__input:checked + .toggle__track { background: #2E7D51; }
.toggle__input:checked + .toggle__track .toggle__knob { transform: translateX(22px); }
.toggle__input:focus-visible + .toggle__track {
  outline: 2px solid var(--primary); outline-offset: 2px;
}
.toggle__input:disabled + .toggle__track { opacity: .5; cursor: not-allowed; }
/* inline-flex so a glyph can sit before the word without disturbing the
   baseline. Harmless on toggles that carry no glyph — the gap collapses. */
.toggle__text {
  display: inline-flex; align-items: center; gap: 6px;
  font-weight: 600; color: var(--text-strong);
}
/* Always lit, in the colour the same symbol carries in the listings table. It
   names the concept; the switch beside it reports the state. Colours are set
   here rather than inherited so they survive .toggle__text's own colour. */
.toggle__glyph { display: inline-flex; flex: 0 0 auto; }
.toggle__glyph svg { width: 18px; height: 18px; display: block; }
.toggle__glyph--publish { color: var(--primary); }
.toggle__glyph--list    { color: #2E7D51; }
.toggle__glyph--feature { color: var(--featured); }
.toggle__state { display: block; font-size: 13px; color: var(--muted); }

.toggle-expiry { display: grid; gap: 4px; max-width: 240px; padding-left: 68px; }
/* The pickup note is a sentence, not a date — it gets the width of the box it
   sits in rather than the narrow column the expiry dates use. */
.toggle-expiry--wide { max-width: none; }

/* Split rows: toggle left, the field it governs right. Applied on the listings
   form only. The resource form keeps its stacked rows, because its right-hand
   field is a sentence-length pickup note that wants the full width rather than
   a 220px column. Below 720px this collapses back to stacked, where the indent
   under the toggle returns to tie the field to its switch. */
.toggles--split .toggle-row {
  grid-template-columns: minmax(0, 1fr) minmax(0, 220px);
  align-items: end;
  gap: var(--s-md);
}
.toggles--split .toggle-expiry { padding-left: 0; max-width: none; }
@media (max-width: 720px) {
  .toggles--split .toggle-row { grid-template-columns: 1fr; }
  .toggles--split .toggle-expiry { padding-left: 68px; max-width: 240px; }
}
.toggle-expiry label { font-size: 13px; color: var(--muted); }
@media (max-width: 640px) { .toggle-expiry { padding-left: 0; } }

/* ---------- admin filter bar ---------------------------------------------- */
.filters {
  display: grid; gap: var(--s-md); align-items: end;
  grid-template-columns: 1fr;
}
@media (min-width: 720px)  { .filters { grid-template-columns: repeat(3, 1fr); } }
/* Search, seven selects, reset — both admin tables now carry a Review filter
   alongside their existing six. */
@media (min-width: 1100px) { .filters { grid-template-columns: 2fr repeat(7, 1fr) auto; } }
.filters .field__label { margin-bottom: 4px; }
.filters .select, .filters .input { width: 100%; }

/* A glyph standing in for a word in a table cell. The accessible name rides
   along in a visually-hidden span, so the column is still readable aloud and
   still searchable by anyone using find-in-page. */
.cellicon { display: inline-flex; color: var(--muted); }
.cellicon svg { width: 20px; height: 20px; display: block; }
.cellicon--on { color: #2E7D51; }
.visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ---------- tab strip (admin edit forms) ------------------------------------
   The full-width sections at the foot of a form. The strip sits on the page
   background with the active tab reading as the front edge of the panel below
   it, so the two are one object rather than a row of buttons above a box.

   Panels are visible until the script hides them, which is what keeps the
   sections reachable when JavaScript does not run.                          */

.tabs__strip {
  display: flex; flex-wrap: wrap; gap: 4px;
  border-bottom: 1px solid var(--border);
}
.tabs__tab {
  background: none; border: 1px solid transparent; border-bottom: 0;
  border-radius: var(--radius) var(--radius) 0 0;
  padding: var(--s-sm) var(--s-md); margin-bottom: -1px;
  font-family: var(--font-body); font-size: 14px; font-weight: 500;
  color: var(--muted); cursor: pointer;
}
.tabs__tab:hover { color: var(--text-strong); }
.tabs__tab[aria-selected="true"] {
  background: var(--card); border-color: var(--border);
  color: var(--text-strong); font-weight: 600;
}
/* Square off the corner that meets the strip, so the panel and its tab join. */
.tabs__panel { border-top-left-radius: 0; margin-top: 0; }
.tabs__panel[hidden] { display: none; }

/* A lighter rule for dividing content inside a boxed fieldset, where the full
   --s-xl spacing of .rule would push the box open. */
.rule--tight { margin: var(--s-md) 0; }

/* ---------- checkbox dropdown (admin filters) -------------------------------
   The status filter picks a set, which a one-line dropdown cannot express and a
   native multiple-select can only express by being as tall as its options —
   which distorted this row. So: a trigger the exact size of its neighbours, and
   a panel that floats over the page when opened, costing no layout height.

   The trigger carries the .select class as well, so it inherits the field
   styling and never drifts from the controls beside it.                     */

.msel__wrap { position: relative; }
.msel__toggle {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s-sm);
  text-align: left; cursor: pointer;
}
.msel__summary { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.msel__caret { flex: 0 0 auto; font-size: 11px; color: var(--muted); }

/* Floats above the table below it. z-index clears the sticky nav at 50. */
.msel__panel {
  position: absolute; z-index: 60; top: calc(100% + 4px); left: 0;
  min-width: 100%; width: max-content; max-width: 260px;
  background: #fff; border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow-lg);
  padding: var(--s-sm);
}
.msel__panel[hidden] { display: none; }

.msel__actions {
  display: flex; gap: var(--s-md);
  padding: 0 6px var(--s-sm); margin-bottom: 6px;
  border-bottom: 1px solid var(--border);
}
/* Buttons rather than anchors: they act on this page and go nowhere, so an
   href would be a lie and a bare anchor would drop out of the tab order. */
.msel__link {
  background: none; border: 0; padding: 0;
  color: var(--primary); font-family: var(--font-body); font-size: 13px;
  text-decoration: underline; cursor: pointer;
}
.msel__link:hover { color: var(--primary-hover); }

.msel__opt {
  display: flex; align-items: center; gap: var(--s-sm);
  padding: 5px 6px; border-radius: 4px;
  font-size: 14px; color: var(--text); white-space: nowrap; cursor: pointer;
}
.msel__opt:hover { background: var(--bg); }
/* accent-color repaints the checked box with our blue instead of the browser's
   own — Chrome's default is a far brighter blue than anything in this palette
   and read as belonging to a different site. --primary rather than --nav: it is
   the action blue used by buttons and links, including the All and Clear links
   directly above these boxes. */
.msel__opt input { flex: 0 0 auto; margin: 0; cursor: pointer; accent-color: var(--primary); }


/* ---------- row glyph legend (admin) ---------------------------------------
   A key to the row states, sitting between the filters and the count. Reset as
   a list because it is one semantically — a set of terms and their symbols —
   without the bullets and indent a list carries by default.

   Ranged right and held to one line, so it sits directly over the Actions
   column whose glyphs it names. The glyph colours match the on-state colours
   those same symbols carry in that column, so the legend is not a second
   palette to learn.                                                          */

/* The band between the filters and the table: creating action ranged left, key
   ranged right. Wraps on a narrow window so the legend drops below the button
   rather than crushing it — the legend itself still holds its single line. */
.legend-row {
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; gap: var(--s-sm) var(--s-md);
  margin: calc(var(--s-md) * 1.1) 0;
}
.legend-row .btn { flex: 0 0 auto; }

.legend {
  /* One line, ranged right, so the key sits over the Actions column where the
     glyphs it describes actually appear. nowrap keeps it to the single line it
     is meant to be; overflow-x lets a narrow window scroll the strip instead of
     the whole page. The safe keyword stops flex-end from pushing the first
     entry off the unreachable side when it does overflow — a browser that does
     not know it drops the declaration and falls back to left-aligned, which is
     merely the old layout rather than a broken one.

     No backticks in this comment, or any comment in this file: the stylesheet
     is a template literal and one would end the string here. */
  display: flex; flex-wrap: nowrap; align-items: center;
  justify-content: safe flex-end;
  overflow-x: auto;
  gap: var(--s-sm) var(--s-lg);
  list-style: none; padding: 0;
  /* Spacing now belongs to .legend-row, which owns both sides of the band.
     min-width:0 is what lets this shrink inside that flex row so its own
     overflow-x can take effect instead of forcing the row wider. */
  margin: 0; flex: 1 1 auto; min-width: 0;
}
.legend__item {
  display: inline-flex; align-items: center; gap: 6px;
  /* 17px, a third up from the 13px used for secondary admin text. The legend is
     read once by someone learning the glyphs, so it is sized to be read rather
     than skimmed — deliberately above the table's own 14px. */
  font-size: 17px; color: var(--muted); white-space: nowrap;
}
.legend__icon { display: inline-flex; }
.legend__icon svg { width: 18px; height: 18px; display: block; }

.legend__icon--validated { color: var(--validated); }
.legend__icon--published { color: var(--primary); }
.legend__icon--verified  { color: var(--verified); }
.legend__icon--listed    { color: #2E7D51; }
.legend__icon--featured  { color: var(--featured); }
.legend__icon--archived  { color: var(--archived); }

/* ---------- row action glyphs (admin) --------------------------------------
   Icon-only buttons. State is carried by BOTH shape and colour where the glyph
   allows it — star and bookmark switch outline to filled — because colour alone
   fails for anyone who cannot separate grey from amber. Every button also
   carries an aria-label naming the action and the current state, so 175 rows do
   not read as a wall of unnamed buttons.                                     */

.actions { display: flex; align-items: center; justify-content: flex-end; gap: 2px; }
.iconbtn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; padding: 0;
  background: none; border: 0; border-radius: var(--radius);
  color: var(--muted); cursor: pointer;
  transition: background-color .12s ease, color .12s ease;
}
.iconbtn:hover { background: var(--bg); color: var(--text-strong); }
.iconbtn:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }
.iconbtn svg { width: 20px; height: 20px; display: block; }
/* Archive is the one action whose OFF state is not muted grey. It is always
   available and always meaningful, so it sits at the theme blue like the other
   live controls, and previews its result by turning brown on hover. When the
   row IS archived the button shows the unarchive glyph, already brown. */
.iconbtn--archive       { color: var(--primary); }
.iconbtn--archive:hover { color: var(--archived); }
.iconbtn--on-archive       { color: var(--archived); }
.iconbtn--on-archive:hover { color: var(--archived); }

.iconbtn--on-publish  { color: var(--primary); }
.iconbtn--on-list     { color: #2E7D51; }
.iconbtn--on-feature  { color: var(--featured); }
/* Edit and delete are always available — they have no off state, so they must
   not sit at the --muted grey that means "switched off" on the three toggles. */
.iconbtn--plain       { color: var(--text); }
.iconbtn--danger:hover { color: #B3261E; background: #FDECEA; }

/* ---------- confirm dialogs ------------------------------------------------
   Real <dialog>s rather than window.confirm(), so the prompt appears centred
   over the table instead of pinned to the top of the window as browser chrome.
   Escape closes them, focus is trapped, and the backdrop comes free. Deleting
   also needs a typed slug, which a window.confirm() could not have carried.  */

dialog.confirm {
  /* margin:auto centres in both axes against the modal's containing block;
     the top/bottom margins are what a UA sheet leaves at 0. */
  margin: auto;
  border: 1px solid var(--border); border-radius: var(--radius-card);
  padding: var(--s-lg); max-width: 520px; width: calc(100% - 32px);
  color: var(--text); background: var(--card);
  box-shadow: var(--shadow-lg);
}
dialog.confirm::backdrop { background: rgba(28,44,69,.45); }
dialog.confirm h3 { margin-bottom: var(--s-sm); }
dialog.confirm p { margin: 0; }
dialog.confirm .row-actions { display: flex; gap: var(--s-sm); margin-top: var(--s-md); }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    transition-duration: .001ms !important;
  }
}
