/* ============================================================================
   CivilHive Design System — 03 · Layout
   ----------------------------------------------------------------------------
   The page skeletons: app shell, marketing shell, auth split, admin console,
   and the list-page pattern (filter rail + toolbar + results) that the
   reference design is built around.

   Everything here is CSS Grid / Flexbox on our own class names. Bootstrap's
   grid stays available for in-page column work in legacy templates, but no
   shell depends on it.
   ========================================================================== */

/* ─── Containers ──────────────────────────────────────────────────────────
   Fluid: content uses the full viewport width rather than being capped at a
   fixed measure. Gutters grow with the viewport so the content never touches
   the edge on a wide monitor.

   `--prose` is the deliberate exception and stays capped. Running text has a
   readability limit regardless of how wide the screen is — a 200-character
   line in the terms page or a blog post is unreadable no matter how much room
   there is for it. */
.cv-container,
.cv-container--wide,
.cv-container--prose {
  width: 100%;
  margin-inline: auto;
  padding-inline: var(--cv-sp-5);
}
.cv-container--prose { max-width: var(--cv-container-prose); }

@media (min-width: 768px)  { .cv-container, .cv-container--wide, .cv-container--prose { padding-inline: var(--cv-sp-8); } }
@media (min-width: 1600px) { .cv-container, .cv-container--wide { padding-inline: var(--cv-sp-12); } }
@media (min-width: 2000px) { .cv-container, .cv-container--wide { padding-inline: var(--cv-sp-16); } }

/* Bootstrap's containers, and the legacy `.wrap`, adopt the same contract.

   Bootstrap is this app's grid and utility substrate, and its `.container`
   caps width at a per-breakpoint measure (1320px at the top) — the opposite
   of the decision taken for this product, which is that a page uses the width
   it is given. Rather than edit `class="container"` on the templates that
   still carry it, the class itself is redefined here: one rule, and a page
   that has not been rebuilt yet is fluid too. `.container-fluid` was already
   full-width and only needs the gutter scale to match.

   This is deliberate and permanent, not a migration shim — do not move it to
   compat.css, now deleted. Anything that genuinely needs a measure
   uses `.cv-container--prose` or its own `ch`-based cap. */
.container, .container-sm, .container-md, .container-lg,
.container-xl, .container-xxl, .container-fluid, .wrap {
  width: 100%;
  max-width: none;
  margin-inline: auto;
  padding-inline: var(--cv-sp-5);
}
@media (min-width: 768px) {
  .container, .container-sm, .container-md, .container-lg,
  .container-xl, .container-xxl, .container-fluid, .wrap { padding-inline: var(--cv-sp-8); }
}
@media (min-width: 1600px) {
  .container, .container-sm, .container-md, .container-lg,
  .container-xl, .container-xxl, .container-fluid, .wrap { padding-inline: var(--cv-sp-12); }
}
@media (min-width: 2000px) {
  .container, .container-sm, .container-md, .container-lg,
  .container-xl, .container-xxl, .container-fluid, .wrap { padding-inline: var(--cv-sp-16); }
}

/* A page-level wrapper that still hard-codes its own cap in a `style`
   attribute or an inline <style> block cannot be reached by a class rule, so
   those are removed from the templates themselves as each page is rebuilt.
   `.cv-fluid` is the escape hatch for one that has to keep its markup: it
   beats an inline max-width only with `!important`, which is why it is the
   single place in this stylesheet that uses it. */
.cv-fluid { max-width: none !important; }

/* Script-driven visibility. A page that hides a panel must still show it when
   scripting is off, so this is applied BY script and never by the server: the
   estimator's two input panels both render without it, which is what makes the
   form usable with no JavaScript.

   `!important` for the same reason `.cv-fluid` has it. This is a utility, and a
   utility that loses to the component it is applied to is worse than useless:
   `.cv-field` sets `display: flex` in components.css, which loads AFTER this
   file, so at equal specificity source order handed it the win and a "hidden"
   panel stayed on screen. Measured, not guessed. */
.cv-hidden { display: none !important; }

/* Vertical rhythm primitives. `--gap` is overridable per instance so a stack
   does not need a modifier class for every spacing value. */
.cv-stack { display: flex; flex-direction: column; gap: var(--gap, var(--cv-sp-4)); }
.cv-row   { display: flex; align-items: center; gap: var(--gap, var(--cv-sp-3)); flex-wrap: wrap; }
.cv-row--between { justify-content: space-between; }
.cv-row--end     { justify-content: flex-end; }
.cv-row--top     { align-items: flex-start; }
.cv-row--nowrap  { flex-wrap: nowrap; }
.cv-spacer { flex: 1 1 auto; }

.cv-section { padding-block: clamp(var(--cv-sp-10), 6vw, var(--cv-sp-24)); }
.cv-section--tight { padding-block: var(--cv-sp-10); }
.cv-section--sunk  { background-color: var(--cv-surface-sunk); }

/* ============================================================================
   Header — one bar, used by every shell
   ============================================================================ */
.cv-header {
  position: sticky;
  top: 0;
  z-index: var(--cv-z-header);
  background-color: var(--cv-surface);
  border-bottom: 1px solid var(--cv-line);
}
.cv-header__inner {
  display: flex;
  align-items: center;
  gap: var(--cv-sp-4);
  min-height: var(--cv-header-h);
  padding-inline: var(--cv-sp-5);
  margin-inline: auto;
  width: 100%;
}
@media (min-width: 768px)  { .cv-header__inner { padding-inline: var(--cv-sp-8); } }
@media (min-width: 1600px) { .cv-header__inner { padding-inline: var(--cv-sp-12); } }
@media (min-width: 2000px) { .cv-header__inner { padding-inline: var(--cv-sp-16); } }

.cv-brand {
  display: inline-flex;
  align-items: center;
  gap: var(--cv-sp-2);
  flex: 0 0 auto;
  font-size: var(--cv-text-h4);
  font-weight: var(--cv-weight-bold);
  letter-spacing: -0.02em;
  color: var(--cv-ink);
}
.cv-brand:hover { color: var(--cv-ink); }
.cv-brand img { height: 30px; width: auto; display: block; }

/* Primary navigation. The active item is marked with amber ink plus a 2px
   underline anchored to the header's bottom border — colour alone would not
   be enough for anyone who cannot distinguish it. */
.cv-nav { display: none; align-items: center; gap: var(--cv-sp-1); flex: 1 1 auto; }
@media (min-width: 1100px) { .cv-nav { display: flex; } }

.cv-nav__link {
  position: relative;
  /* The nav is a mix of <a> and <button> (the dropdown toggles). Reset the
     button chrome so the two are indistinguishable. */
  appearance: none;
  border: 0;
  background: transparent;
  font-family: inherit;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: var(--cv-sp-2);
  padding: var(--cv-sp-2) var(--cv-sp-3);
  border-radius: var(--cv-r-md);
  font-size: var(--cv-text-sm);
  font-weight: var(--cv-weight-medium);
  color: var(--cv-ink-2);
  white-space: nowrap;
  transition: color var(--cv-t-fast) var(--cv-ease),
              background-color var(--cv-t-fast) var(--cv-ease);
}
.cv-nav__link:hover { color: var(--cv-ink); background-color: var(--cv-surface-sunk); }
.cv-nav__link.is-active {
  color: var(--cv-accent-text);
  font-weight: var(--cv-weight-semibold);
}
.cv-nav__link.is-active::after {
  content: "";
  position: absolute;
  left: var(--cv-sp-3);
  right: var(--cv-sp-3);
  bottom: calc(-1 * (var(--cv-header-offset, var(--cv-header-h)) - 100%) / 2 - 1px);
  height: 2px;
  border-radius: 2px 2px 0 0;
  background-color: var(--cv-accent);
}

.cv-nav__link.dropdown-toggle::after {
  margin-left: 0.15rem;
  border-top-color: currentColor;
  opacity: 0.55;
}

.cv-header__tools { display: flex; align-items: center; gap: var(--cv-sp-2); flex: 0 0 auto; margin-left: auto; }

/* Controls that give up their place on a phone. The header carries brand +
   create + messages + notifications + theme + avatar + burger; at 390px that
   measured 428px, so the burger — the one control that reaches everything
   else — was pushed off-screen and every authenticated page scrolled
   sideways. What hides here is reachable from the mobile menu instead. */
@media (max-width: 480px) { .cv-hide-xs { display: none !important; } }

/* Header search — the reference's global "Search tenders, projects…" field. */
.cv-header__search { display: none; position: relative; flex: 0 1 260px; }
@media (min-width: 1280px) { .cv-header__search { display: block; } }
/* Room on the left for the magnifier, which is absolutely positioned over the
   field. This lost the cascade until base.css's by-element control rule was
   dropped to zero specificity — see the note there. */
.cv-header__search input[type="search"] {
  min-height: 2.25rem;
  padding: 0.375rem 0.75rem 0.375rem 2.25rem;
  border-radius: var(--cv-r-pill);
  background-color: var(--cv-surface-sunk);
  border-color: transparent;
  font-size: var(--cv-text-xs);
}
.cv-header__search input:focus { background-color: var(--cv-surface); }
.cv-header__search .bi {
  position: absolute;
  left: 0.8rem; top: 50%;
  transform: translateY(-50%);
  color: var(--cv-ink-3);
  font-size: 0.9rem;
  pointer-events: none;
}

/* Round icon buttons in the header: bell, messages, theme. */
.cv-icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  position: relative;
  width: 2.25rem; height: 2.25rem;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background-color: transparent;
  color: var(--cv-ink-2);
  font-size: 1.05rem;
  cursor: pointer;
  transition: background-color var(--cv-t-fast) var(--cv-ease), color var(--cv-t-fast) var(--cv-ease);
}
.cv-icon-btn:hover { background-color: var(--cv-surface-sunk); color: var(--cv-ink); }
.cv-icon-btn:disabled { opacity: 0.4; cursor: default; }
.cv-icon-btn:disabled:hover { background-color: transparent; color: var(--cv-ink-2); }
/* Inside a row of fields, where a 2.25rem circle would be the tallest thing. */
.cv-icon-btn--sm { width: 2rem; height: 2rem; font-size: 0.9rem; }
.cv-icon-btn__dot {
  position: absolute;
  top: 0.3rem; right: 0.3rem;
  min-width: 1.05rem; height: 1.05rem;
  padding: 0 0.25rem;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--cv-r-pill);
  border: 2px solid var(--cv-surface);
  background-color: var(--cv-danger);
  color: #fff;
  font-size: 0.6rem;
  font-weight: var(--cv-weight-bold);
  line-height: 1;
}

.cv-burger { display: inline-flex; }
@media (min-width: 1100px) { .cv-burger { display: none; } }

/* Mobile nav drawer — the same links, revealed by the burger. */
.cv-mobile-nav { border-top: 1px solid var(--cv-line); background-color: var(--cv-surface); }
.cv-mobile-nav .cv-nav__link { width: 100%; border-radius: 0; padding: var(--cv-sp-3) var(--cv-sp-5); }
.cv-mobile-nav .cv-nav__link.is-active::after { display: none; }
.cv-mobile-nav .cv-nav__link.is-active {
  background-color: var(--cv-accent-soft);
  box-shadow: inset 3px 0 0 var(--cv-accent);
}
@media (min-width: 1100px) { .cv-mobile-nav { display: none !important; } }

/* ============================================================================
   Page header — title block above content
   ============================================================================
   Deliberately unfilled: the old design put every page title on a brand
   gradient band, which meant the loudest thing on every screen was a
   decoration. Here it is dark type on the page, closed by a hairline.
   ========================================================================== */
.cv-page-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--cv-sp-4);
  flex-wrap: wrap;
  padding-bottom: var(--cv-sp-5);
  margin-bottom: var(--cv-sp-6);
  border-bottom: 1px solid var(--cv-line);
}
.cv-page-head--plain { border-bottom: 0; padding-bottom: 0; }
.cv-page-head__text { min-width: 0; }
.cv-page-head__title { margin: 0; font-size: var(--cv-text-h2); letter-spacing: var(--cv-track-h2); }
.cv-page-head__sub { margin: var(--cv-sp-2) 0 0; color: var(--cv-ink-2); font-size: var(--cv-text-sm); max-width: 60ch; }
.cv-page-head__actions { display: flex; align-items: center; gap: var(--cv-sp-2); flex-wrap: wrap; }

.cv-breadcrumb {
  display: flex; align-items: center; gap: var(--cv-sp-2);
  flex-wrap: wrap;
  margin-bottom: var(--cv-sp-3);
  padding: 0; list-style: none;
  font-size: var(--cv-text-xs);
  color: var(--cv-ink-3);
}
.cv-breadcrumb li { margin: 0; display: inline-flex; align-items: center; gap: var(--cv-sp-2); }
.cv-breadcrumb li + li::before { content: "/"; color: var(--cv-ink-4); }
.cv-breadcrumb a { color: var(--cv-ink-3); }
.cv-breadcrumb a:hover { color: var(--cv-accent-text); }
.cv-breadcrumb [aria-current] { color: var(--cv-ink); font-weight: var(--cv-weight-medium); }

/* ============================================================================
   App shell
   ============================================================================ */
.cv-app { display: flex; flex-direction: column; min-height: 100vh; min-height: 100svh; }
.cv-main { flex: 1 1 auto; padding-block: var(--cv-sp-8) var(--cv-sp-16); }
.cv-main--flush { padding-block: 0; }

/* ============================================================================
   List page — filter rail + results
   ============================================================================
   The core reference layout. One grid, reused by every list page so a user who
   learns Tenders already knows Marketplace.
   ========================================================================== */
.cv-listing { display: grid; grid-template-columns: 1fr; gap: var(--cv-sp-6); align-items: start; }
@media (min-width: 992px) {
  .cv-listing { grid-template-columns: var(--cv-rail) minmax(0, 1fr); gap: var(--cv-sp-8); }
}

.cv-rail {
  position: static;
  min-width: 0;
}
@media (min-width: 992px) {
  .cv-rail {
    position: sticky;
    top: calc(var(--cv-header-offset, var(--cv-header-h)) + var(--cv-sp-6));
    max-height: calc(100vh - var(--cv-header-offset, var(--cv-header-h)) - var(--cv-sp-10));
    overflow-y: auto;
    overscroll-behavior: contain;
    /* Hide the scrollbar until the rail is actually hovered — a permanent
       track next to a filter list looks like a rendering artefact. */
    scrollbar-width: thin;
    scrollbar-color: transparent transparent;
  }
  .cv-rail:hover { scrollbar-color: var(--cv-line-strong) transparent; }
}

.cv-rail__panel {
  background-color: var(--cv-surface);
  border: 1px solid var(--cv-line);
  border-radius: var(--cv-r-lg);
  padding: var(--cv-sp-5);
}
.cv-rail__head {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: var(--cv-sp-4);
}
.cv-rail__title { margin: 0; font-size: var(--cv-text-h4); }
.cv-rail__group { padding-block: var(--cv-sp-4); border-top: 1px solid var(--cv-line-soft); }
.cv-rail__group:first-of-type { padding-top: 0; border-top: 0; }
.cv-rail__group:last-child { padding-bottom: 0; }

/* The filter rail is taller than a laptop viewport, and it is its own scroll
   container, so a submit button at the end of the form sits below the fold —
   the one control a keyword filter cannot do without. Pinning it to the bottom
   of the rail keeps it reachable without scrolling the filters back down. */
/* ============================================================================
   Detail layout — a record and its action panel
   ----------------------------------------------------------------------------
   The mirror of .cv-listing: the record's content in a measured main column,
   with the things you *do* about it (bid, award, contact, status) in a panel
   that stays in view while you read. The panel comes second in the source so
   that on a phone the record leads and the actions follow, which is also the
   right reading order for a screen reader.
   ============================================================================ */
.cv-detail { display: grid; gap: var(--cv-sp-6); grid-template-columns: minmax(0, 1fr); align-items: start; }
@media (min-width: 1100px) {
  .cv-detail { grid-template-columns: minmax(0, 1fr) var(--cv-panel, 21rem); gap: var(--cv-sp-8); }
  .cv-detail__side {
    position: sticky;
    top: calc(var(--cv-header-offset, var(--cv-header-h)) + var(--cv-sp-6));
    max-height: calc(100vh - var(--cv-header-offset, var(--cv-header-h)) - var(--cv-sp-10));
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: thin;
    scrollbar-color: transparent transparent;
  }
  .cv-detail__side:hover { scrollbar-color: var(--cv-line-strong) transparent; }
}
.cv-detail__main { min-width: 0; }
.cv-detail__side { min-width: 0; display: flex; flex-direction: column; gap: var(--cv-sp-4); }

/* Running prose inside a record — a description the client typed. Capped for
   line length even though the page itself is fluid. */
.cv-prose { max-width: 72ch; color: var(--cv-ink-2); font-size: var(--cv-text-sm); line-height: var(--cv-leading); }
.cv-prose > *:first-child { margin-top: 0; }
.cv-prose > *:last-child { margin-bottom: 0; }

/* A back link above a page head. */
.cv-back {
  display: inline-flex; align-items: center; gap: var(--cv-sp-2);
  margin-bottom: var(--cv-sp-3);
  font-size: var(--cv-text-xs); font-weight: var(--cv-weight-medium); color: var(--cv-ink-3);
}
.cv-back:hover { color: var(--cv-ink); }

.cv-rail__foot { padding-top: var(--cv-sp-4); border-top: 1px solid var(--cv-line-soft); }
@media (min-width: 992px) {
  /* The panel gives up its bottom padding to the footer, which takes it as its
     own. A sticky element's constraint includes its margins, so keeping a
     negative bottom margin here would push the button below the scrollport
     edge by exactly that margin — only the horizontal bleed is a margin. */
  .cv-rail__panel:has(> .cv-rail__foot) { padding-bottom: 0; }
  .cv-rail__foot {
    position: sticky;
    bottom: 0;
    margin-inline: calc(var(--cv-sp-5) * -1);
    padding: var(--cv-sp-4) var(--cv-sp-5) var(--cv-sp-5);
    background-color: var(--cv-surface);
    border-radius: 0 0 var(--cv-r-lg) var(--cv-r-lg);
  }
}
.cv-rail__legend {
  display: block;
  margin-bottom: var(--cv-sp-3);
  font-size: var(--cv-text-2xs);
  font-weight: var(--cv-weight-semibold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--cv-ink-3);
}

/* On phones the rail collapses into a disclosure so results stay above the
   fold; <details> gives us that with no JavaScript. */
.cv-rail__mobile { margin-bottom: var(--cv-sp-4); }
.cv-rail__mobile > summary {
  display: flex; align-items: center; gap: var(--cv-sp-2);
  padding: var(--cv-sp-3) var(--cv-sp-4);
  background-color: var(--cv-surface);
  border: 1px solid var(--cv-line);
  border-radius: var(--cv-r-md);
  font-size: var(--cv-text-sm);
  font-weight: var(--cv-weight-medium);
  color: var(--cv-ink);
  cursor: pointer;
  list-style: none;
}
.cv-rail__mobile > summary::-webkit-details-marker { display: none; }
.cv-rail__mobile > summary::marker { content: ""; }
.cv-rail__mobile[open] > summary { border-radius: var(--cv-r-md) var(--cv-r-md) 0 0; border-bottom-color: transparent; }
.cv-rail__mobile > summary .bi-chevron-down { margin-left: auto; transition: transform var(--cv-t-fast) var(--cv-ease); }
.cv-rail__mobile[open] > summary .bi-chevron-down { transform: rotate(180deg); }

/* On desktop the rail is not a disclosure at all: the summary disappears and
   the panel is always shown, whether or not the <details> happens to be open.
   The element stays a <details> so the same markup collapses on a phone. */
@media (min-width: 992px) {
  .cv-rail__mobile > summary { display: none; }
  .cv-rail__mobile > *:not(summary) { display: block; }
}

/* ─── Results toolbar ─────────────────────────────────────────────────────*/
/* ─── Inline filter bar ───────────────────────────────────────────────────
   The rail (.cv-rail) is for a marketplace list with a dozen facets. A
   record's own sub-list — the progress log of one project, the invitations on
   one team — has two or three, and a 20rem rail beside it would be mostly
   empty. This is that lighter form: one row of controls that wraps. */
.cv-filterbar {
  display: flex;
  align-items: center;
  gap: var(--cv-sp-3);
  flex-wrap: wrap;
  margin-bottom: var(--cv-sp-4);
  padding: var(--cv-sp-3) var(--cv-sp-4);
  background-color: var(--cv-surface);
  border: 1px solid var(--cv-line);
  border-radius: var(--cv-r-lg);
}
.cv-filterbar__field { flex: 1 1 12rem; min-width: 0; }
.cv-filterbar__field--wide { flex: 2 1 18rem; }
.cv-filterbar .cv-btn { flex: 0 0 auto; }

.cv-toolbar {
  display: flex;
  align-items: center;
  gap: var(--cv-sp-3);
  flex-wrap: wrap;
  margin-bottom: var(--cv-sp-4);
}
.cv-toolbar__count { font-size: var(--cv-text-sm); color: var(--cv-ink-2); }
.cv-toolbar__count strong { color: var(--cv-ink); font-variant-numeric: tabular-nums; }
.cv-toolbar__controls {
  display: flex; align-items: center; gap: var(--cv-sp-2);
  margin-left: auto;
  flex-wrap: wrap;
}
@media (min-width: 640px) { .cv-toolbar__controls { flex-wrap: nowrap; } }
.cv-toolbar select { width: auto; min-width: 9.5rem; }

/* Grid / list view switch */
.cv-viewswitch { display: inline-flex; border: 1px solid var(--cv-line-strong); border-radius: var(--cv-r-md); overflow: hidden; }
.cv-viewswitch a, .cv-viewswitch button {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2.25rem; height: 2.25rem;
  border: 0; background-color: var(--cv-surface);
  color: var(--cv-ink-3);
  cursor: pointer;
  transition: background-color var(--cv-t-fast) var(--cv-ease), color var(--cv-t-fast) var(--cv-ease);
}
.cv-viewswitch a + a, .cv-viewswitch button + button { border-left: 1px solid var(--cv-line); }
.cv-viewswitch a:hover, .cv-viewswitch button:hover { background-color: var(--cv-surface-sunk); color: var(--cv-ink); }
.cv-viewswitch .is-active { background-color: var(--cv-ink); color: var(--cv-n-0); }

/* ─── Result collections ──────────────────────────────────────────────────*/
.cv-results { display: grid; gap: var(--cv-sp-4); }
/* 18rem, not 20: with the 272px rail and the 1320px container, a 20rem
   minimum lands three cards at 992px inside 952px of space and silently drops
   to two. 18rem fits three and still reads comfortably. */
.cv-results--grid { grid-template-columns: repeat(auto-fill, minmax(min(100%, 18rem), 1fr)); }

/* Applied-filter summary chips above the results. */
.cv-applied { display: flex; align-items: center; gap: var(--cv-sp-2); flex-wrap: wrap; margin-bottom: var(--cv-sp-4); }

/* ============================================================================
   Auth split
   ============================================================================ */
.cv-auth { display: grid; grid-template-columns: 1fr; min-height: 100vh; min-height: 100svh; }
@media (min-width: 992px) { .cv-auth { grid-template-columns: 1fr 1fr; } }

.cv-auth__aside {
  display: none;
  position: relative;
  flex-direction: column;
  justify-content: space-between;
  padding: var(--cv-sp-12);
  background-color: var(--cv-n-900);
  color: var(--cv-n-0);
  overflow: hidden;
  isolation: isolate;
}
@media (min-width: 992px) { .cv-auth__aside { display: flex; } }
.cv-auth__aside h1, .cv-auth__aside h2, .cv-auth__aside h3 { color: var(--cv-n-0); }
.cv-auth__aside p { color: rgb(255 255 255 / 0.72); }
/* A faint amber blueprint grid — enough to say "engineering" without becoming
   a texture the eye has to fight. */
.cv-auth__aside::before {
  content: "";
  position: absolute; inset: 0; z-index: -1;
  background-image:
    linear-gradient(rgb(244 155 20 / 0.10) 1px, transparent 1px),
    linear-gradient(90deg, rgb(244 155 20 / 0.10) 1px, transparent 1px);
  background-size: 56px 56px, 56px 56px;
  mask-image: radial-gradient(120% 90% at 20% 0%, #000 20%, transparent 78%);
  -webkit-mask-image: radial-gradient(120% 90% at 20% 0%, #000 20%, transparent 78%);
}
/* The brand mark inside the card. It exists for one reason: `.cv-auth__aside`
   — which carries the logo — is hidden below 992px, so without this an auth
   page on a phone showed no branding at all. Above 992px the panel has it, and
   a second copy in the card would just be the logo twice. */
.cv-authmark {
  display: inline-flex; align-items: center; justify-content: center;
  width: 3rem; height: 3rem;
  margin-bottom: var(--cv-sp-5);
  border-radius: var(--cv-r-md);
  background-color: var(--cv-surface);
  border: 1px solid var(--cv-line);
  box-shadow: var(--cv-shadow-sm);
}
/* Sized explicitly rather than left to `img { max-width: 100% }`. The page
   this replaces relied on a rule in the old civilhive.css, which this shell
   stopped loading — so the 2339px master rendered at the card's full width. */
.cv-authmark img { width: 1.75rem; height: 1.75rem; }
@media (min-width: 992px) { .cv-authmark { display: none; } }

/* A labelled rule — the "or" between a form's own submit and a federated
   sign-in button. The label sits in the gap rather than on top of the line, so
   it needs no opaque background and works on any card colour. */
.cv-sep {
  display: flex;
  align-items: center;
  gap: var(--cv-sp-3);
  color: var(--cv-ink-3);
  font-size: var(--cv-text-2xs);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.cv-sep::before,
.cv-sep::after {
  content: "";
  flex: 1 1 auto;
  height: 1px;
  background-color: var(--cv-line);
}

/* A one-time code. Wide letter-spacing and tabular figures so six digits read
   as six digits; `text-security` is deliberately absent — an OTP is short-lived
   and typing it blind is the main reason people mistype it. */
.cv-otp {
  text-align: center;
  font-size: var(--cv-text-h3);
  font-weight: var(--cv-weight-semibold);
  letter-spacing: 0.4em;
  text-indent: 0.4em;
  font-variant-numeric: tabular-nums;
}

/* The terminal state of a flow: a link was sent, a password was changed, a
   token had expired. One tinted tile, a heading, a line of explanation and a
   single onward action. It replaces the hand-styled centred blocks the four
   password-reset pages each carried their own copy of. */
.cv-outcome {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--cv-sp-3);
}
.cv-outcome__icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 3.5rem; height: 3.5rem;
  border-radius: var(--cv-r-lg);
  background-color: var(--cv-surface-sunk);
  color: var(--cv-ink-2);
  font-size: 1.5rem;
}
.cv-outcome__icon--success { background-color: var(--cv-success-soft); color: var(--cv-success-text); }
.cv-outcome__icon--danger  { background-color: var(--cv-danger-soft);  color: var(--cv-danger-text); }
.cv-outcome__text { margin: 0; max-width: 40ch; font-size: var(--cv-text-sm); color: var(--cv-ink-2); }
.cv-outcome__note { margin: 0; font-size: var(--cv-text-2xs); color: var(--cv-ink-3); }
.cv-outcome > .cv-btn { margin-top: var(--cv-sp-3); width: 100%; }

/* Role selection: two equal choices, each a whole card that submits its own
   form. Two columns from 720px so neither reads as the default. */
.cv-rolegrid {
  display: grid;
  gap: var(--cv-sp-4);
  grid-template-columns: minmax(0, 1fr);
}
@media (min-width: 720px) { .cv-rolegrid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.cv-rolecard {
  display: flex; flex-direction: column; align-items: flex-start;
  gap: var(--cv-sp-2);
  width: 100%;
  padding: var(--cv-sp-5);
  text-align: left;
  background-color: var(--cv-surface);
  border: 1px solid var(--cv-line);
  border-radius: var(--cv-r-lg);
  color: var(--cv-ink);
  cursor: pointer;
  transition: border-color var(--cv-t-fast) var(--cv-ease),
              box-shadow var(--cv-t-fast) var(--cv-ease);
}
.cv-rolecard:hover { border-color: var(--cv-ink-3); box-shadow: var(--cv-shadow-sm); }
.cv-rolecard:focus-visible { outline: none; box-shadow: var(--cv-ring); }
.cv-rolecard__icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2.5rem; height: 2.5rem; margin-bottom: var(--cv-sp-2);
  border-radius: var(--cv-r-md);
  background-color: var(--cv-accent-soft);
  color: var(--cv-accent-text);
  font-size: 1.15rem;
}
.cv-rolecard__title { margin: 0; font-size: var(--cv-text-md); font-weight: var(--cv-weight-semibold); }
.cv-rolecard__text { margin: 0; font-size: var(--cv-text-sm); color: var(--cv-ink-3); }
.cv-rolecard__go {
  margin-top: var(--cv-sp-3);
  font-size: var(--cv-text-xs);
  font-weight: var(--cv-weight-semibold);
  color: var(--cv-accent-text);
}

.cv-auth__main {
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: var(--cv-sp-8) var(--cv-sp-5);
  background-color: var(--cv-surface);
}
@media (min-width: 768px) { .cv-auth__main { padding: var(--cv-sp-12); } }
.cv-auth__card { width: 100%; max-width: 26rem; margin-inline: auto; }
/* A 26rem card fits a single column of fields, which is every auth page but
   one: role selection shows two choices side by side and needs the room. */
.cv-auth__card--wide { max-width: 40rem; }
/* A terminal state — link sent, password changed, token expired — centres the
   whole card. The shell owns the heading, so a page cannot centre it from the
   inside; this is how a page asks for it. */
.cv-auth__card--center { text-align: center; }

/* ============================================================================
   Admin console
   ============================================================================ */
.cv-console { display: flex; min-height: 100vh; min-height: 100svh; }
.cv-console__nav {
  position: fixed;
  inset-block: 0;
  left: 0;
  z-index: var(--cv-z-rail);
  width: var(--cv-console-nav);
  display: flex;
  flex-direction: column;
  background-color: var(--cv-surface);
  border-right: 1px solid var(--cv-line);
  transform: translateX(-100%);
  transition: transform var(--cv-t-mid) var(--cv-ease);
}
.cv-console__nav.is-open { transform: translateX(0); }
@media (min-width: 1100px) { .cv-console__nav { transform: translateX(0); } }

.cv-console__brand {
  display: flex; align-items: center; gap: var(--cv-sp-2);
  flex: 0 0 auto;
  padding: var(--cv-sp-4) var(--cv-sp-5);
  border-bottom: 1px solid var(--cv-line);
  font-weight: var(--cv-weight-bold);
  color: var(--cv-ink);
}
.cv-console__links {
  /* `min-height: 0` is what makes the `overflow-y` below actually work. A flex
     item defaults to `min-height: auto`, so this list refused to shrink under
     its own content height and the nav overflowed instead of scrolling —
     measured at 1288px of links in a 900px viewport, which put Audit log,
     Application log and "Back to site" permanently off-screen on any laptop.
     `overflow-y: auto` was already here and did nothing without this line. */
  flex: 1 1 auto; min-height: 0;
  overflow-y: auto; overscroll-behavior: contain;
  padding: var(--cv-sp-3); margin: 0; list-style: none;
}
.cv-console__links li { margin: 0; }
.cv-console__heading {
  padding: var(--cv-sp-4) var(--cv-sp-3) var(--cv-sp-2);
  font-size: var(--cv-text-2xs);
  font-weight: var(--cv-weight-semibold);
  letter-spacing: 0.07em;
  text-transform: uppercase;
  /* Was `--cv-ink-4`, the placeholder step: 2.39:1 in light, 3.32 in dark.
     A section heading in the console nav is real text. */
  color: var(--cv-ink-3);
}
.cv-console__link {
  display: flex; align-items: center; gap: var(--cv-sp-3);
  padding: var(--cv-sp-2) var(--cv-sp-3);
  margin-bottom: 2px;
  border-radius: var(--cv-r-md);
  font-size: var(--cv-text-sm);
  color: var(--cv-ink-2);
  transition: background-color var(--cv-t-fast) var(--cv-ease), color var(--cv-t-fast) var(--cv-ease);
}
.cv-console__link:hover { background-color: var(--cv-surface-sunk); color: var(--cv-ink); }
.cv-console__link.is-active {
  background-color: var(--cv-accent-soft);
  color: var(--cv-ink);
  font-weight: var(--cv-weight-semibold);
  box-shadow: inset 2px 0 0 var(--cv-accent);
}
.cv-console__link .bi { font-size: 1rem; color: var(--cv-ink-3); }
.cv-console__link.is-active .bi { color: var(--cv-accent-text); }

.cv-console__main { flex: 1 1 auto; min-width: 0; margin-left: 0; }
@media (min-width: 1100px) { .cv-console__main { margin-left: var(--cv-console-nav); } }
.cv-console__body { padding: var(--cv-sp-6) var(--cv-sp-5) var(--cv-sp-16); }
@media (min-width: 768px) { .cv-console__body { padding: var(--cv-sp-8) var(--cv-sp-8) var(--cv-sp-16); } }

.cv-console__scrim {
  position: fixed; inset: 0;
  z-index: calc(var(--cv-z-rail) - 1);
  background-color: var(--cv-overlay);
  opacity: 0; visibility: hidden;
  transition: opacity var(--cv-t-mid) var(--cv-ease), visibility var(--cv-t-mid);
}
.cv-console__scrim.is-open { opacity: 1; visibility: visible; }
@media (min-width: 1100px) { .cv-console__scrim { display: none; } }

/* ============================================================================
   Footer
   ============================================================================ */
.cv-footer {
  background-color: var(--cv-n-900);
  color: rgb(255 255 255 / 0.66);
  padding-block: var(--cv-sp-12) var(--cv-sp-8);
  font-size: var(--cv-text-sm);
}
.cv-footer h5 {
  margin-bottom: var(--cv-sp-4);
  color: var(--cv-n-0);
  font-size: var(--cv-text-xs);
  font-weight: var(--cv-weight-semibold);
  letter-spacing: 0.07em;
  text-transform: uppercase;
}
.cv-footer a { color: rgb(255 255 255 / 0.66); }
.cv-footer a:hover { color: var(--cv-amber-300); }
.cv-footer__grid {
  display: grid;
  gap: var(--cv-sp-8);
  grid-template-columns: 1fr;
}
@media (min-width: 640px) { .cv-footer__grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 992px) { .cv-footer__grid { grid-template-columns: 1.6fr repeat(3, 1fr); } }
.cv-footer__bar {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--cv-sp-4); flex-wrap: wrap;
  margin-top: var(--cv-sp-10);
  padding-top: var(--cv-sp-6);
  border-top: 1px solid rgb(255 255 255 / 0.12);
  font-size: var(--cv-text-xs);
}

/* A compact footer for dense app pages, where the marketing footer would be
   more chrome than the content above it. */
.cv-footer--slim {
  background-color: transparent;
  color: var(--cv-ink-3);
  border-top: 1px solid var(--cv-line);
  padding-block: var(--cv-sp-5);
  font-size: var(--cv-text-xs);
}
.cv-footer--slim a { color: var(--cv-ink-3); }
.cv-footer--slim a:hover { color: var(--cv-accent-text); }

/* ============================================================================
   Landing page composition
   ============================================================================ */

/* Hero: copy and search on the left, imagery on the right. The copy column is
   given the larger share — the picture is supporting evidence, not the pitch. */
.cv-home-hero {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--cv-sp-10);
  align-items: center;
}
@media (min-width: 992px) {
  .cv-home-hero { grid-template-columns: 1.15fr 1fr; gap: var(--cv-sp-16); }
}

/* The inline search card from the reference: labelled fields sitting in one
   row inside a single surface, so the whole thing reads as one control rather
   than four boxes that happen to be adjacent. */
.cv-searchbar {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--cv-sp-4);
  padding: var(--cv-sp-5);
  background-color: var(--cv-surface);
  border: 1px solid var(--cv-line);
  border-radius: var(--cv-r-lg);
  box-shadow: var(--cv-shadow-sm);
}
.cv-searchbar .cv-btn { min-height: 2.5rem; }
@media (max-width: 575.98px) {
  .cv-searchbar > .cv-field { flex: 1 1 100% !important; }
  .cv-searchbar .cv-btn { width: 100%; }
}

/* Two-up section: pitch beside a panel (the live-rates table). */
.cv-home-split {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--cv-sp-10);
  align-items: center;
}
@media (min-width: 992px) {
  .cv-home-split { grid-template-columns: 1fr 1.1fr; gap: var(--cv-sp-16); }
}

/* Closing call to action — the one dark band on the page. */
.cv-home-cta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--cv-sp-8);
  flex-wrap: wrap;
  padding: clamp(var(--cv-sp-8), 4vw, var(--cv-sp-16));
  background-color: var(--cv-n-900);
  border-radius: var(--cv-r-xl);
  color: rgb(255 255 255 / 0.72);
}

/* Specialty tiles — the "start with the trade you need" row. Auto-fitting
   rather than a fixed column count so a fluid container fills the width it
   has instead of leaving a gap at the end of the row. */
.cv-tilegrid {
  display: grid;
  gap: var(--cv-sp-3);
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
/* An explicit column count rather than auto-fit: there are eight specialties,
   and auto-fit at a fluid width lands six on the first row and orphans two on
   the second. Four-up gives two full rows at any desktop size. */
@media (min-width: 768px)  { .cv-tilegrid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (min-width: 1100px) { .cv-tilegrid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
.cv-tile {
  display: flex;
  flex-direction: column;
  gap: var(--cv-sp-1);
  padding: var(--cv-sp-5);
  background-color: var(--cv-surface);
  border: 1px solid var(--cv-line);
  border-radius: var(--cv-r-lg);
  color: var(--cv-ink);
  transition: border-color var(--cv-t-fast) var(--cv-ease),
              background-color var(--cv-t-fast) var(--cv-ease);
}
.cv-tile:hover { border-color: var(--cv-accent); background-color: var(--cv-accent-soft); color: var(--cv-ink); }
.cv-tile__icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2.25rem; height: 2.25rem; margin-bottom: var(--cv-sp-2);
  border-radius: var(--cv-r-md);
  background-color: var(--cv-accent-soft);
  color: var(--cv-accent-text);
  font-size: 1rem;
}
.cv-tile:hover .cv-tile__icon { background-color: var(--cv-surface); }
.cv-tile__name { font-weight: var(--cv-weight-semibold); font-size: var(--cv-text-sm); }
.cv-tile__meta { font-size: var(--cv-text-2xs); color: var(--cv-ink-3); font-variant-numeric: tabular-nums; }
