﻿
/* Work Sans, self-hosted (wwwroot/fonts/work-sans/) instead of loaded from
   fonts.googleapis.com/fonts.gstatic.com - avoids the GDPR concern around Google Fonts
   transmitting visitors' IP addresses to Google without consent (a live legal issue for German
   sites specifically, see the LG München ruling), and means it doesn't need to go through the
   cookie-consent banner at all. Both files are variable fonts covering the full 400-700 weight
   range in one file each (that's genuinely how Google itself serves this specific family/version
   - not a mistake), so two @font-face rules with a font-weight range is correct here, not one
   rule per weight. */
@font-face {
    font-family: 'Work Sans';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('/fonts/work-sans/work-sans-normal.woff2') format('woff2');
}

@font-face {
    font-family: 'Work Sans';
    font-style: italic;
    font-weight: 400 700;
    font-display: swap;
    src: url('/fonts/work-sans/work-sans-italic.woff2') format('woff2');
}

.dev-version-badge {
    position: fixed;
    right: 8px;
    bottom: 8px;
    background: rgba(36, 26, 32, 0.75);
    color: #fff;
    font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
    font-size: var(--fs-xs);
    padding: 0.2rem 0.5rem;
    border-radius: 3px;
    z-index: 1000;
    pointer-events: none;
}

:root {
    /* Palette confirmed 2026-09-17 from the theme switcher, replacing the greener off-whites
       this site shipped with (--paper #e8edec, --surface-soft #f3f6f5, --tile #f3f6f5). */
    --paper: #ffffff;
    --surface: #ffffff;
    --ink: #241a20;

    /* Brand color (2026-08) - the site's wine palette uses the exact magenta sampled from the
       Particip logo (--logo-magenta), confirmed as the final brand color. Old pre-magenta
       values kept below, commented out, in case of a future revert.
    --wine-original: #422139;
    --wine-deep-original: #231820;
    --wine-soft-original: #f0d3e8;
    */
    --wine: var(--logo-magenta);
    --wine-deep: #50394a;
    --wine-soft: #f7dcf1;

    /* Hover state for magenta text: --wine (#974e83) darkened ~17% at the same hue.
       Distinct from --wine-deep, which buttons use for their hover background - that
       one is both darker and desaturated, which reads as a hue change rather than a
       darkening when applied to text. */
    --wine-hover: #7d416d;
    --surface-soft: #fcfafa;

    /* The tiles and list entries a reviewer is choosing a palette for: reference and job rows,
       the sector tiles, Featured Projects tiles, the Home services teaser cards, affiliated company
       tiles, the account form cards, and the Jobs application panel.
       Two card types are deliberately NOT here and stay on --surface: the contact form card and
       the contact office cards. (A third, the Services page's quick-fact cards, was in this list
       until those were removed on 2026-09-17.) They were included at first and it was too
       much - changing one colour repainted the Company and Contact pages' own furniture along with
       the actual content tiles. The office card is also what holds the Google Maps consent gate,
       so moving it read as the map changing colour.
       One token for the rest because they are meant to read as one family - before this they
       were split across three treatments (most on white, the affiliated tiles on the soft band
       tint, and the reference/job rows with no background at all, showing the page through), which
       is why a single "tile colour" did not previously exist to change.
       Now the same #ffffff as --paper and --surface, so tiles carry no colour contrast against
       the page at all and their border plus --shadow does the entire job of separating them -
       worth remembering before softening either, and the reason .service-detail-row was given a
       border of its own when it could otherwise collide with its band. */
    --tile: #ffffff;

    /* An expanded tile or row. The token stays separate from --tile so the two CAN differ, but
       in the confirmed palette they do not - both are #ffffff - and that has one real
       consequence, accepted rather than overlooked: of its four consumers, three
       (.highlight-panel-open, .service-detail-panel-open, .affiliated-panel-open) also carry a
       --wine left border and a heavy shadow, so they still read as open; .reference-row-open is
       background-only, so an expanded reference or job row is now distinguished solely by the
       content it reveals. Give the two tokens different values again to restore that cue. */
    --tile-open: #ffffff;

    /* Navbar and footer. Its default is the literal value of Bootstrap's own --bs-tertiary-bg,
       which is what the bg-body-tertiary class those two elements used to carry resolved to - so
       introducing this token changed nothing on screen. It exists because those two surfaces are
       the only ones the page/band background tokens never reached, and a reviewer needs to be
       able to try a colour for them too. */
    --chrome: #f8f9fa;
    --slate: #6b6069;

    --logo-grey: #3d4f51;
    --logo-magenta: #974e83;
    --line: #d5dede;
    --magenta: var(--logo-magenta);

    /* Text tones for the three --magenta bands - the world map, the Services framework-contract
       lots, and Company's Facts & Figures. Introduced 2026-09-23, when an audit found SIX literal
       light values spread across those bands with no token behind any of them, three of them
       near-identical whites doing the same job. This file had already predicted it: the
       .stat-label comment says "if a fourth appears, introduce a token", and by then there were
       three surfaces and six tones.
       Four roles, measured against --magenta #974e83:
         --band-heading  5.28:1  headings, stat figures, lot numbers, panel titles, links
         --band-text     4.75:1  body copy, and the colour the bands themselves set
         --band-muted    2.25:1  supporting text - labels, meta, empty and loading states
         --band-hover    5.62:1  interactive hover, one step above --band-heading
       Three values moved when they were folded into these roles, all of them upward, so every
       change improved contrast rather than trading it: .map-head p 3.22 -> 4.75,
       .map-projects-see-all 4.59 -> 5.28, and Company's band section heading 4.75 -> 5.28, that
       last one being the real inconsistency the audit was looking for - two section headings on
       two magenta bands were using different whites.
       --band-muted is deliberately the lowest and stays that way: it is supporting text by
       definition. It is NOT for controls - see .map-projects-close, which was in this tier and
       measured 2.01:1 until it was moved to --band-text.
       Note .world-map-tooltip keeps a literal #fdf6f9 BACKGROUND. Same value, unrelated role:
       it is a light surface behind dark text rather than band text, so it must not follow these
       if they are ever retuned. */
    /* The small-text scale. Everything below 1rem goes through these four steps.
       Before 2026-09-23 there were TWELVE distinct values under 1rem, each picked at its own call
       site, and three clusters of them could not be told apart: .highlight-link at 0.87rem beside
       .affiliated-card-registered at 0.875rem is 0.08px at a 16px root, and 0.78 / 0.8 / 0.82 sat
       within 0.64px of each other. That is not a scale, it is twelve independent guesses, and the
       cost is that nobody adding a rule knows which value to reach for.
       The steps are chosen so no existing value moved more than 0.64px, which is why they are not
       round numbers - 0.87 rather than 0.875 because .highlight-link's is the documented "majority
       size", and 0.8 rather than 0.81 because four rules already sat there. Largest shifts:
       .highlight-chip 0.68 -> 0.72 and .map-project-meta 0.76 -> 0.8, both up by 0.64px.
       One of these carries earlier reasoning and is worth checking rather than assuming:
       .reference-row-code was deliberately raised 0.78 -> 0.85 so it stayed legible at its
       knowingly-low 2.10:1 contrast. --fs-md is 0.87, i.e. very slightly LARGER, so it moves with
       that reasoning rather than against it.
       Sizes at or above 1rem are deliberately not in this scale: they are headings and leads, which
       take Bootstrap's own display-* and .lead rather than a value of ours. */
    --fs-xs: 0.72rem;
    --fs-sm: 0.8rem;
    --fs-md: 0.87rem;
    --fs-lg: 0.95rem;

    --band-heading: #fdf6f9;
    --band-text: #f4e9ee;
    --band-muted: #cf93ab;
    --band-hover: #ffffff;
    --shadow: 0 1px 2px rgba(36, 26, 32, 0.05), 0 12px 28px -18px rgba(36, 26, 32, 0.32);

    /* Shared by every place the site draws a title over a photograph - the page heroes and the
       services sector tiles. One definition rather than two copies, for the same anti-drift reason
       as --company-divider-gap: "the same treatment" is a claim that has to stay true, and nothing
       checks two identical gradients against each other.
       The stops approximate an ease-out curve rather than a straight ramp, because a linear alpha
       fade terminates visibly and reads as a band laid over the image. See ADR 0031 for why the
       scrim is this weak: most of the legibility work belongs to the type's own shadow, which
       follows the letterforms instead of veiling the picture. Those shadows are NOT shared - they
       scale with the type, and the tile's are roughly a third of the hero's. */
    --photo-scrim-stops:
        rgba(36, 26, 32, 0.40) 0%,
        rgba(36, 26, 32, 0.29) 15%,
        rgba(36, 26, 32, 0.20) 30%,
        rgba(36, 26, 32, 0.12) 45%,
        rgba(36, 26, 32, 0.064) 60%,
        rgba(36, 26, 32, 0.025) 75%,
        rgba(36, 26, 32, 0.004) 90%,
        rgba(36, 26, 32, 0) 100%;

    /* Not pure #fff: nothing in a photograph is fully white across an area of any size, so #fff
       puts the type outside the image's tonal range and reads as a cut-out laid on top. */
    --photo-title-color: rgba(252, 247, 243, 0.92);

    --font-display: 'Work Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    --font-body: 'Work Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    --font-mono: ui-monospace, 'SF Mono', 'Cascadia Code', 'Roboto Mono', Consolas, monospace;

    /* Kept in sync with the navbar's real rendered height by site.js (it changes with
       the mobile hamburger menu open/closed, viewport width, etc.) so anything that
       scrolls itself into view can offset for the sticky navbar sitting on top of it. */
    --navbar-height: 0px;

    /* Gap between a vertical rule and the text on either side of it. A single value used by both
       columns so the two sides cannot drift apart, and reused below lg as the vertical gap once
       the columns stack - one value for the one conceptual separation, whichever direction it
       happens to run in. Introduced for Corporate Objectives, which no longer has a rule at all
       (its list was dropped on 2026-09-17); the Facts & Figures divider is the consumer now. */
    --company-divider-gap: 2.5rem;

}

/* .home-intro-section is gone (2026-09-22). Home rendered its intro as a section of its own with
   padding-top: 7rem - the room the retired introHeading had occupied, tuned by measuring the
   rendered page (5.5rem read as too close to the image, 8.5rem as too much). It was reported as
   simply too big a gap, and the fix was structural rather than a smaller number: homePage.cshtml
   now passes the intro through HeroBanner as every other page does, so it takes that partial's
   own .hero-banner-content padding and the two cannot drift apart again.
   Worth keeping from the tuning that is now discarded, in case a bespoke gap is ever wanted here
   again: the obvious reference does NOT work, because the Featured Projects heading sits only
   76px below the intro's bottom edge while reading as further away - a band's own background
   change supplies separation that a boundary under a full-bleed image does not get. */

/* The navbar and the footer, which are deliberately one rule rather than two: they are meant to
   be the same colour as each other always, so there is nowhere for them to drift apart. Replaces
   the bg-body-tertiary class both elements used to carry - swapped out rather than overridden, so
   the result doesn't depend on this stylesheet loading after Bootstrap's. */
.site-chrome {
    background-color: var(--chrome);
}

/* Sticky footer: on pages shorter than the viewport, main grows to fill the remaining space
   so the footer always sits flush against the bottom of the browser window instead of floating
   above it with page background visible underneath. */
body {
    background-color: var(--paper);
    color: var(--ink);
    font-family: var(--font-body);
    display: flex;
    flex-direction: column;
    min-height: 100vh;
}

body > main {
    flex: 1 0 auto;
}

/* Bold text authored in a rich text field renders at normal weight inside .lead, and the reason
   is not obvious from either rule on its own. Bootstrap sets .lead to font-weight: 300 and
   b/strong to font-weight: bolder - and 'bolder' is *relative*, not absolute: against an
   inherited 300 the CSS weight table resolves it to 400. So an editor's bold comes out one step
   heavier than its surroundings instead of bold, which reads as having done nothing at all.
   An absolute weight fixes it. Scoped to .lead rather than to one page because eleven views use
   that class and most of them render editor-authored rich text, so every one of them has the
   same latent bug the first time somebody bolds a line. */
.lead strong,
.lead b {
    font-weight: 700;
}

/* Every element that gets programmatically scrolled into view (expanding a featured
   project or a reference row) needs this, or the sticky navbar just overlaps the top of
   it once the scroll lands. */
.highlight-card,
.highlight-panel,
.reference-row,
#references-results,
#references-top,
#featured-projects,
#contact-legal .accordion-item,
.service-detail-tile,
.service-detail-row,
.service-detail-panel,
.affiliated-tile,
.affiliated-panel,
#account-confirmation {
    scroll-margin-top: calc(var(--navbar-height) + 12px);
}

/* The whole tile is a click target, not just its Read more button - see bindExpandGrid in
   site.js - so the cursor has to say so. A card that expands on click while showing a default
   arrow gives a visitor nothing to discover it by, which was the substance of the complaint
   this answers. The button itself stays: it is the visible hint AND the only keyboard- and
   screen-reader-operable control, neither of which a clickable <article> is. */
.highlight-card,
.service-detail-tile,
.affiliated-tile {
    cursor: pointer;
}

h1, h2, h3, h4 {
    font-family: var(--font-display);
    font-weight: 600;
    text-wrap: balance;
}

.mono {
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
}

.navbar-brand {
    font-family: var(--font-display);
    font-weight: 600;
    color: var(--wine) !important;
}

/* Bootstrap's own .nav-link.active only changes colour, which against this navbar's palette is
   too slight to read as "you are here" - so the weight carries it, with the wine underline as a
   second, non-colour-dependent cue. Deliberately not applied to .topbar-link items (LinkedIn,
   newsletter, Login/Register): those are actions rather than places in the site. */
.navbar-nav .nav-link.active {
    font-weight: 600;
    color: var(--wine);
    box-shadow: inset 0 -2px 0 var(--wine);
}

/* The current page's name beside the burger, below lg only - see master.cshtml for why it uses
   the node name rather than the nav label. Truncates rather than wrapping or growing, so a long
   page name can never push the toggler off the edge of a narrow screen. */
.navbar-current-page {
    flex: 1 1 auto;
    min-width: 0;
    margin: 0 0.75rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-weight: 600;
    color: var(--wine);
}

/* The row/centring is scoped to lg and up on purpose. These items live inside the navbar
   collapse, and Bootstrap's own .navbar-expand-lg rules already lay .navbar-nav out as a column
   below lg and a row at lg - so an unconditional flex-direction: row (which is what this used to
   be) forced all three onto one line inside the opened mobile menu, where .topbar-link's
   white-space: nowrap then guaranteed they ran off the side of a phone screen. Leaving the
   direction to Bootstrap makes them stack as a list on mobile with no extra rules.

   Everything that spaces these items apart is scoped to lg and up for the same reason, and it
   is easy to get wrong: below lg they are rows in the same stacked list as the main nav items
   above them, so anything extra here misaligns them against those. Two things did. The gap
   became vertical space between items that the main list does not have, and .topbar-link's own
   padding overrode Bootstrap's .navbar-nav .nav-link (which is 0.5rem 0 - zero horizontal), so
   these four sat 8px further right than every item above them and were packed tighter. Both now
   apply only where they are actually wanted, in the inline desktop row. */
.topbar-inline {
    gap: 0;
}

@media (min-width: 992px) {
    .topbar-inline {
        flex-direction: row;
        align-items: center;
        margin-left: 0.5rem;
        gap: 0.25rem;
    }

    .topbar-link {
        padding: 0.25rem 0.5rem;
    }
}

.topbar-link {
    color: var(--slate);
    white-space: nowrap;
}

.topbar-link:hover {
    color: var(--wine);
}

.topbar-language-switcher .dropdown-toggle {
    font-size: 1rem;
    line-height: 1;
    padding-left: 0.4rem;
    padding-right: 0.4rem;
}

/* Scoped to these specific navbar icons (not a blanket .bi-globe2 rule) since that same icon
   class is also used elsewhere, e.g. JobCard.cshtml's location indicator, which shouldn't turn
   magenta too. */
.topbar-link .bi-linkedin,
.topbar-language-switcher .bi-globe2 {
    color: var(--wine);
}

/* Bootstrap's .dropdown-menu sets --bs-dropdown-min-width: 10rem and reads it back via
   min-width: var(...) on the same selector - way more than the short "EN"/"FR" codes it holds
   now. Overriding the min-width property alone wasn't enough (Popper/Bootstrap's own JS can
   still size the menu against that custom property), so reset the variable itself too. */
.language-dropdown-menu {
    --bs-dropdown-min-width: 0rem;
    min-width: 0 !important;
    width: max-content;
    padding: 0.25rem;
}

.language-dropdown-menu .dropdown-item {
    text-align: center;
    padding: 0.25rem 0.75rem;
}

.topbar-language-switcher .dropdown-toggle::after {
    vertical-align: 0.15em;
}

.topbar-language-switcher .dropdown-menu {
    font-size: var(--fs-md);
    min-width: 8rem;
}

.topbar-language-switcher .dropdown-item.active {
    background: var(--wine-soft);
    color: var(--wine);
}

.btn-primary {
    background-color: var(--wine);
    border-color: var(--wine);
}

.btn-primary:hover, .btn-primary:focus {
    background-color: var(--wine-deep);
    border-color: var(--wine-deep);
}

.btn-outline-secondary {
    color: var(--wine);
    border-color: var(--wine);
}

.btn-outline-secondary:hover {
    background-color: var(--wine);
    border-color: var(--wine);
}

.page-item.active .page-link {
    background-color: var(--wine);
    border-color: var(--wine);
}

.page-link {
    color: var(--wine);
}

/* ---------- shared reference styling ---------- */

.reference-card-title {
    color: var(--wine);
}

.reference-card-meta li {
    line-height: 1.6;
}

.reference-card-description p:last-child,
.reference-card-description ul:last-child {
    margin-bottom: 0;
}

/* Job and reference descriptions come from the Promt API, not from this CMS, and the job ones
   arrive carrying inline styles: `<span style="font-size: 16px; font-family: system-ui, ...;
   color: #212529">`, twelve spans per language on the jobs page. #212529 is Bootstrap's default
   body colour rather than this site's --ink #241a20, so job descriptions were the one place on
   the site where body text was not --ink - subtle, since both are near-black, but real and
   entirely unintended.
   !important is necessary and is not the usual code smell. An inline style outranks any class
   selector without it, and the rule this file states elsewhere - that inline styling should be
   removed at source rather than fought - applies to content WE author, where the fix is the
   Tiptap configuration. This content is served by an external API this repo does not own, so
   there is no source to fix it at. .download-link already does exactly this, for the same reason.
   The second rule keeps links out of it. There are no anchors in any description today, checked
   against all ten live listings, but that content is authored upstream and could gain one at any
   time, at which point a blanket override would silently paint it --ink and it would stop looking
   like a link. Stated explicitly rather than with :not(a *), which is a Selectors 4 complex
   :not() and buys nothing here.
   Deliberately colour ONLY. The same spans also force font-family: system-ui and font-size: 16px,
   which is the more visible defect - the jobs page renders its descriptions in a different
   typeface from the rest of the site - but that was not part of this change. One more declaration
   in the first rule closes it if wanted. */
.reference-card-description,
.reference-card-description * {
    color: var(--ink) !important;
}

.reference-card-description a,
.reference-card-description a * {
    color: var(--wine) !important;
}

/* Moves the spam honeypot out of sight without display:none, which some bots deliberately
   skip - see Partials/SpamHoneypot.cshtml for the whole arrangement. Off-screen rather than
   zero-size so a field left focusable by accident still cannot be typed into visibly. */
.spam-honeypot {
    position: absolute;
    left: -9999px;
    width: 1px;
    height: 1px;
    overflow: hidden;
}

/* ---------- References page list rows ---------- */

/* Frames every list row on both the References and Jobs pages as its own card, so adjacent
   listings read as distinct entries rather than one continuous list. Job rows carry
   .reference-row alongside .job-row (see JobCard.cshtml), so this one rule is literally the
   same frame on both pages rather than a copy per page - the two were asked to look identical,
   and duplicating five declarations is how they would silently drift the next time one side is
   nudged. References used to be bottom-border-only here (padding-bottom + border-bottom) while
   only Jobs had the frame; that difference is what this replaced.
   The 1px --line border alone read as too faint to separate adjacent listings, so the frame is
   reinforced with --shadow - the same elevation token .contact-office-card and
   .service-detail-row already use to lift a card off its background. Chosen over a heavier or
   darker border because the palette has no mid-grey between --line and --slate, and 2px of a
   very light grey adds bulk without much more definition. */
.reference-row {
    border: 1px solid var(--line);
    border-radius: 6px;
    padding: 0.85rem 1rem;
    margin-bottom: 0.6rem;
    box-shadow: var(--shadow);
}

/* Reference and job rows had no background of their own until the --tile token existed: they
   showed the page through, and an expanded row was distinguished purely by gaining a tint. Now
   both states have a fill, which is the one place where the default palette is not a no-op - a
   closed row looks slightly different than it did. */
.reference-row {
    background: var(--tile);
}

/* An expanded row keeps the full frame above and only changes this background. It used to also set
   its own radius/padding/margin plus a border-bottom: none - all of which existed to work
   around the old bottom-border-only collapsed style, and would now either fight the frame or
   punch a hole in its bottom edge. */
.reference-row-open {
    background: var(--tile-open);
}

.reference-row-header {
    gap: 1rem;
    align-items: flex-start;
}

/* Same reasoning as .highlight-panel-open above: a 125px thumbnail plus a 1rem gap leaves too
   little room for the title and metadata beside it on a phone, so they go underneath instead.
   The image widens to fill the row once it has nothing sitting next to it - at 125px it would
   read as a stray postage stamp above the text. */
@media (max-width: 575.98px) {
    .reference-row-header {
        flex-direction: column;
    }

    .reference-row-img {
        width: 100%;
        height: 180px;
    }

    /* Source order would drop the code below the text once the header is a column. order keeps
       it at the top, where the auto margin now right-aligns it on the cross axis instead. */
    .reference-row-code {
        order: -1;
    }
}

.reference-row-img {
    width: 125px;
    height: 94px;
    object-fit: cover;
    border-radius: 4px;
}

.reference-row-body {
    flex: 1 1 auto;
    min-width: 0;
}

/* The project code, sitting top right of a reference row. References only: JobCard shares
   .reference-row-header but renders no code, so this is its own class rather than anything on
   the shared one. flex-shrink: 0 with nowrap keeps it intact - it is an identifier, and one
   that has wrapped or been truncated is worse than not showing it; the body beside it carries
   min-width: 0, so the title wraps instead. */
/* Appearance only, shared by the References row's code (top right of its header) and Featured
   Projects' own (top right of the expanded panel). One rule rather than a copy per page,
   because the two were asked to look identical - the same reasoning as the shared .highlight-link
   and .reference-row frame, both of which this file records as having drifted when duplicated.
   Placement is deliberately NOT here: each site positions its own, below. */
.reference-row-code,
.highlight-panel-code {
    /* Raised from 0.78rem on 2026-09-18, in the same pass that made the colour much paler and
       for that reason: at 2.10:1 the code needs the size back to stay readable, and the two
       levers pull against each other rather than both meaning "subtle". Colour is doing the
       receding here; size is only keeping it legible while it recedes. */
    font-size: var(--fs-md);
    letter-spacing: 0.03em;
    /* Lighter than the --slate this used to take, asked for on 2026-09-18 so the code recedes
       behind the title and meta it sits beside. Measured on --slate's own hue against the white
       tile: --slate was 6.00:1, an AA-preserving 4.54:1 (#7e737c) was not perceptibly different,
       3.01:1 (#9b9299) still read as too strong, and this is 2.10:1 - a third of the original.
       For scale, --line, the token used for 1px borders, is 1.37:1, so there is not much room
       below this before the code stops being readable at all. **That is below WCAG AA's 4.5:1 for small
       text**, accepted deliberately here: the code is a supplementary identifier rather than
       content anyone needs to read to use the page, and it duplicates nothing that is only
       available here. Don't copy this value onto anything load-bearing. Not a token, because
       nothing else wants this tone yet; make it one if a second consumer appears. Letter-spacing
       is the one lever still untouched: dropping the 0.03em would soften the code further
       without costing any legibility, which is what to reach for before darkening this again. */
    color: #b8b1b6;
    white-space: nowrap;
}

/* Pushed to the right of the header by margin-left: auto, and never allowed to shrink or wrap -
   a half-shown identifier is worse than none. */
.reference-row-code {
    margin-left: auto;
    flex-shrink: 0;
}

/* Top right of the open panel, in flow rather than absolutely positioned: it is the third item
   on the panel's first flex line, and the headline before it is flex: 1 1 0%, so the headline
   absorbs the free space and this lands hard right. align-self overrides the panel's own
   align-items: center, which would otherwise float it against the middle of the image. Doing it
   this way means the layout reserves room for the code, so a long title cannot run underneath -
   the failure the Services panel used to need a 7rem padding-right to avoid.
   Below the panel's mobile breakpoint the container becomes a column and this simply takes its
   own line under the title, left-aligned, which is the only sensible place for it there. */
.highlight-panel-code {
    flex: 0 0 auto;
    align-self: flex-start;
}

/* Jobs only: overrides .reference-row-header's shared align-items: flex-start so the title
   column and the meta column stretch to match each other's height, then the title column's
   own trailing content (location + the Read more footer, wrapped together in
   .job-row-footer-group) is pushed down to the bottom as one unit via margin-top: auto - the
   two columns end at the same height instead of the footer sitting wherever the title column's
   own (usually shorter) natural content happens to end. The auto margin goes on the wrapper,
   not on location or the footer individually - putting it on just one of two adjacent flex
   children each gets its own independent share of the free space, which can visibly separate
   them instead of moving them down together. */
.job-row-header {
    align-items: stretch;
}

.job-row-header .reference-row-body {
    display: flex;
    flex-direction: column;
}

.job-row-footer-group {
    margin-top: auto;
}

/* Jobs-only second column (References uses this space for .reference-row-img instead) -
   a fixed width with the title column's min-width: 0 above lets long job/project titles
   wrap normally instead of overflowing into or squashing this column. */
.job-row-meta {
    flex: 0 0 200px;
    padding-left: 1.25rem;
    border-left: 1px solid var(--line);
    margin-bottom: 0 !important;
}

.job-row-meta-item {
    margin-bottom: 0.35rem;
}

.job-row-meta-item:last-child {
    margin-bottom: 0;
}

.job-row-meta dt {
    color: var(--magenta);
    font-weight: 600;
    font-size: var(--fs-sm);
}

.job-row-meta dd {
    color: var(--bs-body-color);
    margin-bottom: 0;
}

/* The location line moved out of .job-row-meta - now its own row at the bottom of the card,
   directly above the Read more/expand button. */
.job-row-location {
    color: var(--bs-body-color);
}

/* There is deliberately no .job-row frame rule here any more: the card frame moved up to the
   shared .reference-row rule in the References section, and job rows pick it up through their
   own .reference-row class. Don't re-add a jobs-only copy - the two pages are meant to stay
   identical, and a second copy is exactly how they would drift. */

/* Mobile job card, reordered to title -> project -> meta -> location -> Read more. The source
   order can't produce that on its own: .job-row-meta is a sibling of .reference-row-body while
   the location and the Read more button (plus everything the button expands - the full
   description and the application form) sit *inside* that body, so `order` has no way to place
   the meta between them. Left alone, the state-of-assignment facts land after the entire
   expanded form on a phone. display: contents promotes the body's children into this header's
   own flex flow so all four can be ordered together.
   Two things carried over from the row layout had to go, both of them the traps documented in
   BACKLOG.md: .reference-row-header's own mobile rule already makes this a column, so
   flex-wrap: wrap would spill into a second column to the right rather than onto a new row, and
   the meta's flex-basis of 100% would mean 100% of the container's *height* rather than its
   width. Neither had visibly broken yet - this container's height is indefinite, so the wrap
   never triggered - but both were live hazards. */
@media (max-width: 575.98px) {
    .job-row-header {
        flex-wrap: nowrap;
    }

    .job-row-header .reference-row-body {
        display: contents;
    }

    .job-row-header .reference-card-title {
        order: 1;
    }

    .job-row-header .job-row-project {
        order: 2;
    }

    .job-row-footer-group {
        order: 4;
    }

    .job-row-meta {
        order: 3;
        flex: 0 0 auto;
        padding-left: 0;
        border-left: none;
        border-top: 1px solid var(--line);
        padding-top: 0.75rem;
        margin-top: 0.5rem !important;
    }
}

.reference-row-footer {
    display: flex;
    flex-direction: column;
}

/* Sets the Jobs page's application section apart from the job description above it, so it reads
   as its own sub-section rather than a continuation of the same content. It takes --tile against
   its containing row's --tile-open, which is the relationship it has always had (before the
   tokens existed it was --paper inside a --surface-soft row).
   The border is what makes that survive a palette change rather than depend on the two tokens
   differing - and in the palette confirmed 2026-09-17 they do NOT differ, both being #ffffff, so
   without it this panel rendered completely flush with its row and stopped reading as a section
   at all. Same collision, and the same fix, as .service-detail-row against its own band. */
.job-application-panel {
    background: var(--tile);
    border: 1px solid var(--line);
    border-radius: 3px;
    padding: 1.25rem 1.5rem;
}

/* Mirrors the featured-projects/service-tile pattern: once expanded, the description
   comes first and the toggle button drops below it, bottom-right, instead of sitting
   above the text it controls. reference-row-open is toggled by site.js on the
   show/hide collapse events, including a one-time sync on load for rows that start
   already expanded (a forceExpanded map link), so it stays the single source of truth
   for "is this row open" regardless of how it got that way. */
.reference-row-open .reference-card-toggle {
    order: 2;
    align-self: flex-end;
}

.reference-row-open .reference-row-footer .collapse {
    order: 1;
}

/* Bootstrap's .collapse is display:none until .show, so the filter panel has to be forced back
   open above lg - otherwise the desktop layout, which has no toggle button (d-lg-none), would
   show no filters at all. Only meaningful below lg, where the columns stack and the panel is
   genuinely collapsible. */
@media (min-width: 992px) {
    .filter-panel-collapse {
        display: block !important;
        height: auto !important;
        visibility: visible !important;
    }
}

.filter-list {
    max-height: 220px;
    overflow-y: auto;
    border: 1px solid var(--line);
    border-radius: 4px;
    padding: 0.5rem 0.75rem;
}

/* Shorter on phones, so there is page left either side of it to swipe on.
   A nested scroller traps the gesture: momentum stays inside it, so a thumb landing on the
   list scrolls the list and the page stays put until you lift and swipe again. At 220px this
   was a third of a phone screen and easy to land on by accident - worst on My Particip, whose
   three preference lists sit permanently in the page with no collapse in front of them.
   The scroller itself has to stay: the country list is 250 items (regions 53, keywords 27), so
   letting it flow would make the page enormous. This is a mitigation, not a cure - the cure is
   fewer items on screen, which the type-to-filter box above each list already provides.
   A fixed px rather than a vh: vh scales the wrong way, growing the list on the tall phones
   where the problem is already worst. And NOT overscroll-behavior: contain, which prevents the
   chaining that currently rescues you at the end of the list and would make this worse. */
@media (max-width: 575.98px) {
    .filter-list {
        max-height: 150px;
    }
}

.reference-active-filters:empty {
    display: none;
}

.reference-active-filters-label {
    font-size: var(--fs-sm);
    color: var(--slate);
}

.filter-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    background: var(--wine-soft);
    color: var(--wine);
    border: none;
    border-radius: 999px;
    padding: 0.35rem 0.75rem;
    font-size: var(--fs-sm);
    font-weight: 600;
    line-height: 1;
    cursor: pointer;
    transition: background 0.15s ease, color 0.15s ease;
}

.filter-chip:hover {
    background: var(--wine);
    color: #fff;
}

.filter-chip-clear {
    background: none;
    color: var(--slate);
    text-decoration: underline;
    text-underline-offset: 2px;
}

.filter-chip-clear:hover {
    background: none;
    color: var(--wine);
}

/* Replacement for the native file input's own widget, used by the My Particip upload and the job
   application form. The native "Choose file / No file chosen" text is drawn by the user agent and
   follows the BROWSER's language rather than the page's - it is not settable from HTML, CSS or a
   dictionary key, confirmed against MDN - so on a French or Spanish page it read as English for
   anyone whose browser is English. The real input is hidden and a <label for> stands in, which
   opens the picker with no JavaScript at all.
   Clipped rather than display:none or visibility:hidden, and this is load-bearing: both inputs are
   `required`, and a required control the browser cannot focus makes Chrome refuse to submit with
   "An invalid form control is not focusable" in the console and nothing at all on screen. Clipping
   keeps it rendered, focusable and able to anchor its own validation bubble. */
.file-picker-input {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

.file-picker {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.75rem;
}

/* Two treatments on purpose, because the two file inputs sit in different kinds of company.
   The job application's is a standalone control above its own chip list, so a plain button reads
   correctly there. My Particip's sits in a form row beside a .form-select and an Upload button, so
   it has to look like a field or the row falls apart - which it did: the first version of this
   dropped .form-control from the input (it had to, the input is hidden now) without giving the
   replacement an equivalent frame, and the field stopped reading as a field at all.
   The metrics below are Bootstrap's own .form-control values, and the button reproduces what
   Bootstrap does to ::file-selector-button - negative margins pulling it flush into the box with a
   divider on its inner edge - so this matches the control it replaced rather than approximating
   it.
   flex-wrap is overridden to nowrap, and that is load-bearing rather than tidiness. .file-picker
   sets wrap, and the button only sits flush in the box by stretching to its own FLEX LINE - so the
   moment the readout wraps to a second line, the divider and the button's bottom edge stop partway
   down the field and the frame visibly falls apart. Spanish is what surfaced it ("Seleccionar
   archivo" beside "Ningun archivo seleccionado"), but the width is the cause, not the language:
   this field is col-md-5, whose narrowest instance is ~281px at the md breakpoint - narrower than
   the ~366px it gets stacked full-width on a phone, which is why fixing this per-breakpoint would
   have missed the worst case. */
.file-picker-field {
    width: 100%;
    gap: 0;
    flex-wrap: nowrap;
    min-height: calc(1.5em + 0.75rem + 2px);
    padding: 0.375rem 0.75rem;
    border: 1px solid var(--line);
    border-radius: 0.375rem;
    background-color: var(--tile);
}

/* flex: 0 0 auto so the readout, not the button, absorbs a narrow field. Without it both items
   shrink and the button's own label starts wrapping instead, which is the same broken frame by a
   different route. */
.file-picker-field .file-picker-button {
    display: inline-flex;
    align-items: center;
    align-self: stretch;
    flex: 0 0 auto;
    margin: -0.375rem 0.75rem -0.375rem -0.75rem;
    padding: 0.375rem 0.75rem;
    border: 0;
    border-right: 1px solid var(--line);
    border-radius: 0;
    background-color: var(--surface-soft);
    color: var(--ink);
    cursor: pointer;
}

/* A literal rather than a token, deliberately: since the 2026-09-17 palette change --paper,
   --surface, --tile and --tile-open are all #ffffff, so there is no token holding "one step darker
   than the button's own --surface-soft" and var(--tile-open) here would have been an invisible
   hover. If a second control ever needs the same value, that is the point to introduce a token
   rather than copy this. */
.file-picker-field .file-picker-button:hover {
    background-color: #f2eff1;
}

/* The label is the only visible control, so it has to carry the states the input would have shown.
   :focus-visible rather than :focus so a mouse click does not leave a ring behind, matching how the
   rest of the site treats focus. The framed variant takes the ring on the box, the way a focused
   form control does; the bare one takes it on the button, since that is the whole control. */
.file-picker-input:focus-visible + .file-picker-field {
    border-color: var(--wine);
    box-shadow: 0 0 0 0.2rem rgba(151, 78, 131, 0.25);
}

.file-picker-input:focus-visible + .file-picker:not(.file-picker-field) .file-picker-button {
    outline: 2px solid var(--wine);
    outline-offset: 2px;
}

.file-picker-input.is-invalid + .file-picker-field {
    border-color: var(--bs-form-invalid-border-color, #dc3545);
}

.file-picker-input.is-invalid + .file-picker:not(.file-picker-field) .file-picker-button {
    border-color: var(--bs-form-invalid-border-color, #dc3545);
    color: var(--bs-form-invalid-color, #dc3545);
}

/* Truncated rather than wrapped. A filename is chosen by the member, so its length is unbounded,
   and this control has to stay one line tall for the reason above. Replaces an earlier
   overflow-wrap: anywhere, which answered the same "a long filename must not widen the row"
   problem by wrapping - now the thing being prevented rather than the remedy.
   min-width: 0 is the declaration that is easy to leave out and silently defeats the other three:
   white-space: nowrap makes this span's min-content width the entire string, and a flex item will
   not shrink below its min-content size on its own, so without it the field is pushed wider
   instead of the text ellipsing. */
.file-picker-name {
    color: var(--slate);
    font-size: var(--fs-md);
    min-width: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.job-application-file-list {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    padding: 0;
    margin: 0.5rem 0 0;
    list-style: none;
}

.job-application-file-item {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    background: var(--wine-soft);
    color: var(--wine);
    border-radius: 999px;
    padding: 0.35rem 0.5rem 0.35rem 0.75rem;
    font-size: var(--fs-sm);
    font-weight: 600;
    line-height: 1;
}

.job-application-file-remove {
    background: none;
    border: none;
    color: inherit;
    font-size: 1rem;
    line-height: 1;
    cursor: pointer;
    padding: 0;
}

.job-application-file-remove:hover {
    color: var(--ink);
}

.job-application-submitted-files {
    padding: 0;
    margin: 0 0 0.5rem;
    list-style: none;
    font-size: var(--fs-md);
    color: var(--slate);
}

/* Dims the results while an AJAX filter refresh is in flight, so a slow response still
   gives some feedback instead of looking like the click did nothing. */
.references-loading {
    opacity: 0.5;
    pointer-events: none;
    transition: opacity 0.15s ease;
}

/* ---------- home page: alternating section bands ---------- */

/* Home is logically 4 sections - hero+intro, Featured Projects, and the two ServiceOverview
   blocks - that alternate between the page's normal --paper background (sections 1 and 3, no
   class needed) and this slightly different tint (sections 2 and 4), World Map excluded since
   it already has its own fixed magenta band regardless of where it falls in between. */
.home-band-alt {
    background: var(--surface-soft);
}

/* ---------- home page: project highlight grid ---------- */

/* baseline, not flex-end or flex-start. The intro text used to be a single line, so flex-end
   looked fine; once it wraps to several lines flex-end drags the button down to the bottom of
   the text block, and flex-start then sits it level with the top of the display-5 heading's
   box, which reads too high against a heading that large. baseline lines the button's own text
   up with the "Featured projects" baseline: a flex item's baseline is that of its first line
   box, which here is the heading. Same reasoning, and the same fix, as the References page's
   title row - see BACKLOG.md, where aligning box edges left a much smaller right-hand element
   visibly off against the title's baseline despite the boxes being flush.

   This block carried a padding-top: 1.75rem until 2026-09-22, undocumented and on top of the
   section's own py-5, which put this heading 76px below its band's top edge against 48px on
   Company and Services. Removed; the band's py-5 is the whole spacing now. Don't reintroduce a
   top padding here - if this heading ever needs more room than the others, the honest place to
   say so is the section's own padding utility, where it is visible in the markup. */
.highlights-intro {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: 1.25rem;
    margin-bottom: 2.5rem;
}

/* The basis is 22rem rather than auto, and that is what keeps the button on the first line.
   Flex decides line-breaking from each item's *hypothetical* size - flex-basis, before any
   shrinking is applied - so with `auto` this block claims its max-content width, i.e. the
   whole intro sentence on one line, and the button is wrapped to a second line before
   flex-shrink or min-width:0 ever get a say. Pinning a modest basis means it never claims
   more than 22rem while the browser is deciding whether both fit; it then grows to fill the
   row and the sentence wraps inside its own box, which is where the wrapping belongs.
   22rem is also what preserves the deliberate mobile behaviour: 22rem + the button + the gap
   is roughly 570px, so below that the button still drops to its own line rather than
   squeezing the text into a sliver. */
.highlights-intro-text {
    flex: 1 1 22rem;
    min-width: 0;
}

/* .highlights-stat wraps rich text from the backend (content.Text), which brings its own
   <p> tags - a div (not a <p>) avoids nesting block elements inside a paragraph, which
   would otherwise make the browser close the tag early and break the flex layout below it.
   Sized to match Bootstrap's .lead (1.25rem at weight 300) because a display-5 heading is
   paired with a lead paragraph everywhere else on the site - see the Services page, where
   that pairing is deliberate and documented. It was 0.9rem in muted --slate on a single
   nowrap line until 2026-09-17: the element was built to hold a short stat beside the
   button ("N projects in M countries"), not the section intro prose it actually carries.
   The nowrap had to go with the size rather than separately - this copy is ~800px on one
   line even at the old 0.9rem, and would collide with the button at lead size. */
.highlights-stat {
    color: var(--ink);
    font-size: 1.25rem;
    font-weight: 300;
}

/* 1rem, matching Bootstrap's own paragraph rhythm and therefore every other block of authored
   copy on the site. It was 0.6rem, which made this the one place paragraphs sat closer together
   than everywhere else - found by auditing every rule that sets a margin on `p`, where this was
   the sole outlier. The :last-child trim below is what keeps the tighter gap where it is
   actually wanted, under the last line. */
.highlights-stat p {
    margin: 0 0 1rem;
}

.highlights-stat p:last-child {
    margin-bottom: 0;
}

.highlights-stat .mono,
.highlights-stat strong,
.highlights-stat b {
    color: var(--wine-deep);
    font-weight: 700;
}

/* Flexbox (not grid) so an expanded tile can grow wider right where it sits, which replaced
   an earlier grid-column:1/-1 + order:-1 jump-to-top trick. The Home services teaser's own
   .service-grid carried the matching note and was deleted with that teaser on 2026-09-18. */
.highlights-grid {
    display: flex;
    flex-wrap: wrap;
    gap: 2rem 1.75rem;
}

.highlight-card {
    flex: 0 1 calc(33.333% - 1.167rem);
}

/* 700px, not 880px: the grid holds nine tiles, and nine is odd - so any two-column window
   strands one tile alone on the last row. Staying three-across through tablet widths (iPad
   portrait is 768px) keeps it a clean 3x3 for every size that can carry three columns. */
@media (max-width: 700px) {
    .highlight-card {
        flex-basis: calc(50% - 0.875rem);
    }
}

@media (max-width: 600px) {
    .highlight-card {
        flex-basis: 100%;
    }
}

.highlight-card {
    background: var(--tile);
    border-radius: 3px;
    overflow: hidden;
    box-shadow: var(--shadow);
    display: flex;
    flex-direction: column;
    transition: transform 0.25s ease;
}

/* Hover effects on the three click-to-expand tile families are gated on the pointer being able to
   hover at all. On touch there is no hover state to leave: a tap latches :hover and it stays until
   the visitor taps something else, so a tile sits in its hover appearance long after it has been
   expanded and collapsed again. Reported on the sector tiles, where the effect is an un-blur and
   so unmissable; this lift is the same bug, only quieter.
   Deliberately NOT applied to the other hover rules in this file, of which there are fifteen. A
   latched :hover is only ever visible on something you tap that stays on screen - a link or button
   that navigates away, submits, or closes a panel has replaced the page before the stuck state
   could show - so gating those is a wide diff for no observable difference. Three of them also
   combine :hover with :focus (.btn-primary, .download-link, .not-found-home-link), and wrapping
   those as they stand would switch focus styling off on touch devices, which have keyboards.
   Wrapped in place rather than collected into one block, because these rules are scattered across
   1700 lines and moving them would reorder the cascade. */
@media (hover: hover) {
    .highlight-card:hover {
        transform: translateY(-3px);
    }
}

.highlight-card-media {
    aspect-ratio: 4 / 3;
    overflow: hidden;
    background: var(--line);
}

.highlight-card-media img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: transform 0.5s ease;
}

/* Pointer-only, see .highlight-card:hover above. */
@media (hover: hover) {
    .highlight-card:hover .highlight-card-media img {
        transform: scale(1.04);
    }
}

.highlight-card-body {
    padding: 1.25rem 1.375rem 1.5rem;
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    flex: 1;
}

.highlight-card-headline {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
}

.highlight-card-footer {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    /* Pins the "Read more" button to the bottom of the card regardless of how much (or
       little) room the title/meta above it take up - together with the title's fixed
       min-height below, this is what keeps every collapsed tile the same height even
       though titles vary from one to three lines. */
    margin-top: auto;
}

.highlight-chip {
    align-self: flex-start;
    background: var(--wine-soft);
    color: var(--wine);
    font-size: var(--fs-xs);
    font-weight: 600;
    letter-spacing: 0.07em;
    text-transform: uppercase;
    padding: 0.25rem 0.6rem;
    border-radius: 999px;
    /* A completely empty chip (no Regions value) has no text at all, so it gets no line
       box to size - as a flex item it's blockified, so unlike normal inline content it
       won't even get an empty "strut" line box, meaning line-height has nothing to act
       on. min-height reserves the same total (border-box) height regardless of content -
       1.5rem matches a chip with real text at this font-size/padding. */
    min-height: 1.5rem;
}

/* Always rendered (even with no Regions value) and just hidden rather than omitted, so
   its row still reserves the same height - otherwise a tile without a region reads as a
   different, shorter size than its row-mates that do have one. */
.highlight-chip-empty {
    visibility: hidden;
}

.highlight-title {
    font-size: 1.15rem;
    line-height: 1.3;
    margin: 0;
    /* Clamped to 2 lines (with a matching min-height for shorter titles) so every collapsed
       tile's headline takes up the same space whether the title is one line or five. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    min-height: 2.6em;
}

.highlight-meta {
    display: flex;
    gap: 0.85rem;
    font-size: var(--fs-sm);
    color: var(--slate);
}

.highlight-meta .dot {
    opacity: 0.5;
}

/* Hidden by default; revealed by the mechanism that actually uses it now - Featured
   Projects' above-row panel (.highlight-panel .expand-detail-text further below). The
   services grid used to have its own expand-in-place variant of this, but its tiles now
   link straight out instead of expanding at all. */
.expand-detail-text {
    display: none;
    color: var(--slate);
    margin-top: 0.25rem;
}

/* The one "Read more" / "Show less" text button, shared by all four places that have one:
   Featured Projects on Home, the Jobs rows, the References rows and the Services page's sector
   tiles. Each pairs it with its own behaviour class for the JS hook (.expand-toggle,
   .reference-card-toggle, .service-detail-toggle) - this class carries appearance only, so those
   stay free to differ in layout without any of them redefining what the button looks like.

   background and padding are reset explicitly. Neither used to be, which is the whole reason the
   Services page's button looked different: a bare <button> keeps the browser's own buttonface
   grey and default padding, and Bootstrap's reboot does not clear either (it only touches
   border-radius, cursor, appearance, font and margin). Services had its own rule that did clear
   them, so it alone rendered as the plain text link all four were meant to be. Don't drop these
   two lines on the grounds that "a button has no background" - it does. */
.highlight-link {
    background: none;
    padding: 0;
    border: none;
    font-size: var(--fs-md);
    font-weight: 600;
    color: var(--wine);
    text-decoration: none;
}

.highlight-link:hover {
    color: var(--wine-deep);
}

/* Layout, deliberately not in .highlight-link: both of these footers are flex columns, where the
   default align-items: stretch would otherwise make the button span the full card width and give
   it a click target far larger than its text. The Services tile's footer is a centred row and
   wants neither rule, which is exactly why this is separate.
   Kept on the behaviour classes rather than as .reference-row-footer .highlight-link so that
   .reference-row-open .reference-card-toggle above still wins on specificity and can flip an
   expanded row's button to flex-end - a two-class descendant rule here would have tied with it
   and won on source order instead, silently breaking that. */
.expand-toggle,
.reference-card-toggle {
    align-self: flex-start;
    margin-top: 0.25rem;
}

.see-all-btn {
    display: inline-block;
    font-size: var(--fs-lg);
    font-weight: 600;
    color: #fff;
    background: var(--wine);
    text-decoration: none;
    padding: 0.8rem 1.75rem;
    border: 1px solid var(--wine);
    border-radius: 999px;
    box-shadow: var(--shadow);
    white-space: nowrap;
    transition: background 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
}

.see-all-btn:hover {
    background: var(--wine-deep);
    border-color: var(--wine-deep);
    color: #fff;
    transform: translateY(-1px);
}

/* ---------- expand-above-row tiles: Featured Projects only ----------
   Clicking a tile dims it in place and opens a full-width panel with the same content,
   inserted directly above the row that tile belongs to - rather than the tile itself
   growing (see the services grid's expand-in-place mechanism above for that variant). */

.highlight-card-dimmed {
    opacity: 0.45;
    transform: none;
}

/* Pointer-only, see .highlight-card:hover above. These two exist only to neutralise a hover, so
   with hover gated off they are simply inert on touch rather than being needed there. */
@media (hover: hover) {
    .highlight-card-dimmed:hover {
        transform: none;
    }

    .highlight-card-dimmed:hover .highlight-card-media img {
        transform: none;
    }
}

.highlight-panel {
    display: none;
    flex: 1 1 100%;
    max-width: 100%;
}

.highlight-panel-open {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    column-gap: 1.25rem;
    row-gap: 0.75rem;
    padding: 1.5rem;
    background: var(--tile-open);
    border-radius: 3px;
    border-left: 3px solid var(--wine);
    box-shadow: 0 4px 10px rgba(36, 26, 32, 0.08), 0 24px 48px -24px rgba(36, 26, 32, 0.45);
}

/* On a phone the image, headline and countries have nowhere near enough width to sit beside
   each other - the wrap above turns into a cramped jumble rather than a row. Stack them
   instead, and let the thumbnail take the full width since there's no text beside it to
   balance against any more. */
@media (max-width: 575.98px) {
    /* nowrap is not optional here. The base rule sets flex-wrap: wrap for the row layout, and in
       a column container wrap does not mean "start a new line" - it means "start a new column to
       the right" once the content exceeds the container's height. This panel's height is
       constrained by the tile row site.js inserts it into, so the description wrapped into a
       second column beside the image and ran off the side of the screen, taking the page into
       horizontal scroll with it. */
    .highlight-panel-open {
        flex-direction: column;
        flex-wrap: nowrap;
        align-items: stretch;
    }

    .highlight-panel .highlight-card-media {
        flex: 0 0 auto;
        width: 100%;
        height: 180px;
    }

    /* Both of these carry a flex-basis chosen for the row layout above - 0% on the headline so
       long titles don't get measured at max-content width, 100% on the text so it takes a line
       of its own. Once the container is a column, flex-basis means *height* instead of width, so
       those same values stop meaning "share the row" and start constraining height: the text
       ended up with a 100% height basis it was then free to shrink out of, against a
       fixed-height image that could not, and collapsed to nothing. Auto basis lets both size to
       their content, which is all that's wanted when they're stacked. */
    .highlight-panel .highlight-card-headline,
    .highlight-panel .expand-detail-text {
        flex: 0 0 auto;
    }
}

/* A bigger thumbnail than the collapsed tile's - large enough to still read as the same
   photo, not just a logo-sized placeholder next to the title. */
.highlight-panel .highlight-card-media {
    flex: 0 0 160px;
    width: 160px;
    height: 120px;
    aspect-ratio: auto;
}

/* flex-basis:0 (not auto) so long titles don't get measured at their unwrapped
   max-content width when the wrap algorithm decides what fits on this line - that
   would push the whole headline onto its own row below the image instead of letting
   the title text wrap within the column beside it. Also lift the 2-line clamp inherited
   from the collapsed .highlight-title so the full title shows. */
.highlight-panel .highlight-card-headline {
    flex: 1 1 0%;
    min-width: 0;
}

.highlight-panel .highlight-title {
    display: block;
    -webkit-line-clamp: unset;
    overflow: visible;
    min-height: 0;
    font-size: 1.3rem;
}

.highlight-panel .expand-detail-text {
    display: block;
    flex: 1 1 100%;
    margin-top: 0.25rem;
    padding-top: 0.9rem;
    border-top: 1px solid var(--line);
}

.highlight-panel-footer {
    flex: 1 1 100%;
    display: flex;
    justify-content: flex-end;
}

/* ---------- home page: services grid ---------- */

/* Section heading sits above both columns, full-width, with a rule underneath separating
   it from the text+tiles row - matches the original site's own layout for this section
   (title text kept in the same plain ink color every other h2 on the site uses, though -
   not copying the original's blue/wine heading tint). */
/* Sits between the section heading and the tile grid. Rich text arrives as a run of <p>
   elements, so the spacing below is set here on the wrapper and the last paragraph's own
   margin is cleared, matching how .company-prose handles the same shape. */
.service-section-intro {
    margin-bottom: 2rem;
}

.service-section-intro p:last-child {
    margin-bottom: 0;
}

/* Overrides the global h1-h4 `text-wrap: balance` for this page's section headings only.
   balance equalises line lengths, which means giving room away from the first line - so a long
   heading breaks earlier than it needs to and looks like it ran out of space when it did not.
   `pretty` wraps greedily, filling each line, while still avoiding a single stranded word on
   the last one; where it is unsupported it falls back to plain greedy wrapping, which is the
   behaviour wanted here anyway, so it degrades in the right direction.
   Scoped to the three Services-page containers rather than applied globally: balance stays for
   Home's hero and every other page's headings, which were not part of this decision. The sector
   card titles inside the expand panels are deliberately not included - they are card titles
   rather than section headings. */
.service-section-heading h2,
.fwc-section h2,
.fwc-section h3,
.services-cta h2 {
    text-wrap: pretty;
}

/* .service-section-heading carried a `border-bottom: 1px solid var(--line)` until 2026-09-22.
   Removed so the Services page's section headings read the same as every other page's, which
   have no rule under them - see .company-section-heading, whose own comment used to cite this
   one as the deliberate exception. The markup's `pb-3` went with it: that padding existed only
   to hold the text off the rule, and left an unexplained gap once there was nothing to clear.
   The class stays because ServiceOverviewFull.cshtml still uses it as the text-wrap: pretty
   hook above. */

/* The login page's registration card before it is opened. The whole block is a click target
   (site.js forwards the click to the button inside it), so it takes the pointer that a button
   alone would have had - ADR 0028's "tile expands on click, button kept", same as the
   affiliated-company tiles. Hidden for good once the form is open, via the hidden attribute:
   nothing here sets display, so the UA stylesheet's [hidden] rule wins, unlike .map-consent-gate
   where a display:flex silently beat it. */
/* Reserves the Turnstile widget's own height before it exists. The Cloudflare script is
   async/defer and the widget is rendered by JS, so this container is 0px tall on first layout
   and gains ~65px (Cloudflare's normal widget is 300x65) whenever the render lands. Everything
   below it then moves down.
   That is a layout shift on all three forms, but on the login page's registration card it broke
   something outright: site.js scrolls the submit button to the bottom of the viewport when the
   card opens, measured at shown.bs.collapse - before the widget existed - so the button ended up
   one widget-height below the fold and the captcha sat where the button should have been.
   Reserving the space fixes the measurement at source rather than making the scroll chase a
   moving layout. If Cloudflare ever serves an interactive challenge here it will be taller than
   this and the shift returns; the fix then is to re-run the scroll once the widget has rendered,
   not to guess a bigger number. */
[data-turnstile] {
    min-height: 65px;
}

.register-teaser {
    cursor: pointer;
}

/* Once the form is open the teaser is ordinary copy again, not a control, so it gives the
   pointer back. The intro text itself stays - see site.js for why only the button goes. */
.register-teaser-open {
    cursor: auto;
}

/* ---------- services page: hero banner ---------- */

/* Reusable across every page that uses one (see Partials/HeroBanner.cshtml) - capped to a
   fixed height so every page's hero reads as the same size regardless of the picked image's
   own aspect ratio, cropped to fill via object-fit rather than left at natural height. The
   page title is overlaid as real text (see .hero-banner-overlay below), so this crop can no
   longer clip it. The crop is done by the browser, not the server - the rendered URL carries no
   crop instruction at all - so which half of the photo survives is decided purely by
   object-position. It defaults to dead centre here and is overridden inline, per image, from that
   image's backoffice focal point (CMS/Helpers/HeroImageFocalPoint.cs). Intro text sits on plain
   background below. */
/* Fixed heights per breakpoint rather than one height everywhere, and rather than an
   aspect-ratio: the point of a fixed height is that every page's hero reads as the same size
   whatever image an editor picks, and that intent survives if it's the same size *at a given
   width*. Two things this fixes at once, which is worth understanding because they look like
   opposing goals. 400px on a phone is most of the viewport, and it also crops hardest there:
   a 375x400 box is nearly square, so covering it with a landscape photo throws away about half
   the image's *width* - which is how baked-in title text loses its ends. Shortening the box
   makes it more landscape, closer to the photo's own ratio, so the crop shrinks to almost
   nothing. Smaller and less cropped, not a trade.
   Desktop still crops vertically (a 1400x400 box discards roughly half a 16:9 photo's height),
   so a source image whose subject is off-centre needs a focal point set on it in the backoffice.
   Its *title* needs nothing, which is the whole point of the overlay. */
.hero-banner-media img {
    width: 100%;
    height: 400px;
    object-fit: cover;
    display: block;
}

/* The overlaid page title. The headline is a CMS property (pageTitle, or heroTagline on Home)
   drawn over the photo, rather than pixels baked into it - see CMS/Helpers/HeroTitlePlacement.cs
   for why that changed. */
.hero-banner-media {
    position: relative;
}

/* --hero-title-inset is the gap from the photo's top or bottom edge, shared by both placements
   so Home's top-right title and every other page's bottom-left one cannot drift apart when one
   is adjusted later.
   There used to be a second token, --hero-title-edge-gap, adding 1.5rem of horizontal inset on
   top of the nested .container's gutter, because flush against a photo's edge the title read as
   crowded. That was removed on 2026-08-26: it put the title 24px inside the body content below,
   and sharing an exact edge with that content matters more than the breathing room. The title
   now starts from the container gutter and nothing else, so it lines up with whatever sits
   under the hero. Don't reintroduce a horizontal inset here without reopening that. */
.hero-banner-overlay {
    --hero-title-inset: 2.5rem;

    /* The scrim itself is --photo-scrim-stops at the top of this file, shared with the services
       sector tiles so the two cannot drift. A custom property can hold an arbitrary token
       sequence, so the whole gradient body lives there and each placement below supplies only its
       own direction. */

    position: absolute;
    inset: 0;
    display: flex;
    padding: var(--hero-title-inset) 0;
}

/* The scrim keeps a white headline legible over an editor-picked photo without asking the editor
   to choose a text colour per image - the guarantee ADR 0014 built this pattern for, and the
   reason it is weakened here rather than removed.
   Reworked 2026-09-22: it was rgba(36,26,32,0.75) falling linearly to 0 at 60%, and the title
   read as pasted onto the photo rather than as part of it. The band was the larger half of that.
   A uniform full-width rectangle owes nothing to what is in the picture, and Home is the worst
   case for one: that hero's top right, exactly where its title sits, is sky measuring 59%
   luminance against 38% for the image as a whole, so the scrim was darkening the brightest part
   of a bright photo in order to carry white text. Now weaker (0.40), running the full height
   instead of stopping at 60%, and eased so it has no perceptible end - with most of the
   legibility work moved onto .hero-banner-title's own shadow, which follows the letterforms
   instead of covering the photograph.
   This no longer matches .service-detail-media::before, which the previous comment here said it
   did. That one is deliberately left alone: it sits behind 1.4rem type on a small tile, where a
   band is not conspicuous, rather than behind a 4rem display heading on a full-bleed photo. */
.hero-banner-overlay-bottom-left {
    align-items: flex-end;
    background: linear-gradient(0deg, var(--photo-scrim-stops));
}

.hero-banner-overlay-top-right {
    align-items: flex-start;
    background: linear-gradient(180deg, var(--photo-scrim-stops));
}

/* An extra darkening layer for one photograph, stacked ON the shared scrim rather than replacing
   it - so --photo-scrim-stops stays exactly as it was tuned and the other six heroes are
   untouched. That separation is the point: the alternative, raising the shared token, veils every
   photograph on the site to fix one. HeroScrim.Strong carries the measurements and only the
   Company page passes it.
   Why a scrim here at all, when ADR 0031 says to raise the halo and never the scrim. That rule
   holds where it was written - the halo follows the letterforms and costs the photograph nothing
   outside them - but it has a limit, and this hero is past it. Rendering the alternatives over
   the real image settled it: at 14px the halo separated the type no better than at 8px and began
   to read as a smudge, because a halo is a thin rim and the problem here is a PATTERN the letters
   sit across - the Minster's roof ribs - rather than a background that is merely bright. Quieting
   the pattern is the only thing that works, and that is what a scrim does.
   The stops are deliberately short: +0.30 at the very bottom, gone by 45%, so the darkening stays
   under the title's own band and the upper two thirds of the photograph are exactly as before.
   Scoped to the bottom-left placement on purpose. The gradient direction is baked in at 0deg, so
   applying this to a top-right hero would darken the wrong end - the compound selector makes that
   a no-op rather than a silent mistake, and is also what wins it the source-order tie against the
   background shorthand above. */
.hero-banner-overlay-bottom-left.hero-banner-overlay-strong-scrim {
    background-image:
        linear-gradient(0deg,
            rgba(36, 26, 32, 0.30) 0%,
            rgba(36, 26, 32, 0.17) 20%,
            rgba(36, 26, 32, 0.06) 33%,
            rgba(36, 26, 32, 0) 45%),
        linear-gradient(0deg, var(--photo-scrim-stops));
}

.hero-banner-title {
    /* max-width in ch rather than rem so it tracks the font size at every breakpoint. Most
       titles are a single word; Home's tagline is the only one long enough to wrap, and it
       should wrap rather than run the full width of a 1400px photo. */
    max-width: 24ch;
    margin: 0;

    /* Not pure #fff: nothing in a photograph is fully white across an area that size, so #fff
       put the type outside the image's own tonal range and read as a cut-out laid on top. 6% of
       the photo shows through, which is enough to tie it to what is behind it and far too little
       to cost legibility.
       The shadow is what actually carries that legibility now, in place of most of the scrim
       (see .hero-banner-overlay-bottom-left). Two layers doing different jobs: a tight 2px one
       that holds the letter edges against fine detail, and a wide 44px one that pools a soft
       darkness around the whole headline. Because it follows the letterforms, the photograph is
       untouched everywhere else - which is the difference between type sitting in an image and
       type sitting on it. */
    color: var(--photo-title-color);
    /* Three layers, each doing a different job, and the tight one is what makes this work on a
       textured photo. Added 2026-09-22 after the Company hero was reported as unreadable while
       every other page was fine. Measured rather than guessed: the area behind that title is not
       brighter than Home's - it is darker (111/255 against 148) - but it carries 6.5x the edge
       energy (52.4 against 8.0), because it is aerial rooftops where Home is smooth sky. A wide
       diffuse shadow has nothing to bite against in high-frequency detail; it averages out. A
       tight halo creates contrast right at the letter edges, which is exactly where the
       competition is.
       Checked in both directions before choosing the value: at 8px/0.75 the halo is imperceptible
       against Home's sky, while at 10px/0.90 it starts reading as a dark outline around the type -
       the pasted-on look this whole pass exists to remove. If a future hero is busier still,
       raise this one value rather than the scrim: the scrim veils the photograph, the halo
       follows the letterforms. */
    text-shadow:
        0 0 8px rgba(36, 26, 32, 0.75),
        0 1px 2px rgba(36, 26, 32, 0.40),
        0 10px 44px rgba(36, 26, 32, 0.55);

    /* 450, not 600, and this is the change that actually did the work. Softening the scrim on
       its own was measured at 22/255 behind Home's title - about 9% of full range - and was
       barely perceptible; rendering the four variants side by side showed why. The band was
       never the main signal. A large, heavy, stark-white headline is what reads as applied to a
       photograph, because no photograph contains anything like it. Dropping the weight and
       warming the white is what makes the type belong to the picture.
       450 rather than 400 because the shadow now carries most of the legibility and very light
       type over a busy photo gives it too little to hold on to. It is a real interpolated
       weight, not a synthesised one - the @font-face at the top of this file declares Work Sans
       across 400-700, so anything in that range is a genuine instance.
       This rule is bespoke and is NOT display-5, so no other heading on the site follows it -
       which is what keeps the matched-heading convention in BACKLOG.md intact. */
    font-weight: 450;
    font-size: 4.05rem;
    line-height: 1.2;

    /* Last resort, below the width at which shrinking the type can still keep a long single word
       inside the box (see the smallest breakpoint). Breaking a word mid-way looks bad, but it
       never puts a horizontal scrollbar on the page, which an overflowing unbreakable word would. */
    overflow-wrap: break-word;
}

/* Pulled in from the left edge it sits against. Margin rather than padding so the extra gap
   comes off the block's position, not out of its max-width text measure. */
/* No horizontal margin: the title's left edge is the container's own gutter, which is what puts
   it in line with the body content below. */
.hero-banner-overlay-bottom-left .hero-banner-title {
    margin-left: 0;
}

/* Home only - it is the sole top-right hero. The auto left margin pushes the block to the
   container's right edge; without it the block would sit left with its text merely aligned right
   inside it. margin-right is 0 so that edge is the container's own gutter, in line with the
   content below.
   The text is right-aligned, so every line ends flush on the container's right edge and lines up
   with the content below. Left-aligning it was tried on 2026-08-26 and reverted: with the block
   hugging the right, left-aligned text can only reach that edge on its longest line, and the
   rest fall short. Measured at 2560px, the longest line stopped 248px short - text-wrap: balance
   (inherited from the h1-h4 rule) evens the lines out and accounts for ~92px of it, the rest is
   just where the words break inside a 24ch box. width: fit-content does not help either, since
   it resolves to the max-content width clamped by max-width rather than the width the text
   occupies after wrapping - CSS cannot shrink a wrapped block to its longest rendered line.
   Only JS re-measuring on resize could, which is not worth it for a hero title. */
.hero-banner-overlay-top-right .hero-banner-title {
    margin-left: auto;
    margin-right: 0;
    text-align: right;
}

@media (max-width: 991.98px) {
    .hero-banner-media img {
        height: 280px;
    }

    .hero-banner-overlay {
        --hero-title-inset: 1.75rem;
    }

    .hero-banner-title {
        font-size: 3.15rem;
    }
}

@media (max-width: 575.98px) {
    .hero-banner-media img {
        height: 200px;
    }

    /* 200px of photo has to hold the inset twice over plus up to two lines of title, so it comes
       down here. There is no horizontal equivalent any more - the title sits on the container
       gutter at every width, which is exactly what keeps it aligned with the content below. */
    .hero-banner-overlay {
        --hero-title-inset: 1.25rem;
    }

    /* Below this width the type shrinks rather than overflowing, deliberately past the point of
       being readable - a window hand-dragged to 200px is not a case worth designing for, it just
       must not break the layout. 11.7vw is exactly 2.34rem at 320px, the narrowest real phone, so
       every actual device keeps the tuned size and only a resized window ever shrinks. Shrinking
       with the viewport also keeps roughly the same characters per line, so the wrapped block's
       height comes down in step - which is what stops a long title spilling out of a 200px-tall
       photo, not just off its right edge. */
    .hero-banner-title {
        font-size: min(2.34rem, 11.7vw);
    }
}

/* ---------- account pages: login / register / forgot password / reset password ---------- */

/* These four pages are the sparsest on the site - two, five, one and two fields - so the layout
   has to make a small form read as deliberate rather than stranded. Two things do that: the form
   sits in a card with the same frame as .reference-row and .job-row, so it reads as an object
   placed on the page rather than bare fields floating on the background, and the column below is
   capped so the fields keep a readable measure.
   A shorter "compact" hero band used to be the other half of this, at roughly 55% of the standard
   heights. It was removed on 2026-09-24 once every account page had moved back to the standard
   band - Login and My Particip first, then Forgot and Reset password. Don't reintroduce it
   without reading that history: the rule it needed had the same specificity as the standard one
   and won on source order alone, which is exactly the kind of dependency that breaks silently.
   34rem, not the 30rem the inline max-width used to be: the card's own padding would otherwise eat
   into the fields, narrowing every input by that padding on both sides. This keeps the inputs at
   the width they had before the card existed. */
.account-column {
    max-width: 34rem;
}

/* Login only, which is the one account page with two cards. Wider than .account-column because two
   of them sit side by side from lg up, but still capped: at full container width each column would
   be around 640px, which is far too wide a measure for a two-field form. 62rem puts each column at
   roughly the width a single card had, so neither reads as stretched. */
.account-column-pair {
    max-width: 62rem;
}

/* Intro text inside these cards is deliberately NOT .lead, unlike everywhere else on the site.
   Two reasons, and both only became visible once the login page put two cards side by side. .lead
   is 1.25rem at weight 300, which is a page-level treatment - on a long paragraph inside a ~450px
   card it reads as oversized rather than as a lead-in. And it made the body text larger than
   .account-secondary h2 above it, i.e. a heading smaller than the text it introduces. The register
   page's own intro, reused in the invitation card, arrives as plain rich text with no class at all,
   so matching downwards is also the only way the two cards agree without restyling authored
   content. */
.account-card {
    background: var(--tile);
    border: 1px solid var(--line);
    border-radius: 6px;
    padding: 1.75rem;
    box-shadow: var(--shadow);
}

/* The second card on the login page - the "need an account" invitation. A separate card rather
   than a block inside the form card, so it reads as an alternative to signing in rather than part
   of the sign-in form itself. It carries no margin of its own: the two cards are grid columns now,
   so the row gutter spaces them both horizontally when side by side and vertically once stacked. */
.account-secondary h2 {
    margin-bottom: 0.5rem;
    color: var(--wine);

    /* Only just above the 1rem body text it introduces, so it leans on weight and colour to read
       as a heading. 1.05rem was set when that body was .lead at 1.25rem, which made this heading
       smaller than its own paragraph. */
    font-size: 1.15rem;
    font-weight: 600;
}

/* The invitation's body is the register page's own intro text, authored as rich text and so
   arriving wrapped in a paragraph that carries Bootstrap's bottom margin. */
.account-secondary p:last-child {
    margin-bottom: 0;
}

.hero-banner-content {
    padding: 2.5rem 1rem;
}

/* ---------- services page: full-mode service cards ---------- */

/* Same section heading treatment as the Home teaser's .service-section-heading, reused here
   by Partials/ServiceOverviewFull.cshtml so both pages' headings look identical. */

/* Brighter band for the "thematic and sectoral perspective" section specifically (see
   ServiceOverviewFull.cshtml's AlignTextLeft check) - every section gets the plain --paper
   page background by default, this one distinguishes itself with the same --surface-soft
   tint used elsewhere for a section band (.home-band-alt/.fwc-section/.contact-office-section). */
.service-overview-bright {
    background: var(--surface-soft);
}

/* flex column: image on top, .service-detail-collapsed-footer below it. Bordered for the same
   reason as .contact-office-card: the "thematic and sectoral" section renders on a
   .service-overview-bright band, so this tile cannot rely on --tile differing from it. */
.service-detail-row {
    background: var(--tile);
    border: 1px solid var(--line);
    border-radius: 3px;
    overflow: hidden;
    box-shadow: var(--shadow);
    display: flex;
    flex-direction: column;
}

/* Fills its tile's full width - the tile itself is now sized down to 4-per-row (see
   .service-detail-tile), which is what used to make the image look "half width" against its
   old 2-per-row card. Height is a fixed min-height (not aspect-ratio) so it doesn't shrink
   along with the width. */
.service-detail-media {
    /* 240px, raised from 180px when the grid went from 4 tiles per row to 3 (see
       ServiceOverviewFull.cshtml): the wider tiles left the photo looking letterboxed at the old
       height. Because this is a min-height rather than an aspect-ratio, it does not track tile
       width on its own - so it needs revisiting by hand if the tiles-per-row ever changes again. */
    min-height: 240px;
    display: flex;
    align-items: flex-start;
    position: relative;
    overflow: hidden;
}

/* The photo was a background-image until 2026-09-23 and is now a real <img>, so that it can
   carry loading="lazy" and a srcset - a CSS background can do neither, and all twelve tiles on
   the Services page were loading eagerly at one fixed size as a result.

   Absolutely positioned rather than in flow, so the tile keeps taking its height from the
   min-height above exactly as it did when the photo was a background. object-fit/object-position
   reproduce background-size: cover and background-position: center one for one.

   THE Z-INDEXES ARE LOAD-BEARING, and this is the one thing that breaks if it is edited
   carelessly. ::before is the scrim and would previously paint over a background; against a real
   child element it would paint *under* it, because generated content comes before the element's
   own children in paint order. So the three layers are ordered explicitly: photo, then scrim,
   then title. Remove any one of these and the scrim either vanishes behind the photo or covers
   the title. */
.service-detail-media img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: center;
    z-index: 0;

    /* **The photograph is deliberately subordinate here, and this is the whole treatment.** On a
       collapsed tile the title is the message and the photo is decoration; three rounds of
       raising halos and scrims failed because they all fought the picture for parity instead of
       settling the hierarchy. Blurring removes the competing detail *at source* - and detail is
       exactly what the measurements blamed, edge energy running to 57 on the protest crowd - so
       the type then needs only a light shadow rather than a double halo. See ADR 0032's second
       update.
       **3px, not the 1.2px this started at, and the reason is uniformity rather than darkness.**
       A lightly blurred photograph still has light and dark patches, so white letters cross
       both - which is what "the writing still conflicts with the background" meant, and why
       darkening the tile further never helped. At 3px the local variation behind the type is
       gone while the subject stays recognisable; 6px and 10px were rendered too and read as
       increasingly abstract colour fields, losing the thing the photographs are there for.
       saturate() rather than more blur: it drops the shout of the brightest photos without
       taking the mood, which is what makes the section still read as photographs.
       scale(1.08) is NOT a zoom - blur() feathers an element's own edges, and this tile clips
       with overflow:hidden, so the image is overscaled just enough for the soft rim to fall
       outside. It rose with the blur: 3px needs roughly three times the margin 1.2px did, and
       the short axis is what binds (240px tall, so 8% buys about 9px a side). Raise the blur
       again and this has to follow, or the corners look like a rendering bug. */
    filter: blur(3px) saturate(0.6);
    transform: scale(1.08);
    transition: transform 0.5s ease, filter 0.5s ease;
}

/* Hover restores the photograph and zooms, matching .highlight-card's own +0.04 and its
   0.5s ease exactly rather than inventing a second animation - 1.06 to 1.10 is the same
   movement as Featured Projects' 1.00 to 1.04.
   **The scrim deliberately does NOT lift with it.** Un-blurring already gives the photograph
   its moment; lightening the ground as well would hand back the legibility problem this
   treatment exists to solve, at the one moment the visitor is looking closely. */
/* Pointer-only, see .highlight-card:hover. This is the family the latch was reported on, and the
   one where gating it costs nothing: on touch the photograph simply stays blurred, which is the
   resting state the whole treatment above is built around. */
@media (hover: hover) {
    .service-detail-tile:hover .service-detail-media img {
        filter: none;
        transform: scale(1.12);
    }

    /* A dimmed tile is one whose sibling is expanded, so it should not respond - same rule, and
       the same reasoning, as .highlight-card-dimmed. */
    .service-detail-tile-dimmed:hover .service-detail-media img {
        filter: blur(3px) saturate(0.6);
        transform: scale(1.08);
    }
}

/* Scrim behind the title so it stays legible over any photo, without needing a per-image
   editor-chosen text colour - darkens the top, where the title sits.
   **Now the same treatment as the page heroes**, sharing --photo-scrim-stops so the two cannot
   drift. It was briefly left on the old heavy `rgba(36,26,32,0.75)` band when the heroes were
   softened on 2026-09-22, on the reasoning that a band is less conspicuous behind 1.4rem type on
   a small tile than behind a display heading on a full-bleed photograph. That reasoning was
   sound and the call was still reversed the same day, deliberately: the site should not carry two
   answers to one question, and mocking the tiles at their real 400x240 size showed the lighter
   treatment reads better here too - legible over aerial rooftops, and the photograph no longer
   muddied across its top third. See ADR 0032, which supersedes that consequence of ADR 0031. */
.service-detail-media::before {
    content: "";
    position: absolute;
    inset: 0;

    /* Above the photo, below the title - see the z-index note on .service-detail-media img. */
    z-index: 1;

    /* A scrim of the tiles' own, NOT --photo-scrim-stops. The tiles shared the heroes' scrim
       from 2026-09-22 until 2026-09-23, on the principle that the site should not carry two
       answers to one question. Reverted deliberately, because it is two questions - see
       ADR 0032.

       **The hero treatment does not transfer, and the reason is proportion, not taste.** A hero
       title sits on a small part of a large photograph, so it can be placed over a calm corner
       and a light touch is enough to let it sit *in* the picture. A tile title covers roughly
       45% of its image - there is no calm corner to find, and nothing for the type to sit in.
       It needs a real ground.

       Measured over the title region of all twelve photographs at the rendered 400x240 size:
       luminance runs 21 to 200 and edge energy 3.7 to 57.0. Three are genuinely hostile -
       "Institutional and organisational development" at 170/38.8 is pale AND busy at once,
       which is why no single lever ever rescued it.

       **Near-uniform, and no band.** A band behind the title was rendered and rejected: at a
       fixed 46% of the tile it reads as a seam across the picture, and sized to hug the text
       instead it becomes a caption plate - either way the tile stops being one image. With the
       blur above carrying legibility, an even ground is all this needs to do.
       Barely graded (0.55 to 0.45) rather than flat, so the tile keeps some depth. Raising it
       is not the lever if the type is ever hard to read again - the blur is. */
    background: linear-gradient(180deg,
        rgba(36, 26, 32, 0.55) 0%,
        rgba(36, 26, 32, 0.45) 100%);
}

.service-detail-media-title {
    position: relative;

    /* Above both the photo and the scrim - see the z-index note on .service-detail-media img.
       position: relative alone was enough while the photo was a background; it is not now. */
    z-index: 2;

    /* The ground behind the type is the band on ::before, not a background here. This shadow is
       the mockup's - soft and small, because the band is what carries legibility now. */
    text-shadow: 0 1px 4px rgba(36, 26, 32, 0.55);

    /* Pure white rather than --photo-title-color: on an opaque band, plain contrast beats a
       warm off-white that was chosen for type sitting inside a photograph. */
    color: #fff;

    /* 600, not the 500 the hero treatment brought with it. Small type over a photograph needs
       more presence than a display heading does. */
    font-weight: 600;

    /* **min-width: 0 is load-bearing and is not a tidying-up line.** This is a flex item, and a
       flex item's automatic minimum size is its min-content width - here the longest word in the
       title. At phone width a col-6 tile leaves roughly 140px for text, and "organisational",
       "communication" and "Development" are all wider than that at this size, so the tile
       refused to shrink, the row grew past the viewport, and the whole right-hand column was
       cut off - "Read more" panel included. It read as a text-clipping bug and was a horizontal
       overflow one.
       overflow-wrap then lets the word itself break rather than spill, and hyphens: auto breaks
       it at a sensible point instead of mid-syllable - which works because master.cshtml sets
       <html lang> per culture, so the browser hyphenates by the right language. */
    min-width: 0;
    overflow-wrap: break-word;
    hyphens: auto;

    /* 1.54rem is 1.4rem +10%, raised on 2026-09-23 because the title is the message on a
       collapsed tile and was carrying it at the same size as ordinary body copy. The photograph
       is treatment rather than content here, so the type can afford the room. */
    font-size: 1.54rem;
    line-height: 1.3;
    padding: 1rem 1.25rem;
}

/* ---------- services page: expandable sector card ----------
   Every card (see Partials/ServiceOverviewFull.cshtml) uses this - same "dim tile + panel
   above its row" mechanic as the Home page's Featured Projects tiles (.highlight-card/
   .highlight-panel), but with its own class names since this grid is Bootstrap-column-based
   rather than a plain flex grid. The name also avoided a clash with .service-card, the compact
   Home teaser tile, which was removed along with that teaser on 2026-09-18. */

.service-detail-collapsed-footer {
    padding: 1.25rem;
    display: flex;
    align-items: center;
    justify-content: center;
}

/* Matches the panel's "See our references" button color - reads as the sector's own
   headline once expanded, rather than repeating the media overlay title verbatim with no
   distinction. It used to carry padding-right: 7rem to stay clear of a "Back to Home" link
   pinned to the panel's top-right corner; that link went with the Home services teaser on
   2026-09-18, so the heading gets its full width back. */
.service-detail-panel-title {
    color: var(--wine);
    margin-bottom: 0.75rem;
}

/* Only shown on the one card reached via a Home tile's own ?expand= link - same idea as
   References' "Back to map"/"Back to services" buttons, but corner-positioned (like the World
   Map's own .map-projects-close) rather than inline, since this panel already has its own
   toggle-driven "Show less" control for the same-page collapse case; this is purely for
   navigating back to a different page. */
/* Sector copy is authored as rich text, and currently arrives with the brand magenta baked into
   every heading as an inline style - color: rgb(151, 78, 131), which is literally --wine/
   --logo-magenta. Setting it here instead lets the authored HTML be plain h4/h5/p: far less
   markup to reproduce when the copy is translated, one place to change if the brand colour ever
   moves, and no inline style out-competing the stylesheet the way the pasted download titles
   still do. Matches .service-detail-panel-title above, which already takes the same token.
   Scoped to the panel because ServiceOverviewFull.cshtml is the only place this text renders.
   Note h5 is deliberately absent from the site's global h1-h4 heading rule, so it keeps
   Bootstrap's slightly lighter weight - that reads as the intended level below h4. */
/* The expertise list, tightened from a default list because it is a scan-list of headings rather
   than running prose. Two selectors on purpose, and the second is the load-bearing one: a bare ul
   inside the panel has to look identical to one carrying the class.
   The original reason was that twelve of the then-thirteen French sectors had been authored as a
   plain <ul> with no class, so they rendered as an untouched default list. That is no longer true -
   the set is twelve, and as of 2026-09-21 both cultures were re-pasted from
   service-sector-texts-en-fr.txt, so English and French now carry identical markup. The rule stays
   anyway, for the reason that never depended on that history: an authored list carries no class at
   all, and Tiptap is free to drop an unknown class the next time an editor opens the field, which
   would otherwise silently lose this styling with nothing to point at. */
.service-detail-expertise,
.service-detail-panel ul {
    margin-top: 0.75rem;
    margin-bottom: 0;
    padding-left: 1.25rem;
}

.service-detail-expertise li,
.service-detail-panel ul li {
    margin-bottom: 0.35rem;
}

.service-detail-expertise li:last-child,
.service-detail-panel ul li:last-child {
    margin-bottom: 0;
}

/* Tiptap wraps each list item's text in its own <p>, which the French sectors carry. Bootstrap's
   own p margin-bottom would otherwise double every gap in the list and leave one hanging after
   the last item. Same flattening as .download-link p on the Company page. */
.service-detail-panel li > p {
    margin: 0;
}

.service-detail-panel h4,
.service-detail-panel h5 {
    color: var(--wine);
}


/* .service-detail-toggle is now a behaviour hook only - site.js finds the button by it, and the
   panel-footer rule further down reorders it. Its appearance comes from .highlight-link, which
   the element also carries; the duplicate visual rule that used to live here is what made this
   button the odd one out. */

.service-detail-tile-dimmed .service-detail-row {
    opacity: 0.45;
}

/* The position:relative that used to sit here anchored a "Back to Home" link to this panel's
   own top-right corner rather than to the page. That link was removed with the Home services
   teaser on 2026-09-18 and nothing inside this panel is absolutely positioned now, so it went
   too - put it back if anything ever needs to position against the panel again. */
.service-detail-panel {
    display: none;
}

.service-detail-panel-open {
    display: block;
    padding: 1.5rem;
    background: var(--tile-open);
    border-radius: 3px;
    border-left: 3px solid var(--wine);
    box-shadow: 0 4px 10px rgba(36, 26, 32, 0.08), 0 24px 48px -24px rgba(36, 26, 32, 0.45);
}

/* The toggle button (see site.js) moves into this footer while the panel is open - matching
   Featured Projects' own .highlight-panel-footer pattern, one button/one source of truth
   rather than a separate close control. Inserted as the footer's first child so it lands on
   the left, with the "See our references" link on the right. */
.service-detail-panel-footer {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-top: 1rem;
}

/* Stacked on a phone, where the two buttons side by side leave each barely wide enough for its
   own label. The explicit order matters: site.js *prepends* the toggle into this footer
   (insertBefore firstChild, see the comment there) so it sits to the left of the see-all button
   on wide screens - without this it would therefore stack on top of it, which is the opposite
   of what's wanted. */
@media (max-width: 575.98px) {
    .service-detail-panel-footer {
        flex-direction: column;
        align-items: stretch;
        gap: 0.5rem;
    }

    .service-detail-panel-footer .service-detail-toggle {
        order: 2;
    }
}

/* ---------- services page: framework contract (FWC/SEA) section ---------- */

.fwc-section {
    background: var(--surface-soft);
}

/* "Get involved" band closing the Services page. Deliberately NOT --surface-soft: it sits
   directly below .fwc-section, and matching it would merge the two into one continuous band
   with no boundary. The page's default background gives the alternation every other section
   on the site has. Text only - ctaImage stays unread, so there is no photo composition here. */
.services-cta {
    background: var(--paper);
}

/* The lots band. Same --magenta as .map-band and .company-facts-band, and the two light values
   below are the same literals those use for figure and body text on that ground. There is no
   token for them yet - worth introducing one if a fourth magenta surface ever appears, since
   three places now repeat the same two hex values. */
.fwc-lots-band {
    background: var(--magenta);
    color: var(--band-text);
}

/* The member-lot intro sits on its own soft band BETWEEN the two magenta ones, rather than
   inside them, so the run of lots is broken up instead of reading as one long magenta block. */
.fwc-lots-intro-band {
    background: var(--surface-soft);
}

.fwc-lot-list {
    gap: 2.5rem;
}

/* Number over title over description, each on its own line - replacing the old number-divider-
   title row in --wine. The number and title are both monospace, which is what visually ties
   them to the figures on the site's other two magenta bands. */
.fwc-lot-number {
    font-family: var(--font-mono);
    font-size: clamp(2rem, 3.5vw, 2.75rem);
    font-weight: 400;
    line-height: 1.1;
    color: var(--band-heading);
}

.fwc-lot-title {
    font-family: var(--font-mono);
    font-size: var(--fs-lg);
    font-weight: 400;
    letter-spacing: 0.07em;
    line-height: 1.4;
    text-transform: uppercase;
    color: var(--band-heading);
    margin-top: 0.4rem;
}

/* Body font rather than monospace, and capped: these run long, and a full-width measure on a
   coloured ground is markedly harder to read than the same text on paper. */
.fwc-lot-description {
    color: var(--band-text);
    font-size: 1rem;
    line-height: 1.6;
    margin-top: 0.9rem;
    max-width: 58rem;
}

/* ---------- world map band ---------- */

/* 3rem top and bottom is Bootstrap's own py-5, which every other band on the site carries as a
   utility class in the markup. Matched here in CSS rather than by adding py-5 to the section,
   because this band also needs the background and colour below and splitting its box model
   across two places is how the two drift. Was 5.5rem until 2026-09-22, which made the heading
   sit 88px from the band's top edge against 48px everywhere else - see .highlights-intro above,
   which had the same problem from a different cause and was corrected in the same pass. */
.map-band {
    background: var(--magenta);
    color: var(--band-text);
    padding: 3rem 0;
}

/* Overrides Bootstrap's own responsive .container max-width (up to 1320px at the xxl
   breakpoint) so .map-layout's own max-width below isn't clipped back down by it. Higher
   specificity than Bootstrap's plain `.container` breakpoint rules (2 classes vs 1), so this
   wins regardless of stylesheet order without needing !important. */
.map-band .container {
    max-width: 1450px;
}

.map-head {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    gap: 2.5rem;
    flex-wrap: wrap;
    margin-bottom: 2.5rem;
}

.map-head-text {
    max-width: 62ch;
}

.map-head h2 {
    color: var(--band-heading);
}

.map-head p {
    color: var(--band-text);
    font-size: var(--fs-lg);
    margin-top: 0.85rem;
}

.map-stats {
    display: flex;
    gap: 2.5rem;
    flex-wrap: wrap;
}

.map-stats div {
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
}

/* Shared by Home's world-map band and the Company page's Facts & Figures grid. Both show a
   large mono figure over a small uppercase label on the same magenta ground and are meant to
   look identical, so the type treatment is defined once rather than copied - the same reasoning
   as the .reference-row frame shared by References and Jobs. Only the layout differs per page
   (.map-stats is a single row, .company-facts a 2-column grid), which is why the layout lives
   on those classes and not here. Was .map-stats .mono / .map-stats span.map-stat-label until
   2026-09-17, when the Company page became the second call site. */
.stat-figure {
    color: var(--band-heading);
    font-weight: 600;
}

.stat-label {
    letter-spacing: 0.07em;
    text-transform: uppercase;
    color: var(--band-muted);
}

/* Size is deliberately NOT shared, and for the label neither is colour. The map's stats sit in
   a narrow column beside the map, where 1.6rem is right; the Company band is full width with
   six figures as its entire content, where that value reads as an afterthought. The map also
   dims its labels to a dusty pink beneath brighter figures, where the Company band wants them
   the same near-white, per the design it was built from.
   So what actually stays shared is the figure's colour and weight, and the label's uppercase +
   letter-spacing treatment. That is thinner than when these were extracted on 2026-09-17, and
   is worth knowing before assuming a change here reaches both places - the label now only
   shares its shape, not its colour or size. */
.map-stats .stat-figure {
    font-size: 1.6rem;
}

.map-stats .stat-label {
    font-size: var(--fs-xs);
}

.company-facts .stat-figure {
    font-size: clamp(2.25rem, 3.5vw, 3rem);
    line-height: 1.05;
}

.company-facts .stat-label {
    font-size: 1.1rem;
    color: var(--band-heading);
}

.map-layout {
    display: flex;
    gap: 1.5rem;
    align-items: flex-start;
    max-width: 1430px; /* was 1100px - ~30% bigger, per request */
    margin: 0 auto;
}

.map-main {
    flex: 1 1 62%;
    min-width: 0;
}

.world-map {
    width: 100%;
    aspect-ratio: 2.1 / 1;
    min-height: 380px;
    max-height: 832px; /* was 640px - scaled with .map-layout's ~30% width increase, or the map would overflow this box's old height cap at the new width */
}

.world-map svg {
    width: 100%;
    height: auto;
}

.map-projects {
    position: relative;
    flex: 1 1 38%;
    min-width: 0;
    background: rgba(253, 246, 249, 0.06);
    border: 1px solid rgba(253, 246, 249, 0.15);
    border-radius: 4px;
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

.map-projects[hidden] {
    display: none;
}

/* Sits on the aside itself (not the scrolling body below), so it stays in view
   no matter how far the project list is scrolled. */
/* --band-text, the bands' own body-copy tone, on the principle that the control which dismisses
   this panel should not be fainter than the content inside it. It was --band-muted - supporting
   text by definition, and still correct for .stat-label, .map-project-meta and the loading and
   empty states. A control is not supporting text, which is the whole reason this one
   disappeared. Don't move it back for consistency with its neighbours - the divergence is
   deliberate, and it is the worked example named in the --band-muted note at the top of this
   file. Measured against the panel's own effective background (#9d588a - --magenta under the 6%
   near-white wash .map-projects lays over it, so slightly lighter than the band itself): it sat
   at 2.01:1, below the 3:1 WCAG asks of a non-text UI component. --band-text is 4.23:1 there,
   and the --band-hover hover keeps a real step above it at 5.00:1.
   top/right are deliberately untouched: those two-decimal values are hand-tuned against the
   panel's corner, so they are the dials if the larger glyph ever looks a pixel out - not
   something to re-derive. */
.map-projects-close {
    position: absolute;
    top: 0.10rem;
    right: 1.00rem;
    z-index: 1;
    background: none;
    border: none;
    font-size: 1.6rem;
    line-height: 1;
    color: var(--band-text);
    cursor: pointer;
    padding: 0.25rem;
}

.map-projects-close:hover {
    color: var(--band-hover);
}

/* The actual scroll container - the extra right padding (beyond the aside's own
   former padding) keeps content clear of both the close button and the scrollbar. */
.map-projects-body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    padding: 1.25rem 2.625rem 1.25rem 1.375rem;
}

.map-projects-loading,
.map-projects-empty {
    color: var(--band-muted);
    font-size: var(--fs-md);
    margin: 0;
}

.map-projects-title {
    font-family: var(--font-display);
    font-weight: 600;
    color: var(--band-heading);
    margin: 0 0 0.6rem;
    font-size: 1.05rem;
}

.map-projects-see-all {
    display: inline-block;
    font-size: var(--fs-sm);
    font-weight: 600;
    color: var(--band-heading);
    text-decoration: none;
    border-bottom: 1px solid rgba(253, 246, 249, 0.4);
    margin-bottom: 1rem;
}

.map-projects-see-all:hover {
    color: var(--band-hover);
    border-color: var(--band-hover);
}

.map-project-row {
    display: block;
    padding: 0.65rem 0;
    border-bottom: 1px solid rgba(253, 246, 249, 0.12);
    text-decoration: none;
    color: inherit;
    cursor: pointer;
}

.map-project-row:hover .map-project-title {
    color: var(--band-hover);
    text-decoration: underline;
}

.map-project-row:last-child {
    border-bottom: none;
}

.map-project-title {
    color: var(--band-heading);
    font-size: var(--fs-md);
    font-weight: 600;
    line-height: 1.35;
    margin: 0 0 0.25rem;
}

.map-project-meta {
    color: var(--band-muted);
    font-size: var(--fs-sm);
}

@media (max-width: 860px) {
    .map-layout {
        flex-direction: column;
    }

    .map-main,
    .map-projects {
        flex: 1 1 auto;
        width: 100%;
    }

    .map-projects {
        max-height: 320px !important;
    }
}

@media (max-width: 767.98px) {
    .map-band {
        display: none;
    }
}

.world-map-tooltip {
    position: absolute;
    pointer-events: none;
    background: #fdf6f9;
    color: var(--wine-deep);
    padding: 0.5rem 0.75rem;
    border-radius: 4px;
    font-size: var(--fs-md);
    font-weight: 600;
    opacity: 0;
    z-index: 10;
}

.map-legend {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    margin-top: 1.25rem;
    font-size: var(--fs-sm);
    color: var(--band-muted);
}

/* These two stops must stay equal to worldmap.js's COLOR_FEW_PROJECTS and COLOR_MANY_PROJECTS -
   the legend is a claim about what the map draws, and nothing checks it. They had drifted to
   #fce1ec/#8a5468 (close enough to look right, not the real ramp) and were resynced 2026-09-22.
   Note the legend describes the ramp only: no-data countries are neutral grey and deliberately
   off it, with no swatch here - offered and declined, since a swatch needs its own dictionary
   key in all three languages. */
.map-legend-bar {
    width: 160px;
    height: 8px;
    border-radius: 999px;
    background: linear-gradient(90deg, #ffd9f0, #692f5a);
}

/* The borders disclaimer. --band-text rather than the --band-muted its neighbours use,
   and that is deliberate: this is the one line on the band whose entire purpose is to be
   legible if anyone does read it, so it keeps AA (4.75:1) where --band-muted would put a
   legal statement at 2.25:1 AND at the smallest size on the site. Size alone makes it
   subordinate - the same split the reference-row code note describes, applied the other
   way round, because there the colour was free to recede and here it is not.

   It spans the full band width rather than being capped to a reading measure, so its right edge
   lines up with the map above it instead of stopping somewhere arbitrary. That is as far as it
   goes: DON'T ADD white-space: nowrap to force one line. Measured at --fs-xs against this
   container's 1450px (1426px inside the gutter), in DejaVu Sans, which is WIDER than Work Sans so
   these are upper bounds: English 1259px fits, French 1454px does not, Spanish 1518px does not.
   So a single line is not available in all three languages even on the widest desktop, and below
   about 1450px of viewport none of them fit - nowrap would then push the page into horizontal
   scroll, which is what cost .highlights-stat its own nowrap and what cut the right-hand column
   off the sector tiles in round 8. English landing on one line and the other two on two lines is
   the accepted outcome. */
.map-disclaimer {
    margin: 1rem 0 0;
    font-size: var(--fs-xs);
    line-height: 1.5;
    color: var(--band-text);
}

/* Sitewide (see master.cshtml) - hidden until site.js toggles .is-visible once the page has
   scrolled past a threshold, so it never appears on a page too short to need it without any
   per-page opt-in logic. */
.scroll-to-top-btn {
    position: fixed;
    right: 1.5rem;
    /* --cookie-banner-height is kept in sync with the cookie banner's real rendered height by
       cookie-consent.js, the same pattern site.js already uses for --navbar-height - 0px
       whenever the banner isn't showing, so this collapses back to a plain 5rem then. */
    bottom: calc(5rem + var(--cookie-banner-height, 0px));
    width: 44px;
    height: 44px;
    border: none;
    border-radius: 50%;
    background: var(--magenta);
    color: #fff;
    font-size: 1.15rem;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.25);
    opacity: 0;
    visibility: hidden;
    transform: translateY(8px);
    transition: opacity 0.2s ease, transform 0.2s ease, visibility 0.2s, bottom 0.2s ease;
    z-index: 1030;
}

.scroll-to-top-btn.is-visible {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
}

.scroll-to-top-btn:hover {
    background: var(--wine-deep);
}

/* Fixed bottom bar - shown by cookie-consent.js on first visit (no stored decision yet) or
   via the footer's "Cookie settings" link, hidden again once Accept/Decline is clicked. */
.cookie-consent-banner {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 1040;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: 1rem 1.5rem;
    background: var(--ink);
    color: #fff;
    box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.25);
}

.cookie-consent-banner a {
    color: #fff;
    text-decoration: underline;
}

.cookie-consent-message {
    flex: 1 1 320px;
    font-size: var(--fs-md);
}

.cookie-consent-actions {
    display: flex;
    gap: 0.75rem;
    flex-shrink: 0;
}

/* Placeholder shown in place of a Google Maps iframe until the visitor consents - see
   ContactPage.cshtml/cookie-consent.js. Matches the iframe's own fixed height so nothing
   shifts around once the real map replaces this. */
.map-consent-gate {
    height: 250px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.75rem;
    text-align: center;
    padding: 1rem;
    background: var(--surface-soft);
    border-radius: 3px;
    border: 1px dashed var(--logo-grey);
}

.map-consent-gate p {
    margin: 0;
    font-size: var(--fs-md);
    color: var(--slate);
    max-width: 40ch;
}

/* ---------- contact page ---------- */

/* Bootstrap's default accordion styling tints an open button's background and gives it a
   permanent border-shadow, plus a blue glow on click/focus - removed here so the Imprint/
   Disclaimer toggle (see #contact-legal in ContactPage.cshtml) just reads as a plain heading
   that expands, with nothing left visually "selected" once it's open. */
#contact-legal .accordion-button:not(.collapsed) {
    background-color: transparent;
    color: inherit;
    box-shadow: none;
}

#contact-legal .accordion-button:focus {
    box-shadow: none;
}

/* Intro text sits in its own column beside the form (see ContactPage.cshtml) rather than
   under the hero banner - the accent border gives it some visual weight of its own next to
   the form's card surface, instead of reading as a plain leftover paragraph. */
.contact-intro {
    border-left: 4px solid var(--wine);
    padding-left: 1.5rem;
}

/* Full container width, matching the intro text above it and every other section on this
   page (offices, legal text) - per explicit direction, dropping the previous 480px readability
   cap despite the tradeoff that a single-line email/subject input now stretches much wider
   than is strictly comfortable to read on a large viewport. Given its own card surface
   (background/shadow/radius) so it still reads as a distinct element rather than bare form
   fields sitting directly on the page background. Deliberately --surface rather than the shared
   --tile colour - see .contact-office-card for why this page's cards are excluded from it. */
.contact-form-card {
    background: var(--surface);
    border-radius: 3px;
    box-shadow: var(--shadow);
    padding: 2rem;
}

/* Sets the office-location cards apart from the form/intro section above and the legal text
   below, matching the alternating section-band pattern already used on Home (.home-band-alt)
   and the Services page (.fwc-section). */
.contact-office-section {
    background: var(--surface-soft);
}

/* Exactly 2 offices (see BACKLOG.md), shown side by side rather than behind an accordion -
   own card surface so each reads as a distinct unit against the section's soft background.
   Deliberately --surface, not --tile: this card and the contact form card are excluded from the
   tile colour by explicit choice, so a reviewer changing that colour does
   not repaint the Contact and Company pages' own furniture. Note it holds the Google Maps consent
   gate, which is what made a change here look like the map itself changing. */
.contact-office-card {
    height: 100%;
    background: var(--surface);
    border-radius: 3px;
    box-shadow: var(--shadow);
    padding: 2rem;
}


/* ---------- company page ---------- */

/* Same soft-tint section band already used on Home (.home-band-alt), the Services page
   (.fwc-section/.service-overview-bright) and Contact (.contact-office-section) - each page
   keeps its own class name rather than sharing one, matching the existing convention. Applied
   to Facts & Figures, Corporate Objectives and Download so the six sections alternate. */
.company-band-alt {
    background: var(--surface-soft);
}

/* Deliberately steps out of the page's surface/soft alternation, the same way Home's world-map
   band does - the figures are meant to read as a block of their own. */
.company-facts-band {
    background: var(--magenta);
}

/* .company-section-heading now inherits --ink from the page, which is close to unreadable on
   magenta, so this band lifts it to the shared --band-heading tone. It read --slate until
   2026-09-23; the override was needed for the same reason then and is unaffected by that change. */
.company-facts-band .company-section-heading {
    color: var(--band-heading);
}

/* The section's intro, sitting under the heading in the same half-width column, left of the
   divider rule. Deliberately not the .lead treatment the Corporate Objectives and Affiliated
   Companies intros carry: this one shares its column with the figures rather than spanning the
   page, and was asked for as a small text under the title, so it stays at body size. It takes
   the heading's own light tone for the same reason the heading overrides its colour at all -
   the inherited --ink is near-unreadable on magenta. */
/* No overflow-wrap guard on .company-prose, deliberately. Rich text that cannot wrap at all -
   a paste whose every space is a U+00A0, which is what one authored value here turned out to
   be - can be contained with `overflow-wrap: break-word`, and that was tried and rejected: it
   keeps the text inside its box only by slicing a word in half. Word boundaries are where lines
   belong, so the no-break spaces are converted to ordinary ones on the way out instead, by
   CMS.Helpers.RichText at each .company-prose render site in CompanyPage.cshtml.
   Don't add a CSS guard back as a belt-and-braces measure: it would only ever fire on copy the
   helper had already failed to fix, and it would fire by breaking a word. */

/* Capped rather than filling the column. With the heading column shrunk to its text, the
   figures inherit everything left over - on a wide screen that spread two columns across
   ~900px and read as sparse. Left-aligned by the max-width alone, so the grid still butts up
   against the divider rule.
   This was 42rem until Spanish shipped (2026-09-21), sized so that the longest English label,
   "GLOBAL PROJECT OFFICES" at 1.1rem uppercase and letter-spaced, stayed on one line. Spanish
   renders that label as "OFICINAS DE PROYECTO EN TODO EL MUNDO" - 37 characters against 22 -
   which wrapped at 42rem, hence 46rem. The lesson generalises past this rule: a width tuned
   against the longest *English* string is a latent bug in every other language, and the label
   that breaks first will not be the one you tuned for.
   One measurement worth keeping from a 50/50 split tried and reverted on 2026-09-18: at roughly
   16rem per column all six English labels still sat on one line, so there was slack at 42rem
   even before this - the Spanish label simply needs more than that slack. */
.company-facts {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 2.75rem 3.5rem;
    max-width: 46rem; /* was 42 before but the spanish figures broke line so I changed it - frs */
}

/* Heading left, figures right, divided by a vertical rule - the same arrangement Corporate
   Objectives uses further down the page, and deliberately reusing its --company-divider-gap
   token rather than a second value, so the space either side of the rule moves together when
   one is tuned. The rule takes --band-muted (the bands' supporting tone, shared with the world
   map's labels on this same magenta) rather than the --wine that Objectives uses, which would
   be near-invisible here. */
@media (min-width: 992px) {
    .company-facts-intro-col {
        padding-right: var(--company-divider-gap);
    }

    .company-facts-col {
        border-left: 1px solid var(--band-muted);
        padding-left: var(--company-divider-gap);
    }
}

/* Below lg the columns stack and the rule disappears with its padding, so the separation has to
   come back as vertical space - same token again, for the one conceptual gap. */
@media (max-width: 991.98px) {
    .company-facts-col {
        margin-top: var(--company-divider-gap);
    }
}

.company-facts > div {
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
}

@media (max-width: 600px) {
    .company-facts {
        grid-template-columns: minmax(0, 1fr);
        gap: 2rem;
    }
}

/* ---------- language picker: not-yet-available language ---------- */

/* Bootstrap's .dropdown-item.disabled sets pointer-events:none, which suppresses the native
   title tooltip entirely - so the "not yet finished" explanation would never be reachable.
   Pointer events are restored here; the item stays unclickable because master.cshtml renders it
   as a <span> rather than a link, not because pointer events were off. */
.language-dropdown-menu .dropdown-item.language-unavailable {
    pointer-events: auto;
    cursor: default;
}

/* Spacing only. The type now comes from Bootstrap's .display-5, which CompanyPage.cshtml puts on
   all four of these headings alongside this class - the same utility Home, Services and the
   sector sections already use for a section heading.
   This class used to set the type itself, and it was the site's one typographic outlier: colour
   --slate against every other page's inherited --ink, and clamp(1.75rem, 3vw, 2.5rem) against
   display-5's 3rem at desktop, so Company's section headings were both grey and half a rem
   smaller than everywhere else. The previous comment here said this treatment and
   .service-section-heading's "now agree rather than contrasting" - true only of the border they
   had both lost, while size and colour still diverged, which is exactly how a comment stops being
   load-bearing and starts being reassuring.
   Taking the type from .display-5 rather than restating its values is deliberate, and is the same
   reasoning as --company-divider-gap further down: two things meant to match should read from one
   source so they cannot drift. Note this class must therefore NOT reintroduce font-size, colour,
   font-weight or line-height - site.css is loaded after Bootstrap, so anything declared here wins
   the tie on source order and silently reopens the divergence.
   The one intended override survives untouched: .company-facts-band .company-section-heading
   above, which recolours these to --band-heading where they sit on the magenta band. It works
   because .display-5 sets no colour of its own. */
.company-section-heading {
    margin-bottom: 1.5rem;
}

.company-intro-tagline {
    margin-bottom: 1.5rem;
}

/* Body copy blocks (About, Facts & Figures). Rich text arrives as a run of <p> elements, so
   the spacing is set on those rather than on the wrapper. */
.company-prose p:last-child {
    margin-bottom: 0;
}

/* Affiliated companies: a 2x2 grid of tiles carrying only the logo and company name, each
   expanding to a panel above its own row - the same "dim tile + panel above its row" mechanic
   as Featured Projects and the Services sector tiles.
   Flex rather than CSS grid, and that is load-bearing: site.js inserts the panel as a direct
   child of this container and the panel claims a whole row with flex: 1 1 100%, which a grid
   child cannot do without column-span arithmetic that would then depend on the column count.
   It also replaces a Bootstrap row/col-lg-6 structure, which could not work at all here - the
   tiles have to be direct children of the element the panel is inserted into. */
.affiliated-grid {
    display: flex;
    flex-wrap: wrap;
    gap: 1.5rem;
    /* The intro's last paragraph has margin-bottom: 0 (see .company-prose), so without this the
       tiles sat directly under the text. On the grid rather than on the intro because
       .company-prose is shared with About and Corporate Objectives, which should not move. */
    margin-top: 2rem;
}

/* --shadow is the same elevation token .contact-office-card and the
   reference/job rows use. Without it these were the only cards on the site sitting flat on the
   background, which is what made them read as not-quite-tiles against --paper.
   These tiles are the reason --tile defaults to what it does. They were asked for explicitly as a
   slightly warmer off-white rather than the pure white every other card then used, which is
   exactly the split the token now removes - so the shared default took this value rather than the
   majority's white. The contrast against the page is narrow either way (#f3f6f5 on #e8edec is
   about a 12-step difference per channel), so the border and shadow do most of the work of
   separating tile from background - worth remembering before softening either. */
/* 20% of the container. Note this is why the four tiles now sit on ONE row rather than 2x2:
   flex-wrap fits as many as will go, and four 20% tiles plus their gaps come to about 85% of
   the row. Forcing 2x2 at this width would need an explicit full-width break element after
   every second tile, and would leave ~58% of each row empty - see BACKLOG.md, where exactly
   that trick was tried on the services grid and reverted for looking like a dead gap. */
.affiliated-tile {
    flex: 0 1 20%;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1rem;
    padding: 1.5rem;
    border: 1px solid var(--line);
    border-radius: 3px;
    background: var(--tile);
    box-shadow: var(--shadow);
    transition: transform 0.25s ease;
}

/* Matches .highlight-card's own lift exactly - these tiles had no hover state at all, which
   left the cursor as the only sign they do anything. */
/* Pointer-only, see .highlight-card:hover. */
@media (hover: hover) {
    .affiliated-tile:hover {
        transform: translateY(-3px);
    }

    /* An open tile is dimmed and should stop reacting, same as .highlight-card-dimmed. */
    .affiliated-tile-dimmed:hover {
        transform: none;
    }
}

.affiliated-tile-logo {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 90px;
}

/* Logos differ in aspect ratio, so contained rather than cropped - same reasoning as the
   panel's copy below, which is why neither is given a fixed crop box. */
.affiliated-tile-logo img {
    max-width: 100%;
    max-height: 90px;
    height: auto;
    object-fit: contain;
}

/* Pushes the toggle to the bottom so tiles with differently-sized logos still line their
   buttons up with each other. */
.affiliated-tile-footer {
    margin-top: auto;
}

.affiliated-tile-dimmed {
    opacity: 0.45;
}

.affiliated-panel {
    display: none;
    flex: 1 1 100%;
    max-width: 100%;
}

/* The open panel is deliberately the old .affiliated-card layout - logo beside the partner
   list - and reuses that card's own inner classes unchanged, so the verified styling for the
   list, the label and the registration line carries over rather than being restated. */
.affiliated-panel-open {
    display: flex;
    flex-wrap: wrap;
    column-gap: 2.75rem;
    row-gap: 1.5rem;
    padding: 1.5rem;
    background: var(--tile-open);
    border-radius: 3px;
    border-left: 3px solid var(--wine);
    box-shadow: 0 4px 10px rgba(36, 26, 32, 0.08), 0 24px 48px -24px rgba(36, 26, 32, 0.45);
}

.affiliated-panel-footer {
    flex: 1 1 100%;
}

/* Panel-only since the collapsed tile got its own .affiliated-tile-logo. Sized by a max rather
   than the old flex: 0 0 30%, which was 30% of a full-width panel - about 340px - and made the
   logo the loudest thing in an expanded view whose point is the partner list. Contained rather
   than cropped, because these logos' aspect ratios all differ and a fixed crop box distorts
   them. */
.affiliated-card-logo {
    flex: 0 0 auto;
    max-width: 170px;
    display: flex;
    align-items: center;
    justify-content: center;
}

.affiliated-card-logo img {
    max-width: 100%;
    max-height: 80px;
    height: auto;
    object-fit: contain;
}

.affiliated-card-body {
    flex: 1 1 auto;
    min-width: 0;
}

.affiliated-card-partners-label {
    margin-bottom: 0.5rem;
    color: var(--wine);
    font-size: 1.05rem;
    font-weight: 600;
}

.affiliated-card-partners ul {
    margin-bottom: 0;
    padding-left: 1.25rem;
}

.affiliated-card-registered {
    margin-top: 1rem;
    color: var(--slate);
    font-size: var(--fs-md);
}

.affiliated-card-registered p:last-child {
    margin-bottom: 0;
}

/* Stack logo above the partner list on narrow screens - side by side leaves the list too
   cramped once the card is only as wide as the viewport. */
@media (max-width: 575.98px) {
    .affiliated-tile {
        flex-basis: 100%;
    }

    /* flex-wrap: nowrap is not optional once the direction flips. In a column container wrap
       does not mean "start a new line", it means "start a new column to the right" once the
       content exceeds the container's height - the trap BACKLOG.md records biting the Featured
       Projects panel, where it pushed the page into horizontal scroll. The logo's basis is
       reset for the same family of reason: basis is width in a row and height in a column. */
    .affiliated-panel-open {
        flex-direction: column;
        flex-wrap: nowrap;
    }

    .affiliated-card-logo {
        flex: 0 0 auto;
        justify-content: flex-start;
    }
}

/* Downloads are a plain list of links, one per line - no bullets, cards or thumbnails. The
   title is rich text (it needs inline markup for "60<sup>th</sup> anniversary..."), so the
   paragraph its editor wraps the value in is flattened back to inline here to keep each entry
   on a single line. */
.download-list {
    margin-bottom: 0;
    padding-left: 1.25rem;
    list-style: disc;
}

/* The bullet takes its colour from the <li>, not from the link inside it, so it would render in
   the body colour against magenta text without this. */
.download-item::marker {
    color: var(--wine);
}

.download-item + .download-item {
    margin-top: 0.5rem;
}

/* Magenta at all times rather than only on hover, and never underlined. The colour is forced
   with !important because these titles are rich text: content pasted from an external editor
   arrives carrying its own inline style="color: ..." on a wrapper <span>, and an inline style
   beats any stylesheet rule. Clean the pasted formatting out of the content (paste as plain
   text / Remove formatting) and this can drop back to a plain declaration. The descendant
   selector is needed for the same reason - the colour has to reach the pasted <span>, not just
   the anchor. */
.download-link,
.download-link * {
    color: var(--wine) !important;
    text-decoration: none;
}

/* Slight darkening on hover. The cursor already signals the link, so this is reinforcement
   rather than the only affordance - it stays subtle deliberately. */
.download-link:hover,
.download-link:hover *,
.download-link:focus-visible,
.download-link:focus-visible * {
    color: var(--wine-hover) !important;
}

/* Pasted formatting can also wrap a title in a nested <ul><li>. Without this it renders its own
   disc bullet and indentation inside a row that is already a list item, so some entries look
   indented and bulleted while clean ones don't. */
.download-link ul,
.download-link ol,
.download-link li {
    display: inline;
    margin: 0;
    padding: 0;
    list-style: none;
}

.download-link p {
    display: inline;
    margin: 0;
}

/* 404 page. Rendered by NotFoundPageService as a standalone document rather than through
   master.cshtml, so it has no navbar or footer to lean on - a 404 is often the symptom of
   content routing having failed, and an error page that needs the content tree to render is
   unavailable exactly when it is needed. These rules are therefore self-contained, and the
   only thing the page borrows from the site is this stylesheet's palette. */
.not-found-page {
    max-width: 40rem;
    margin: 0 auto;
    padding: 6rem 1.5rem;
    text-align: center;
}

.not-found-title {
    color: var(--wine);
    font-size: 2rem;
    font-weight: 700;
    margin-bottom: 1rem;
}

.not-found-message {
    color: var(--slate);
    margin-bottom: 2rem;
}

.not-found-home-link {
    display: inline-block;
    background-color: var(--wine);
    color: var(--surface);
    text-decoration: none;
    padding: 0.65rem 1.5rem;
    border-radius: 2rem;
}

.not-found-home-link:hover,
.not-found-home-link:focus {
    background-color: var(--wine-deep);
    color: var(--surface);
}

/* Unsaved-changes indicator on My Particip's Personal data and Alert preferences tabs. Sits
   beside the Save button, and a matching dot marks the tab itself so a pending edit is still
   visible from another tab - which is most of the point on a tabbed page, since the indicator
   inside a hidden pane cannot be seen. --wine rather than Bootstrap's text-warning yellow,
   which is not in this palette. */
.unsaved-indicator {
    color: var(--wine);
    font-size: var(--fs-sm);
    font-weight: 600;
}

.nav-link.has-unsaved-changes::after {
    content: '';
    display: inline-block;
    width: 0.45rem;
    height: 0.45rem;
    border-radius: 50%;
    background: var(--wine);
    margin-left: 0.4rem;
    vertical-align: middle;
}

/* The one confirmation note for the My Particip section, sitting directly under the hero.
   Deliberately larger than the small inline text it replaced: it is the only feedback that an
   action succeeded, and it has to read as such from wherever the member happened to be on the
   page. Colours come from the palette rather than Bootstrap's alert-success green, which is not
   in it. */
.account-confirmation {
    background: var(--surface-soft);
    border: 1px solid var(--line);
    border-left: 4px solid var(--wine);
    border-radius: 6px;
    padding: 1rem 1.25rem;
    font-size: 1.05rem;
    font-weight: 600;
    color: var(--wine-deep);
}
