/* The pointer position that drives the lattice reveal, declared so it can be
   TRANSITIONED. A custom property is an untyped token by default and untyped
   tokens cannot be interpolated -- the browser would jump straight to each new
   value, so a transition on it would silently do nothing. Registering a syntax
   makes it a real length that animates.

   initial-value matches the off-screen default in the .page-canvas rule, so a
   page that loads before the script runs starts with nothing revealed rather
   than with a hole animating in from the origin.

   Where @property is unsupported the registration is ignored, the transition
   below has nothing to interpolate, and the reveal simply tracks the pointer
   exactly. That is the pre-existing behaviour, so the failure mode is "no
   trail", not "no reveal". */
@property --vx-cursor-x {
    syntax: "<length>";
    inherits: false;
    initial-value: -9999px;
}

@property --vx-cursor-y {
    syntax: "<length>";
    inherits: false;
    initial-value: -9999px;
}

/* ==========================================================================
   BITSoM Vertex
   Tokens first, then the components that were previously inlined per-template.
   ========================================================================== */

:root {
    /* ----------------------------------------------------------------
       Vertex brand palette. Values are fixed by the 2026 brand
       guidelines -- see docs/superpowers/specs/2026-09-10-vertex-ui-revamp-design.md
       ---------------------------------------------------------------- */

    /* Ground. A warm paper, not a cool grey. */
    --vx-canvas: #F7F5F2;
    --vx-surface: #FFFFFF;
    --vx-inset: #FAF9F6;

    /* Ink. Warm neutrals, to sit on a warm ground.
       --vx-muted is the floor for body text at 7.0:1 on canvas.
       --vx-faint is 4.4:1 and is DECORATIVE AND DISABLED STATES ONLY. */
    --vx-ink: #262626;
    --vx-ink-inverse: #FFFFFF;   /* foreground on a dark or saturated fill */
    --vx-ink-soft: #44403C;
    --vx-muted: #57534E;
    --vx-faint: #78716C;
    --vx-line: #E7E3DC;
    --vx-line-strong: #D6D1C7;

    /* Rules that DIVIDE rather than enclose. --vx-line is a border ink: at
       1.18:1 on canvas it is meant to disappear into the surface it edges,
       which is right for a card and wrong for a section rule, where it read
       as a smudge. This is 1.98:1 -- present enough to separate two blocks
       at a glance, still well short of reading as a heavy keyline. */
    --vx-rule-ink: #B8B0A0;

    /* The orange ramp, darkest to lightest, verbatim from the guidelines. */
    --vx-o-1: #9E4100;
    --vx-o-2: #D55E0A;
    --vx-o-3: #E56D18;
    --vx-o-4: #F57E2A;
    --vx-o-5: #FF9951;
    --vx-o-6: #FFB581;
    --vx-o-7: #FFE1CB;

    /* Roles. Orange is action; red is urgency; navy is structure.
       This is the brand's own assignment and it is why the primary
       button is orange rather than the red it used to be.

       Orange splits into two tokens, because one orange cannot do both jobs.

       --vx-accent is the brand orange, #F57E2A, exactly as the palette page
       names it. It is for NON-TEXT accents only: a panel edge, a stepper
       node, the hero glow. Nothing legible sits on it.

       --vx-action is the button fill, and it is one step darker. The deck
       sets white type on its orange buttons (guidelines p29, sampled at
       #E05F02) and white on #F57E2A is 2.66:1 -- below AA and below the 3:1
       non-text floor too, so the brand orange cannot carry white type at
       all. --vx-o-2 #D55E0A is the nearest named ramp step to the deck's own
       button colour and takes white at 3.86:1. Hover goes DARKER rather than
       lighter, to --vx-o-1 (6.54:1), so the contrast improves under the
       cursor instead of degrading.

       Caveat, recorded so nobody has to rediscover it: 3.86:1 clears WCAG
       AA for large text (>=18.66px bold) and the 3:1 non-text floor, but not
       the 4.5:1 required for normal-size text. .btn-lg labels qualify; a
       default .btn at 16px does not. Moving --vx-action to --vx-o-1 #9E4100
       would clear AA at every size, at the cost of a noticeably browner,
       heavier button. That is a brand call, not a code one. */
    --vx-accent: var(--vx-o-4);       /* #F57E2A -- non-text accents ONLY */
    --vx-action: var(--vx-o-2);       /* #D55E0A -- button fill, takes white */
    --vx-action-hover: var(--vx-o-1);
    --vx-action-ink: var(--vx-ink-inverse);
    --vx-action-text: var(--vx-o-1);  /* orange as text, on white or canvas */

    --vx-structure: #252A61;
    --vx-structure-hover: #1B1F47;

    --vx-urgent: #B93028;
    --vx-urgent-hover: #9A2820;

    /* Approve is the only thing allowed to use green. */
    --vx-positive: #198754;
    --vx-positive-hover: #146C43;

    /* Status tints. Ratios verified in test_design_system.py. */
    --vx-tint-neutral: #EFEBE4;
    --vx-tint-neutral-ink: #57534E;
    --vx-tint-live: #FFE1CB;
    --vx-tint-live-ink: #9E4100;
    --vx-tint-good: #D1E7DD;
    --vx-tint-good-ink: #0F5132;
    --vx-tint-bad: #FADEDC;
    --vx-tint-bad-ink: #8E2019;
    --vx-tint-info: #E6E8F2;
    --vx-tint-info-ink: #252A61;

    /* Rank badge medals (#1/#2/#3). Deliberately not drawn from the status
       tints above -- those already mean approved/interested/rejected/etc.,
       and a gold "#1" sitting next to a green "Validated" pill would read
       as another status rather than a team's own preference order. */
    --vx-tint-rank-1: #FCEEBB;
    --vx-tint-rank-1-ink: #6B4E00;
    --vx-tint-rank-2: #E3E5E8;
    --vx-tint-rank-2-ink: #43474D;
    --vx-tint-rank-3: #F0DDC9;
    --vx-tint-rank-3-ink: #7A4420;

    /* RGB triplets, for rgba() translucency (shadows, glows, dimmed text).
       Kept in sync with the hex tokens above by hand -- there is no
       CSS-native hex-to-rgb conversion, so a channel here must be updated
       alongside its hex value if the palette ever changes. --vx-shadow-rgb
       has no matching hex token: it is a plain black, used for shadow only. */
    --vx-ink-rgb: 38, 38, 38;
    --vx-ink-inverse-rgb: 255, 255, 255;
    --vx-structure-rgb: 37, 42, 97;
    --vx-accent-rgb: 245, 126, 42;
    --vx-action-rgb: 213, 94, 10;
    --vx-urgent-rgb: 185, 48, 40;
    --vx-shadow-rgb: 0, 0, 0;

    /* Type */
    --vx-font-display: "Manrope", system-ui, -apple-system, "Segoe UI", sans-serif;
    --vx-font-body: "Noto Sans JP", system-ui, -apple-system, "Segoe UI", sans-serif;

    /* Square. The brand's geometry is a square lattice and a mnemonic set in
       a square frame, so panels, buttons, fields and pills are all cut square
       to match it. Kept as a token rather than deleted so the decision stays
       in one place and is reversible in one line.

       Genuinely circular marks are NOT covered by this and stay round: the
       state dots on the timeline and the dots of the background lattice
       itself. Those are points, not surfaces with corners, and the lattice
       they echo is drawn with round dots. */
    --vx-radius: 0;
    --vx-ease: 150ms ease;

    /* ----------------------------------------------------------------
       Bootstrap bridge. Bootstrap 5.3 resolves these custom properties at
       runtime, so overriding them here brands the stock components built
       on them -- body colour and background, links, the focus ring,
       .btn-primary (via --bs-primary), .text-danger and form validation
       (via --bs-danger / --bs-danger-rgb), and .text-muted / .form-text /
       placeholder text (via --bs-secondary-color) -- with no template
       edit. It does NOT brand alerts, badges, cards or tables wholesale:
       those components mostly hardcode Bootstrap's own palette rather
       than reading these variables, so most of their colour still comes
       from the classes set in base.css directly (.status-pill, .tile,
       .credentials-box, and so on).
       ---------------------------------------------------------------- */
    --bs-body-bg: var(--vx-canvas);
    --bs-body-color: var(--vx-ink);
    --bs-border-color: var(--vx-line);
    /* Bootstrap keeps a whole scale of radii and its components reach for
       different rungs -- cards and modals take -lg, the rounded-pill utility
       takes -pill. Overriding only the base one would leave rounded corners
       on exactly the components that are largest and most visible. */
    --bs-border-radius: var(--vx-radius);
    --bs-border-radius-sm: var(--vx-radius);
    --bs-border-radius-lg: var(--vx-radius);
    --bs-border-radius-xl: var(--vx-radius);
    --bs-border-radius-xxl: var(--vx-radius);
    --bs-border-radius-pill: var(--vx-radius);
    --bs-primary: var(--vx-action);
    --bs-primary-rgb: 245, 126, 42;
    --bs-link-color: var(--vx-action-text);
    --bs-link-hover-color: var(--vx-o-2);
    --bs-focus-ring-color: rgba(var(--vx-structure-rgb), 0.25);
    --bs-font-sans-serif: var(--vx-font-body);

    /* .text-danger resolves color as rgba(var(--bs-danger-rgb), ...), not
       var(--bs-danger) directly -- the -rgb companion is the one that
       actually reaches it. Bootstrap's own #dc3545 is 4.16:1 on
       --vx-canvas, below AA; --vx-urgent is the brand's red and is the one
       the contrast tests assert (5.97:1 on --vx-canvas, measured). */
    --bs-danger: var(--vx-urgent);
    --bs-danger-rgb: 185, 48, 40;

    /* .text-muted, .form-text and placeholder text all resolve through
       --bs-secondary-color, not --bs-body-color-rgb -- Bootstrap ships
       --bs-secondary-color as a literal rgba(), not as something derived
       from --bs-body-color-rgb via var(), so overriding the latter alone
       leaves .text-muted untouched. --vx-muted already clears AA on both
       --vx-canvas and --vx-surface (see ContrastTests), so it is used
       solid rather than re-deriving an alpha. */
    --bs-secondary-color: var(--vx-muted);
}

/* Bootstrap assumes a primary button carries white text, and so does the
   brand -- but not on the brand's own orange. See --vx-action above: the fill
   is one ramp step darker than #F57E2A precisely so white is usable. */
.btn-primary {
    --bs-btn-color: var(--vx-action-ink);
    --bs-btn-hover-color: var(--vx-action-ink);
    --bs-btn-active-color: var(--vx-action-ink);
    --bs-btn-bg: var(--vx-action);
    --bs-btn-border-color: var(--vx-action);
    --bs-btn-hover-bg: var(--vx-action-hover);
    --bs-btn-hover-border-color: var(--vx-action-hover);
    --bs-btn-active-bg: var(--vx-action-hover);
    --bs-btn-active-border-color: var(--vx-action-hover);
}

html,
body {
    margin: 0;
    padding: 0;
    font-family: var(--vx-font-body);
    background-color: var(--vx-canvas);
    color: var(--vx-ink);
    -webkit-font-smoothing: antialiased;
}

/* Manrope carries every heading and every piece of interface furniture.
   Noto Sans JP carries prose and form values. */
h1, h2, h3, h4, h5, h6,
.navbar-brand,
.btn,
.vx-eyebrow,
.vx-title,
.vx-subtitle {
    font-family: var(--vx-font-display);
}

/* Ink, not navy. Navy is assigned to structure -- the navbar, completed
   stepper nodes, focus rings -- and the brand sets headings in black with
   orange as the accent. Changing this changes every heading in the app.

   A heading inside a context that paints its own white foreground (see
   ".text-white h1, ... h6" below) needs to escape this rule. That override
   works by OUTRANKING this rule on specificity, not by indirecting through
   a custom property: an earlier version of this fix tried
   "--vx-heading-color: inherit" on the coloured context and
   "color: var(--vx-heading-color)" here, on the theory that the ancestor's
   "inherit" would substitute into the child's `color`. It does not.
   `--prop: inherit` is the CSS-wide keyword applied to the custom property
   itself -- it means "take the parent's computed value of --prop", which
   custom properties already do by default, so the declaration is a no-op.
   Substitution happens after the keyword is already consumed; it can never
   make `color` resolve to the ancestor's. Measured against the live page,
   that version still rendered #262626 ink on #252A61 navy (1.14:1) -- the
   original bug, unchanged. Do not reintroduce the indirection. */
h1, h2, h3, h4, h5, h6 {
    color: var(--vx-ink);
    letter-spacing: -0.015em;
}

/* A heading inside a context that paints its own foreground -- currently
   just the two confirmation-modal headers, both `.text-white` -- must take
   that foreground, not ink. This sets `color` directly, at a specificity
   (0-1-1) that beats the bare h1-h6 rule above (0-0-1) regardless of source
   order, so `color: inherit` here really does resolve at computed-value
   time to the ancestor's actual colour (white, from .text-white's
   `!important`) -- unlike the custom-property indirection described above,
   this is `inherit` applied directly to `color`, which is what "inherit"
   is for. .background-primary itself sets no text colour -- .text-white is
   what actually carries white on both modal headers -- so this keys off
   .text-white rather than .background-primary. If a future coloured-ground
   context wraps a heading without using .text-white, extend the selector
   list below rather than reaching for another custom-property trick. */
.text-white h1, .text-white h2, .text-white h3,
.text-white h4, .text-white h5, .text-white h6 {
    color: inherit;
}

/* Despite the name, this is the app's heading-colour utility -- 36 of its 38
   uses across 13 templates are on page/section headings, not on arbitrary
   "primary" text. It resolves to ink, not navy: navy (--vx-structure) is
   reserved for structure -- the navbar, completed stepper nodes, focus rings
   -- and the brand sets headings in near-black with orange as the accent.
   See the identical rationale on the bare h1-h6 rule above. */
.color-primary {
    color: var(--vx-ink);
}

.color-secondary {
    color: var(--vx-urgent);
}

/* Orange as text, which means the dark ramp step: neither --vx-accent
   (2.66:1 on white) nor --vx-action (3.86:1) is legible at body size. */
.color-tertiary {
    color: var(--vx-action-text);
}

.background-primary {
    background-color: var(--vx-structure);
}

.background-secondary {
    background-color: var(--vx-urgent);
}

.background-tertiary {
    background-color: var(--vx-action);
}

/* The page shell every dashboard and detail view sits on.

   At rest it is the dot lattice alone. The edges between those dots exist as
   a second, complete layer that is masked out everywhere except a soft circle
   around the pointer, so moving the cursor draws the connections in as it
   passes and lets them fade again behind it. It is the deck's "from
   possibility to progress" idea (guidelines p23) made to follow the reader
   instead of being frozen into one left-to-right sweep across the artboard.

   How the masking works, since there is no mask here: CSS cannot apply a mask
   to ONE background layer, so this composites the layers instead. Top to
   bottom they are

       1. the dots        -- painted last-over-all, so they always show
       2. a radial gradient of the CANVAS COLOUR, opaque everywhere except a
          hole at the pointer
       3. the edges       -- visible only through that hole
       4. background-color, the same canvas colour

   Layer 2 is doing the work of a mask by simply being the page colour: it
   hides the edges under something indistinguishable from the ground, and the
   dots sit above it so they are never affected. This needs no extra element,
   no pseudo-element and no z-index, which matters because .page-canvas wraps
   every dialog on the site and giving it a stacking context to hold an
   overlay would put one around every modal.

   background-attachment: fixed on every layer is load-bearing twice over. It
   pins the positioning area to the VIEWPORT, which is what makes `cover` fit
   the artwork to the screen exactly once at its own aspect ratio, and it puts
   the gradient in the same coordinate space as the pointer, so the hole can be
   placed straight from clientX/clientY with no scroll arithmetic.

   The two SVGs share a viewBox and a size, so their lattices line up and the
   revealed edges land exactly between the dots.

   The pattern sits behind cards, tiles and panels, all of which paint an
   opaque --vx-surface, so it shows in the gutters between content. Nothing
   legible is ever set on it. */
.page-canvas {
    background-color: var(--vx-canvas);
    background-image: url("basesite/img/vertex/pattern-dots.d6025f02dfd9.svg");
    background-repeat: no-repeat;
    background-attachment: fixed;
    background-position: center;
    background-size: cover;
    min-height: 100vh;
}

/* The reveal is an enhancement for pointing devices only. A touch screen has
   no hovering cursor to follow, so there it would either never fire or, worse,
   snap to wherever a finger last landed; phones also have the least power to
   spare for repainting a full-viewport background. They keep the dots. */
@media (hover: hover) and (pointer: fine) {
    .page-canvas {
        /* Parked off-screen so nothing is revealed until the pointer actually
           moves. cursor_reveal.js overwrites these; if it never runs, or
           declines to run under prefers-reduced-motion, the gradient stays
           opaque everywhere and this renders exactly as the rule above. */
        --vx-cursor-x: -9999px;
        --vx-cursor-y: -9999px;
        --vx-reveal-radius: 210px;

        /* The reveal trails the cursor by 100ms rather than being welded to
           it. The script still reports the pointer once per frame; this makes
           the painted hole chase that report instead of matching it, which
           reads as the lattice lighting up around the cursor rather than
           being dragged by it.

           linear, not an ease: the pointer position is re-set every frame, so
           each transition is restarted long before it finishes and only ever
           plays its first few milliseconds. An ease-in curve would spend
           exactly that window accelerating, and the trail would lag much
           further behind than 100ms suggests. */
        transition:
            --vx-cursor-x 100ms linear,
            --vx-cursor-y 100ms linear;

        background-image:
            url("basesite/img/vertex/pattern-dots.d6025f02dfd9.svg"),
            radial-gradient(circle var(--vx-reveal-radius) at var(--vx-cursor-x) var(--vx-cursor-y),
                transparent 0%,
                transparent 38%,
                var(--vx-canvas) 100%),
            url("basesite/img/vertex/pattern-edges.6fdfcf32e364.svg");
        background-repeat: no-repeat, no-repeat, no-repeat;
        background-attachment: fixed, fixed, fixed;
        background-position: center, center, center;
        background-size: cover, cover, cover;
    }
}

/* --------------------------------------------------------------------------
   Rules
   The deck rules its layouts with full-bleed hairlines rather than boxing
   things in -- see the type pages (guidelines p18-p20), where a single
   horizontal rule and three verticals do all the dividing work. The weight
   weight is the point: heavy enough to separate two blocks at a glance,
   light enough that it never reads as a border around anything.
   -------------------------------------------------------------------------- */

/* A section boundary. Applied between siblings rather than on each one, so a
   run of sections is separated without a stray rule above the first or below
   the last. */
.vx-ruled > * + * {
    border-top: 2px solid var(--vx-rule-ink);
    padding-top: 1.75rem;
    margin-top: 1.75rem;
}

/* A standalone divider, for the places a parent cannot own the run. */
.vx-rule {
    border: 0;
    border-top: 2px solid var(--vx-rule-ink);
    opacity: 1;
    margin: 1.75rem 0;
}

/* The vertical half of the same idea: a hairline down the gutter between two
   columns. Suppressed below lg, where the columns stack and a vertical rule
   would sit across the flow rather than along it. */
@media (min-width: 992px) {
    .vx-ruled-col {
        border-left: 2px solid var(--vx-rule-ink);
    }
}

/* A heading with its own hairline running out to the edge of the container:
   the deck's "Light / Medium / Bold" treatment, where the rule starts at the
   label and continues past it. */
.vx-ruled-heading {
    display: flex;
    align-items: center;
    gap: 0.75rem;
}

/* A count badge sitting in a ruled heading is part of the label, so the rule
   starts after it rather than between it and its noun. */
.vx-ruled-heading > .badge {
    flex: 0 0 auto;
}

.vx-ruled-heading::after {
    content: "";
    flex: 1 1 auto;
    height: 2px;
    background-color: var(--vx-rule-ink);
}

/* --------------------------------------------------------------------------
   Buttons
   Four ad-hoc primaries became two: orange for the page's main action, navy
   for moderator actions that open a panel rather than commit to anything.
   -------------------------------------------------------------------------- */

/* The page's main forward action. Orange, per the brand's role assignment.
   Dark ink is mandatory here -- see the note on --vx-action-ink. */
.btn-brand,
.btn-color-secondary {
    background-color: var(--vx-action);
    border-color: var(--vx-action);
    color: var(--vx-action-ink);
}

.btn-brand:hover,
.btn-brand:focus,
.btn-brand:active,
.btn-color-secondary:hover,
.btn-color-secondary:focus,
.btn-color-secondary:active {
    background-color: var(--vx-action-hover);
    border-color: var(--vx-action-hover);
    color: var(--vx-action-ink);
}

.btn-outline-brand {
    background-color: transparent;
    border-color: var(--vx-action);
    color: var(--vx-action-text);
}

.btn-outline-brand:hover,
.btn-outline-brand:focus,
.btn-outline-brand:active {
    background-color: var(--vx-action);
    border-color: var(--vx-action);
    color: var(--vx-action-ink);
}

.btn {
    font-family: var(--vx-font-display);
    font-weight: 600;
    border-radius: var(--vx-radius);
}

.btn-navy {
    background-color: var(--vx-structure);
    border-color: var(--vx-structure);
    color: var(--vx-ink-inverse);
}

.btn-navy:hover,
.btn-navy:focus,
.btn-navy:active {
    background-color: var(--vx-structure-hover);
    border-color: var(--vx-structure-hover);
    color: var(--vx-ink-inverse);
}

/* The two decision verbs. Used by both moderator flows, nowhere else. */
.btn-approve {
    background-color: var(--vx-positive);
    border-color: var(--vx-positive);
    color: var(--vx-ink-inverse);
}

.btn-approve:hover,
.btn-approve:focus,
.btn-approve:active {
    background-color: var(--vx-positive-hover);
    border-color: var(--vx-positive-hover);
    color: var(--vx-ink-inverse);
}

.btn-reject {
    background-color: var(--vx-urgent);
    border-color: var(--vx-urgent);
    color: var(--vx-ink-inverse);
}

.btn-reject:hover,
.btn-reject:focus,
.btn-reject:active {
    background-color: var(--vx-urgent-hover);
    border-color: var(--vx-urgent-hover);
    color: var(--vx-ink-inverse);
}

/* --------------------------------------------------------------------------
   Tiles
   A whole tile is one target. The anchor is stretched over the card, and
   anything genuinely interactive inside it is lifted back above.
   -------------------------------------------------------------------------- */

.tile {
    position: relative;
    background-color: var(--vx-surface);
    border: 1px solid var(--vx-line);
    border-radius: var(--vx-radius);
    box-shadow: none;
    transition: transform var(--vx-ease), box-shadow var(--vx-ease), border-color var(--vx-ease);
}

/* Hover draws an inner rule in the brand orange, inset just far enough to
   read as a second edge inside the card rather than a thickening of the
   border. It is an inset box-shadow rather than a real border or an outline
   so it costs no layout: a border would change the box's size on hover and
   shift everything inside it by a pixel.

   --vx-accent, not --vx-action: this is a non-text mark, so it takes the
   brand orange itself rather than the darker fill that exists only to carry
   white type.

   The outer border stays neutral. Colouring both it and the ring gives a
   2px orange outline that shouts rather than pops; keeping the edge quiet
   is what lets the orange read as a line INSIDE the card. Rendered all
   three ways side by side to pick this one.

   Applied to the two things that are actually clickable. Static panels
   (.card on the detail pages) deliberately do NOT get this -- a hover
   response on something that cannot be clicked reads as an invitation and
   is a usability bug, not a flourish. */
.tile:hover,
.tile:focus-within {
    transform: translateY(-2px);
    box-shadow:
        inset 0 0 0 1px var(--vx-accent),
        0 .5rem 1rem rgba(var(--vx-ink-rgb), .08);
    border-color: var(--vx-line-strong);
}

.tile-link::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
}

.tile-link:focus-visible::after {
    outline: 2px solid var(--vx-structure);
    outline-offset: 2px;
}

/* Buttons, links and pills inside a tile sit above the stretched anchor. */
.tile .tile-actions,
.tile a:not(.tile-link),
.tile button,
.tile form,
.tile .status-pill {
    position: relative;
    z-index: 1;
}

/* Inset rows (applications inside a card) get the same affordance, flatter. */
.resource-row {
    background-color: var(--vx-inset);
    color: inherit;
    transition: background-color var(--vx-ease), transform var(--vx-ease);
}

.resource-row:hover,
.resource-row:focus-visible {
    background-color: var(--vx-tint-neutral);
    color: inherit;
    transform: translateX(2px);
    box-shadow: inset 0 0 0 1px var(--vx-accent);
}

/* --------------------------------------------------------------------------
   Status pills
   Colour encodes how far along the decision is, not which word it is:
   grey = in flight, orange = live but undecided, green = settled yes,
   red = settled no. A pill carrying a reason says so with a dotted underline,
   so you can see where the explanation is before you go hunting for it.
   -------------------------------------------------------------------------- */

.status-pill {
    display: inline-block;
    padding: 0.25em 0.7em;
    border-radius: var(--vx-radius);
    font-size: 0.75rem;
    font-weight: 500;
    line-height: 1.4;
    white-space: nowrap;
    background-color: var(--vx-tint-neutral);
    color: var(--vx-tint-neutral-ink);
}

.status-pill--pending {
    background-color: var(--vx-tint-neutral);
    color: var(--vx-tint-neutral-ink);
}

.status-pill--live {
    background-color: var(--vx-tint-live);
    color: var(--vx-tint-live-ink);
}

.status-pill--good {
    background-color: var(--vx-tint-good);
    color: var(--vx-tint-good-ink);
}

.status-pill--bad {
    background-color: var(--vx-tint-bad);
    color: var(--vx-tint-bad-ink);
}

.status-pill--has-reason {
    cursor: help;
    text-decoration: underline dotted currentColor;
    text-underline-offset: 0.2em;
}

/* --------------------------------------------------------------------------
   Stat tiles
   The cohort overview's headline numbers -- each one links through to the
   relevant list, pre-filtered, so it also carries the .tile class for the
   standard hover/lift affordance.
   -------------------------------------------------------------------------- */

.stat-tile {
    background-color: var(--vx-surface);
    border: 1px solid var(--vx-line);
    border-radius: var(--vx-radius);
    padding: 0.85rem 1rem;
}

.stat-tile .tile-link {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    color: inherit;
}

.stat-tile__value {
    font-size: 1.6rem;
    font-weight: 700;
    line-height: 1.1;
    color: var(--vx-ink);
}

.stat-tile__label {
    font-size: 0.75rem;
    color: var(--vx-muted);
}

/* Neutral counter next to a section heading -- not a status. */
.count-badge {
    background-color: var(--vx-tint-neutral);
    color: var(--vx-muted);
    font-weight: 500;
}

.meta-badge {
    background-color: var(--vx-tint-neutral);
    color: var(--vx-ink-soft);
}

/* Ranks below #3 keep the plain .meta-badge look above -- only the top
   three preferences get a medal color. */
.rank-badge--1 {
    background-color: var(--vx-tint-rank-1);
    color: var(--vx-tint-rank-1-ink);
}

.rank-badge--2 {
    background-color: var(--vx-tint-rank-2);
    color: var(--vx-tint-rank-2-ink);
}

.rank-badge--3 {
    background-color: var(--vx-tint-rank-3);
    color: var(--vx-tint-rank-3-ink);
}

/* Flags an application awaiting a moderator's Approve/Reject call. Same
   "attention without alarm" tint as .credentials-box -- not a status,
   so it stays visually distinct from .status-pill. */
.needs-decision-badge {
    background-color: var(--vx-tint-live);
    color: var(--vx-tint-live-ink);
    font-weight: 500;
}

/* The one-time credentials callout. Attention without alarm -- this is a
   "write this down" box, not an error. */
.credentials-box {
    background-color: var(--vx-tint-live);
    border: 1px solid var(--vx-o-2);
    border-radius: var(--vx-radius);
    padding: 1.5rem;
}

/* --------------------------------------------------------------------------
   Decision panel
   Shared by "Advance status" and "Review project". Orange earns its place as
   a single edge marking the thing awaiting your call -- not as a full border.
   -------------------------------------------------------------------------- */

.decision-panel {
    background-color: var(--vx-surface);
    border-radius: var(--vx-radius);
    border-left: 3px solid var(--vx-accent);
    box-shadow: 0 .125rem .25rem rgba(var(--vx-shadow-rgb), .075);
}

/* --------------------------------------------------------------------------
   Cohort timeline
   A segmented meter over a disclosure. The meter answers "how far along are
   we" without being read; the disclosure answers "what is the whole
   schedule" for whoever actually needs the dates.

   Five nodes in fixed order, and two adjacent ones can both read "current"
   at once -- Startup Onboarding and Student Team Registration
   are allowed open together. Nothing here counts "stage N of 5" or picks a
   single current stage, so that case needs no special handling: two segments
   simply both light up.
   -------------------------------------------------------------------------- */

.stage-stepper {
    margin-bottom: 1.5rem;
}

.stage-meter {
    display: flex;
    gap: 3px;
    margin-bottom: 0.75rem;
}

.stage-meter__seg {
    flex: 1 1 0;
    height: 6px;
    border-radius: var(--vx-radius);
    background-color: var(--vx-line);
}

.stage-meter__seg.stage-step--completed {
    background-color: var(--vx-structure);
}

/* The live segment takes the brand orange itself: it is a non-text mark, so
   it is one of the few places --vx-accent rather than --vx-action applies. */
.stage-meter__seg.stage-step--current {
    background-color: var(--vx-accent);
}

.stage-legend {
    display: flex;
    gap: 3px;
}

.stage-legend__cell {
    flex: 1 1 0;
    padding-right: 0.5rem;
}

.stage-legend__label {
    font-family: var(--vx-font-display);
    font-size: 0.8rem;
    font-weight: 500;
    line-height: 1.35;
    color: var(--vx-ink-soft);
}

.stage-legend__cell.stage-step--current .stage-legend__label {
    font-weight: 700;
    color: var(--vx-ink);
}

.stage-legend__cell.stage-step--upcoming .stage-legend__label {
    color: var(--vx-muted);
}

.stage-legend__date {
    font-family: var(--vx-font-display);
    font-size: 0.66rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--vx-muted);
    margin-top: 0.15rem;
}

/* --- The disclosure ---------------------------------------------------- */

.stage-detail {
    margin-top: 0.85rem;
}

.stage-detail__toggle {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    font-family: var(--vx-font-display);
    font-size: 0.78rem;
    font-weight: 600;
    color: var(--vx-action-text);
    cursor: pointer;
    list-style: none;
}

/* Both are needed: ::marker is the standard, ::-webkit-details-marker is
   what older WebKit still uses. Without them the chevron sits beside a
   stray triangle. */
.stage-detail__toggle::marker,
.stage-detail__toggle::-webkit-details-marker {
    display: none;
    content: "";
}

.stage-detail__toggle:hover {
    color: var(--vx-o-2);
}

.stage-detail__chevron {
    font-size: 0.7rem;
    transition: transform var(--vx-ease);
}

.stage-detail[open] .stage-detail__chevron {
    transform: rotate(90deg);
}

.stage-detail__list {
    list-style: none;
    margin: 0.6rem 0 0;
    padding: 0;
    border-top: 1px solid var(--vx-line);
}

.stage-detail__row {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.5rem 0;
    border-bottom: 1px solid var(--vx-line);
    font-size: 0.85rem;
}

.stage-detail__dot {
    flex: 0 0 auto;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background-color: var(--vx-line-strong);
}

.stage-detail__row.stage-step--completed .stage-detail__dot {
    background-color: var(--vx-structure);
}

.stage-detail__row.stage-step--current .stage-detail__dot {
    background-color: var(--vx-accent);
}

.stage-detail__row.stage-step--current .stage-detail__name {
    font-weight: 700;
}

.stage-detail__name {
    font-family: var(--vx-font-display);
    color: var(--vx-ink);
}

/* The date never splits across lines, so a narrow status column folds at
   the verb rather than mid-date. */
.stage-detail__when {
    white-space: nowrap;
}

/* Pushed to the far end, and allowed to keep its own line on a narrow
   screen rather than crushing the stage name. */
.stage-detail__status {
    margin-left: auto;
    padding-left: 0.75rem;
    text-align: right;
    font-size: 0.78rem;
    color: var(--vx-muted);
    white-space: nowrap;
}

@media (max-width: 767.98px) {
    /* Five labels cannot share a phone's width: the old stepper wrapped them
       into a grid, which left its connector lines running off to nowhere.
       The meter keeps the full picture, and the legend narrows to whatever
       is actually live -- everything else is one tap away below. When no
       stage is open the legend has no visible children and collapses to
       nothing, which is why its spacing lives on .stage-detail instead. */
    .stage-legend {
        display: block;
    }

    .stage-legend__cell {
        display: none;
        padding-right: 0;
    }

    .stage-legend__cell.stage-step--current {
        display: block;
    }

    /* Columns, not wrapping. Letting the row wrap made each one a different
       shape: a long stage name pushed its date onto a second line while a
       short one kept it inline, so no two dates lined up and the list read
       as ragged. A grid with a fixed final track puts every date in the same
       column whatever the name does, so the rows stay uniform and the dates
       can be scanned down the edge.

       The name track is minmax(0, 1fr) rather than 1fr because a grid item's
       default min-width is auto, which refuses to shrink below its longest
       word and would push the date track off the screen. */
    .stage-detail__row {
        display: grid;
        grid-template-columns: 8px minmax(0, 1fr) 7.5rem;
        gap: 0.6rem;
        align-items: center;
    }

    .stage-detail__status {
        margin-left: 0;
        padding-left: 0;
        text-align: right;
        white-space: normal;
    }
}

@media (prefers-reduced-motion: reduce) {

    .tile,
    .resource-row {
        transition: none;
    }

    .tile:hover,
    .tile:focus-within,
    .resource-row:hover {
        transform: none;
    }
}

/* --------------------------------------------------------------------------
   Utility vocabulary
   These exist so that colour and type live here rather than in a style=
   attribute. Every one of them replaces a shape that was repeated by hand
   across the templates -- --vx-muted alone was written out 25 times.
   -------------------------------------------------------------------------- */

/* Brandon Grotesque is a licensed face we do not ship. Manrope in caps with
   tracking is the stand-in for labels, metadata and eyebrows. */
.vx-eyebrow {
    font-family: var(--vx-font-display);
    font-weight: 600;
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--vx-muted);
}

.vx-title {
    font-family: var(--vx-font-display);
    font-weight: 700;
    color: var(--vx-ink);
    letter-spacing: -0.015em;
    margin-bottom: 0.25rem;
}

/* The guidelines set the subheading at half the heading's size. */
.vx-subtitle {
    font-family: var(--vx-font-display);
    font-weight: 400;
    font-size: 0.95rem;
    color: var(--vx-muted);
}

.vx-meta {
    font-size: 0.875rem;
    color: var(--vx-muted);
}

.vx-meta-sm {
    font-size: 0.8rem;
    color: var(--vx-muted);
}

/* Tertiary text. Uses --vx-muted rather than --vx-faint: at this size
   --vx-faint's 4.4:1 is below AA. */
.vx-hint {
    font-size: 0.75rem;
    color: var(--vx-muted);
}

.vx-label {
    font-family: var(--vx-font-display);
    font-weight: 600;
    font-size: 0.875rem;
    color: var(--vx-ink-soft);
}

.vx-link-accent {
    color: var(--vx-action-text);
    text-decoration: none;
}

.vx-link-accent:hover,
.vx-link-accent:focus {
    color: var(--vx-o-2);
    text-decoration: underline;
}

.vx-avatar {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    border-radius: var(--vx-radius);
    background-color: var(--vx-structure);
    color: var(--vx-ink-inverse);
    font-family: var(--vx-font-display);
    font-weight: 600;
    font-size: 0.875rem;
    flex-shrink: 0;
}

.vx-auth-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 3rem;
    height: 3rem;
    border-radius: var(--vx-radius);
    background-color: var(--vx-action);
    color: var(--vx-action-ink);
}

/* The two-panel auth shell.

   min-height rather than height, and no overflow clipping. The previous
   version pinned the shell to exactly 100vh and hid the overflow, which on a
   phone -- where the hero panel is replaced by a brand band stacked ABOVE
   the form -- pushed the form down by the height of that band and then cut
   off whatever fell past the fold. A form that grows (validation errors, a
   soft keyboard) has to be able to scroll. */
.vx-auth-shell,
.vx-auth-shell > .row {
    min-height: 100vh;
}

/* The row is display:flex with align-items:stretch, so both columns take the
   row's full height on their own. The height therefore lives HERE, on the
   shell and the row, and not as an h-100 chain in the markup: h-100 is
   height:100%, which needs an ancestor with a definite height to resolve
   against, and min-height does not provide one. A row of h-100 columns
   inside a min-height shell silently collapses to content height. */

/* The phone's stand-in for the hero panel. The navy ground and the reversed
   lockup keep the brand introducing the page even when there is no room for
   the full panel. */
.vx-auth-mobile-brand {
    background-color: var(--vx-structure);
    padding: 1.75rem 1rem;
}

.vx-hero {
    position: relative;
    background-color: var(--vx-structure);
    overflow: hidden;
}

/* The hero's supporting line: white at reduced opacity, dimmer than the
   lockup above it so it reads as secondary copy on the navy field. */
.vx-hero-lede {
    font-size: 1.05rem;
    color: rgba(var(--vx-ink-inverse-rgb), 0.62);
}

/* The login hero. The convergence burst sits behind the lockup, with the
   brand's soft orange gradient warming two corners.

   The burst is a MEDALLION, not a texture: pattern-rays.svg is a square tile
   whose rays converge on its exact centre, so it is drawn once, unrepeated,
   centred on the panel, at a size tied to the panel's smaller dimension. It
   must not use background-size: cover -- cover crops the tile off-centre at
   most aspect ratios, which throws the convergence point away from the
   lockup and turns a deliberate pattern into random diagonal marks.

   Positioned at 50% 46% rather than dead centre: the hero centres the
   lockup and its lede as a group, which puts the lockup itself slightly
   above the panel's midline. The rays should converge on the mark, not on
   the gap below it. */
.vx-hero__pattern {
    position: absolute;
    inset: 0;
    background-image: url("basesite/img/vertex/pattern-rays.7a076a84eecc.svg");
    background-repeat: no-repeat;
    background-position: 50% 46%;
    background-size: min(100%, 95vh);
    opacity: 0.85;
    pointer-events: none;
}

.vx-hero__glow {
    position: absolute;
    inset: 0;
    background-image:
        radial-gradient(circle at 78% 76%, rgba(var(--vx-accent-rgb), 0.28) 0%, transparent 55%),
        radial-gradient(circle at 22% 24%, rgba(var(--vx-urgent-rgb), 0.16) 0%, transparent 50%);
    pointer-events: none;
}

.vx-hero__content {
    position: relative;
    z-index: 1;
}

.bg-vx-inset {
    background-color: var(--vx-inset);
}

/* Form fields, so the per-template inline field styling can go. */
.form-control,
.form-select {
    background-color: var(--vx-inset);
    border-color: var(--vx-line-strong);
    color: var(--vx-ink);
}

.form-control:focus,
.form-select:focus {
    background-color: var(--vx-surface);
    border-color: var(--vx-structure);
    box-shadow: 0 0 0 0.2rem rgba(var(--vx-structure-rgb), 0.15);
}

.vx-navbar {
    border-bottom: 1px solid var(--vx-line);
}

.vx-navbar .navbar-brand {
    color: var(--vx-ink);
    letter-spacing: -0.01em;
}

/* Bootstrap centres a navbar's middle child with space-between, which is what
   left the single section link stranded in the middle of the bar looking like
   a stray action. Sections belong next to the identity that owns them, so the
   row packs from the left and the account controls are pushed to the far end
   on their own. */
.vx-navbar > .container-fluid {
    justify-content: flex-start;
    gap: 2rem;
}

.vx-navbar__controls {
    margin-left: auto;
}

.vx-nav {
    display: flex;
    align-items: center;
    gap: 1.5rem;
}

/* The brand site sets its own navigation this way (guidelines p29): Manrope,
   uppercase and tracked, quiet until it is the section you are in, with the
   accent carrying the active state as a rule underneath. Using weight or
   colour alone for "you are here" would either shift the text as you navigate
   or lean on colour by itself; a rule does neither. */
.vx-nav-link {
    position: relative;
    font-family: var(--vx-font-display);
    font-weight: 600;
    font-size: 0.78rem;
    text-transform: uppercase;
    letter-spacing: 0.07em;
    color: var(--vx-muted);
    text-decoration: none;
    padding: 0.35rem 0;
    transition: color var(--vx-ease);
    white-space: nowrap;
}

.vx-nav-link:hover,
.vx-nav-link:focus-visible {
    color: var(--vx-ink);
}

.vx-nav-link--active {
    color: var(--vx-ink);
}

.vx-nav-link--active::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: -2px;
    height: 2px;
    background-color: var(--vx-accent);
}

@media (max-width: 991.98px) {
    /* Five things do not fit across a phone. The sections drop to their own
       full-width row under the brand, which keeps them visible rather than
       hiding them behind a toggle -- there are only ever two. order puts them
       below the account controls in the flow while the auto margin keeps
       those controls up on the first row. */
    .vx-navbar > .container-fluid {
        gap: 0.75rem;
    }

    .vx-nav {
        order: 3;
        width: 100%;
        gap: 1.5rem;
        margin-top: 0.5rem;
        padding-top: 0.6rem;
        border-top: 1px solid var(--vx-line);
    }
}

/* ==========================================================================
   The team x startup matrix (Applications tab)
   ==========================================================================

   One cohort's whole matching picture: teams down, startups across. The
   colour carries two different facts, so it is deliberately two different
   kinds of fill rather than two strengths of one.

     A TINT means the TEAM wanted this. Every application a team files is,
     by definition, a preference -- so every non-empty cell is at least
     --vx-tint-live, with the team's own rank digit in --vx-tint-live-ink.
     Rank is the number, not the shade: a ramp would compete with the
     solid fills below for attention, and it is the mutual cells a
     moderator is scanning for.

     A SOLID means the STARTUP answered. --vx-action orange is mutual
     interest, --vx-structure navy is a validated match -- filled, white
     type, unmistakable against a field of pale tints. Navy rather than a
     darker orange because a decided match is structurally different from
     a live one, not merely further along.

   Rejected cells stay visible but recede to --vx-tint-neutral with a
   strike: a moderator needs to see that a pair was considered and closed,
   without it drawing the eye.

   Column headers are vertical rather than rotated 45deg. A diagonal label
   overflows its own <th> and is painted over by the next column's sticky
   background, which cannot be fixed with z-index for an arbitrary number
   of columns; vertical text stays inside its column, so sticky headers,
   ellipsis truncation and pointer hit-testing all behave. */

.vx-matrix-scroll {
    --vx-matrix-cell: 2.25rem;
    --vx-matrix-rowhead: 12.5rem;
    --vx-matrix-head: 9.5rem;
    --vx-matrix-total: 4.25rem;

    overflow: auto;
    max-height: calc(100vh - 19rem);
    min-height: 12rem;
    /* Hug the grid rather than the page: a 6-startup cohort should not sit
       in a box four times its width. max-width keeps a wide one scrolling. */
    width: max-content;
    max-width: 100%;
    background-color: var(--vx-surface);
    border: 1px solid var(--vx-line-strong);
}

.vx-matrix-scroll:focus-visible {
    outline: 2px solid var(--vx-accent);
    outline-offset: -2px;
}

.vx-matrix {
    border-collapse: separate;
    border-spacing: 0;
    table-layout: fixed;
    font-family: var(--vx-font-display);
    font-variant-numeric: tabular-nums;
}

/* --- headers --------------------------------------------------------- */

.vx-matrix thead th {
    position: sticky;
    top: 0;
    z-index: 3;
    background-color: var(--vx-inset);
    border-bottom: 1px solid var(--vx-line-strong);
}

/* Deliberately over-qualified. `.vx-matrix thead th` above is 0-1-2 (one
   class, two element names) and sets z-index: 3 on every header cell; a
   bare `.vx-matrix__corner` is 0-1-0 and loses to it, so the corner tied
   with the column headers and lost the tie on document order -- it is the
   first cell in its row, so every startup name painted over it as soon as
   the grid scrolled sideways. Do not "simplify" this selector back.
   Guarded by basesite/tests/test_browser_matrix_sticky.py. */
.vx-matrix thead .vx-matrix__corner {
    left: 0;
    z-index: 5;
    width: var(--vx-matrix-rowhead);
    min-width: var(--vx-matrix-rowhead);
    padding: 0.5rem 0.75rem;
    vertical-align: bottom;
    text-align: left;
    border-right: 1px solid var(--vx-line-strong);
}

.vx-matrix__corner-team,
.vx-matrix__corner-startup {
    display: block;
    font-size: 0.68rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--vx-muted);
}

.vx-matrix__corner-startup {
    color: var(--vx-faint);
}

.vx-matrix__corner-startup::before {
    content: "\2192\00a0";  /* arrow: startups run across */
}

.vx-matrix__colhead {
    width: var(--vx-matrix-cell);
    min-width: var(--vx-matrix-cell);
    height: var(--vx-matrix-head);
    padding: 0.35rem 0;
    vertical-align: bottom;
    text-align: center;
    font-weight: 500;
}

.vx-matrix__colhead-label {
    display: inline-block;
    writing-mode: vertical-rl;
    transform: rotate(180deg);   /* reads bottom-to-top, toward its column */
    max-height: calc(var(--vx-matrix-head) - 0.8rem);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 0.76rem;
    color: var(--vx-ink-soft);
    text-decoration: none;
}

.vx-matrix__colhead-label:hover,
.vx-matrix__colhead-label:focus-visible {
    color: var(--vx-action-text);
    text-decoration: underline;
}

.vx-matrix__rowhead {
    position: sticky;
    left: 0;
    z-index: 2;
    width: var(--vx-matrix-rowhead);
    min-width: var(--vx-matrix-rowhead);
    max-width: var(--vx-matrix-rowhead);
    padding: 0 0.75rem;
    text-align: left;
    font-weight: 500;
    font-size: 0.82rem;
    background-color: var(--vx-surface);
    border-right: 1px solid var(--vx-line-strong);
    border-bottom: 1px solid var(--vx-line);
    overflow: hidden;
}

.vx-matrix__rowhead a {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--vx-ink);
    text-decoration: none;
}

.vx-matrix__rowhead a:hover,
.vx-matrix__rowhead a:focus-visible {
    color: var(--vx-action-text);
    text-decoration: underline;
}

.vx-matrix__totalhead,
.vx-matrix__total {
    width: var(--vx-matrix-total);
    min-width: var(--vx-matrix-total);
}

.vx-matrix__totalhead,
.vx-matrix__total,
.vx-matrix__coltotal {
    text-align: center;
    font-size: 0.74rem;
    white-space: nowrap;
}

/* A column's own total sits under a 2.25rem column, so it is sized like a
   cell. Giving it the row-total width instead widens every column in the
   grid to match -- table layout takes the widest cell in a column. */
.vx-matrix__coltotal {
    width: var(--vx-matrix-cell);
    min-width: var(--vx-matrix-cell);
    padding: 0;
    font-size: 0.6rem;
    color: var(--vx-muted);
    background-color: var(--vx-inset);
}

.vx-matrix__totalhead {
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    font-size: 0.66rem;
    color: var(--vx-muted);
    vertical-align: bottom;
    padding-bottom: 0.4rem;
    border-left: 1px solid var(--vx-line-strong);
}

.vx-matrix__total {
    color: var(--vx-muted);
    background-color: var(--vx-inset);
    border-bottom: 1px solid var(--vx-line);
    border-left: 1px solid var(--vx-line-strong);
    padding: 0 0.4rem;
}

.vx-matrix__total-mutual {
    font-weight: 700;
    color: var(--vx-action-text);
}

.vx-matrix__total-sep {
    color: var(--vx-faint);
    padding: 0 0.1em;
}

.vx-matrix tfoot th,
.vx-matrix tfoot td,
.vx-matrix tfoot .vx-matrix__coltotal {
    position: sticky;
    bottom: 0;
    z-index: 2;
    background-color: var(--vx-inset);
    border-top: 1px solid var(--vx-line-strong);
    border-bottom: 0;
}

.vx-matrix tfoot .vx-matrix__rowhead {
    z-index: 4;
    font-weight: 700;
    font-size: 0.66rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--vx-muted);
}

.vx-matrix__total--corner {
    background-color: var(--vx-inset);
}

/* --- cells ----------------------------------------------------------- */

.vx-matrix__cell {
    position: relative;
    width: var(--vx-matrix-cell);
    min-width: var(--vx-matrix-cell);
    height: var(--vx-matrix-cell);
    padding: 0;
    text-align: center;
    border-right: 1px solid var(--vx-line);
    border-bottom: 1px solid var(--vx-line);
}

.vx-matrix__mark {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: var(--vx-matrix-cell);
    font-size: 0.8rem;
    font-weight: 600;
    text-decoration: none;
    color: inherit;
}

.vx-matrix__mark:focus-visible {
    outline: 2px solid var(--vx-ink);
    outline-offset: -3px;
}

.vx-matrix__cell--empty {
    background-color: var(--vx-surface);
}

.vx-matrix__cell--preference {
    background-color: var(--vx-tint-live);
    color: var(--vx-tint-live-ink);
}

.vx-matrix__cell--mutual {
    background-color: var(--vx-action);
    color: var(--vx-ink-inverse);
}

.vx-matrix__cell--validated {
    background-color: var(--vx-structure);
    color: var(--vx-ink-inverse);
}

/* A validated match is the one cell state that must not rest on colour
   alone -- it is the outcome of the whole exercise, and orange-vs-navy is
   exactly the pair a protanope reads as one colour. The notch is the
   second channel. */
.vx-matrix__cell--validated .vx-matrix__mark::after {
    content: "";
    position: absolute;
    top: 0;
    right: 0;
    border-width: 0 0.45rem 0.45rem 0;
    border-style: solid;
    border-color: transparent var(--vx-o-5) transparent transparent;
}

.vx-matrix__cell--rejected {
    background-color: var(--vx-tint-neutral);
    color: var(--vx-faint);
}

/* Struck corner to corner rather than through the glyph: a line-through
   on a single digit turns "1" into something that reads as "+". */
.vx-matrix__cell--rejected .vx-matrix__mark {
    font-weight: 400;
    background-image: linear-gradient(
        to bottom right,
        transparent calc(50% - 0.5px),
        var(--vx-line-strong) calc(50% - 0.5px),
        var(--vx-line-strong) calc(50% + 0.5px),
        transparent calc(50% + 0.5px)
    );
}

/* --- crosshair ------------------------------------------------------- */

/* Reading a 40-wide row back to its team name, or a column down to its
   startup, is the thing that actually goes wrong on a grid this size. The
   row is pure CSS; the column needs the class that matrix_crosshair.js
   puts on every [data-col] in the hovered column. */
.vx-matrix tbody tr:hover .vx-matrix__cell::before,
.vx-matrix__cell.is-col-active::before {
    content: "";
    position: absolute;
    inset: 0;
    background-color: rgba(var(--vx-structure-rgb), 0.1);
    pointer-events: none;
}

.vx-matrix tbody tr:hover .vx-matrix__rowhead,
.vx-matrix__colhead.is-col-active {
    background-color: var(--vx-tint-info);
}

.vx-matrix tbody tr:hover .vx-matrix__rowhead a,
.vx-matrix__colhead.is-col-active .vx-matrix__colhead-label {
    color: var(--vx-structure);
    font-weight: 700;
}

/* --- legend ---------------------------------------------------------- */

.vx-matrix-legend {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem 1.5rem;
    align-items: center;
    margin: 1rem 0 0.75rem;
    font-size: 0.78rem;
    color: var(--vx-muted);
}

.vx-matrix-legend__item {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
}

.vx-matrix-legend__item--hint {
    color: var(--vx-faint);
}

.vx-matrix__swatch {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.35rem;
    height: 1.35rem;
    font-family: var(--vx-font-display);
    font-size: 0.72rem;
    font-weight: 600;
    border: 1px solid var(--vx-line-strong);
}

.vx-matrix__swatch--preference {
    background-color: var(--vx-tint-live);
    color: var(--vx-tint-live-ink);
}

.vx-matrix__swatch--mutual {
    background-color: var(--vx-action);
    color: var(--vx-ink-inverse);
    border-color: var(--vx-action);
}

.vx-matrix__swatch--validated {
    background-color: var(--vx-structure);
    color: var(--vx-ink-inverse);
    border-color: var(--vx-structure);
    position: relative;
}

.vx-matrix__swatch--validated::after {
    content: "";
    position: absolute;
    top: 0;
    right: 0;
    border-width: 0 0.4rem 0.4rem 0;
    border-style: solid;
    border-color: transparent var(--vx-o-5) transparent transparent;
}

.vx-matrix__swatch--rejected {
    background-color: var(--vx-tint-neutral);
    color: var(--vx-faint);
    background-image: linear-gradient(
        to bottom right,
        transparent calc(50% - 0.5px),
        var(--vx-line-strong) calc(50% - 0.5px),
        var(--vx-line-strong) calc(50% + 0.5px),
        transparent calc(50% + 0.5px)
    );
}

.vx-matrix__swatch--unranked {
    background-color: var(--vx-tint-live);
    color: var(--vx-tint-live-ink);
}

/* A 40-column matrix has no honest phone reflow -- there is no smaller
   arrangement of a grid that is inherently two-dimensional. It stays a
   scroll box with the team column pinned, and the row header narrows so
   more of the grid itself is on screen. */
@media (max-width: 767.98px) {
    .vx-matrix-scroll {
        --vx-matrix-rowhead: 8rem;
        --vx-matrix-head: 7.5rem;
        max-height: calc(100vh - 14rem);
    }
}
