/* Cross-page components: hero, stat tile, pool card, empty state, toast, pagination. */

.hero { text-align: center; padding: var(--sp-7) 0; }
/* --c-green-dark, not the brand green: #AFCA0B on white is about 2.4:1 and fails AA at any size.
   The brand green is a background colour in this product, never an ink. */
.hero__eyebrow {
    font-size: var(--fs-200); font-weight: 700; letter-spacing: .12em; text-transform: uppercase;
    color: var(--c-green-dark); margin-bottom: var(--sp-2);
}
.hero__title { font-size: var(--fs-600); margin-bottom: var(--sp-3); }
.hero__subtitle { color: var(--color-text-muted); max-width: 46rem; margin: 0 auto var(--sp-5); }

.stat-grid { display: grid; gap: var(--sp-4);
    grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); margin-bottom: var(--sp-6); }
.stat-tile { background: var(--color-surface); border: 1px solid var(--color-border);
    border-radius: var(--radius); padding: var(--sp-4); border-left: 4px solid var(--color-border); }
.stat-tile__value { font-size: var(--fs-500); font-weight: 700; color: var(--c-navy);
    font-variant-numeric: tabular-nums; }
.stat-tile__label { font-size: var(--fs-200); color: var(--color-text-muted); }
/* The left accent is the ledger state, so a glance tells available from merely reserved. */
.stat-tile--available { border-left-color: var(--state-available); }
.stat-tile--reserved { border-left-color: var(--state-reserved); }
.stat-tile--sold { border-left-color: var(--state-sold); }
.stat-tile--chained { border-left-color: var(--state-chained); }
.stat-tile--unbacked { border-left-color: var(--state-unbacked); }

/* A tile that counts a set is an <a> and goes to that set (CLAUDE.md section 6). Two things have
   to be true and neither is free:

   - It must not look like a link. Without this it inherits a { color: var(--c-blue-dark) } and the
     number turns blue on exactly the tiles that changed.
   - A tile with nothing to show must not be clickable. The page drops the href at zero, and an <a>
     without one is neither focusable nor a link - so :not([href]) drops the affordance with it.
     A link to an empty list is a question with no answer. */
a.stat-tile {
    color: inherit;
    text-decoration: none;
    transition: border-color var(--dur-fast) ease, box-shadow var(--dur-fast) ease;
}
a.stat-tile[href]:hover { box-shadow: var(--shadow-sm); border-color: var(--c-blue); }
a.stat-tile[href]:focus-visible { outline: 3px solid var(--c-blue); outline-offset: 2px; }
a.stat-tile:not([href]) { cursor: default; }

/* --- Tab strip -----------------------------------------------------------
 * The buttons are .btn variants, which is what admin/config already did; there is no .tab class
 * and inventing one would restyle a control that already looks right. This only lays them out. */
.tab-strip {
    display: flex;
    gap: var(--sp-2);
    flex-wrap: wrap;
    margin-bottom: var(--sp-5);
}
.tab-strip[hidden] { display: none; }


.filter-bar { display: flex; flex-wrap: wrap; gap: var(--sp-3); margin-bottom: var(--sp-5); }
.filter-bar .field { margin-bottom: 0; min-width: 12rem; }

.empty-state { text-align: center; padding: var(--sp-7) var(--sp-4); color: var(--color-text-muted); }

.pagination { display: flex; gap: var(--sp-2); justify-content: center; margin-top: var(--sp-5); }
.pagination[hidden] { display: none; }
.pagination__page { padding: var(--sp-2) var(--sp-3); border: 1px solid var(--color-border);
    border-radius: var(--radius-sm); background: var(--color-surface); cursor: pointer; font: inherit; }
.pagination__page--active { background: var(--c-navy); color: var(--c-white); border-color: var(--c-navy); }

/* Toast. Fixed and stacked, so a slow request cannot push the page around when it lands.
 * The bottom offset clears the docked tab bar; --bottom-nav-h is 0px when there is no bar, so on a
 * desktop this is still var(--sp-4). A toast outranks the bar on z-index and would have been drawn
 * ON TOP of it - readable, but sitting over the navigation with its own shadow. */
.toast-stack { position: fixed; right: var(--sp-4); bottom: calc(var(--sp-4) + var(--bottom-nav-h));
    z-index: var(--z-toast);
    display: flex; flex-direction: column; gap: var(--sp-2); max-width: 22rem; }
.toast { padding: var(--sp-3) var(--sp-4); border-radius: var(--radius-sm);
    box-shadow: var(--shadow); background: var(--c-navy); color: var(--c-white); font-size: var(--fs-200); }
.toast--error { background: var(--color-danger); }
.toast--success { background: var(--c-green-dark); }

/* Gate banner. The operator approval gate decides whether the whole account can do anything at
 * all, so its state is carried by colour as well as words: grey until it is known, yellow while
 * it is waiting, green when it is open, red when it is closed. */
/* A banner is always followed by the thing it is talking about, so it owns the gap. Without this
   every call site carried its own inline margin - the last inline style left on those screens. */
/* position:relative so a corner control has something to anchor to - .card does not set it, which
   was checked rather than assumed. padding-right leaves the x a lane of its own, or a long message
   runs underneath it. */
.gate-banner {
    position: relative;
    border-left: 4px solid var(--c-gray);
    margin-bottom: var(--sp-5);
    padding-right: calc(var(--sp-5) + 2rem);
}
.gate-banner.is-waiting { border-left-color: var(--c-yellow); }
.gate-banner.is-approved { border-left-color: var(--c-green); }
.gate-banner.is-blocked { border-left-color: var(--color-danger); }

/* Companion guard for the badge in base.css: it is display:inline-block, and an author rule beats
 * the user agent's [hidden] rule - without this a badge toggled with el.hidden stays on screen. */
.badge[hidden] { display: none; }

/* ==========================================================================
   Marketplace pool card - rebuilt to the customer wireframe
   Country strip, an icon panel tinted with the material's OWN colour, a solid
   band in that colour, the sub-material beneath it, the recovery chain, the
   verified impacts, the price, what is available, and a green action bar.

   The first pass put a green chip, a blue chip, a yellow chevron, a yellow
   badge and a cyan button on one card; the customer's word for it was "too
   busy with colours". Each card now carries ONE hue - the material's - plus
   the single green that means "do something" everywhere else in the product.

   That hue arrives as a custom property the script sets on the card root from
   the DTO. A custom property is a value, not a display rule, so it has none of
   the [hidden] failure the inline-style ban exists for; the alternative, one
   class per material, would put the material list into this stylesheet. Every
   rule below repeats the old fixed blue as its fallback, so a material with no
   colour row still renders exactly as it did before.
   ========================================================================== */

/* An even three-across rhythm, as drawn, rather than as-many-as-fit. */
.pool-grid {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--sp-5);
}
.pool-grid[hidden] { display: none; }

@media (max-width: 1000px) {
    .pool-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 620px) {
    .pool-grid { grid-template-columns: minmax(0, 1fr); }
}

/* The card carries its own surface rather than the .card class, because the illustration and the
   bands have to bleed to the edge and .card padding would prevent that. position:relative is what
   the View link stretches its hit area against, further down. */
.pool-card {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 0;
    overflow: hidden;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sm);
    transition: box-shadow var(--dur-fast) ease, border-color var(--dur-fast) ease;
}
.pool-card[hidden] { display: none; }
.pool-card:hover { box-shadow: var(--shadow); border-color: var(--pool-card-color, var(--c-blue)); }
/* The focus ring belongs to the card, not to the link: the link's hit area IS the whole card, so a
   ring drawn around the link alone would trace a shape nobody can see. */
.pool-card:focus-within { outline: 3px solid var(--c-blue); outline-offset: 2px; }

/* --- Country, inside the picture ---------------------------------------- */

/* In the corner of the tinted panel, not on a strip of its own: a full row that held one word cost
   every card a line and pushed the price down. Absolute, so it takes no height at all. */
/* It carries its own backdrop because it no longer sits on a flat tint: the panel below now holds
   a photograph, and navy on an unknown photograph is legible on some materials and not on others. */
.pool-card__country {
    position: absolute;
    top: var(--sp-2);
    left: var(--sp-3);
    z-index: 1;
    padding: 1px var(--sp-2);
    border-radius: var(--radius-sm);
    background: color-mix(in srgb, var(--c-white) 85%, transparent);
    font-size: var(--fs-100);
    font-weight: 600;
    color: var(--c-navy);
}
.pool-card__country[hidden] { display: none; }

/* --- Illustration panel -------------------------------------------------- */

/* The panel keeps its height with no picture mapped, so a card without art is not shorter than its
   neighbours; empty it reads as a tinted header rather than as a hole. Twelve percent of the
   material's own colour is the "light version of the colour of the icon" the wireframe asks for.
   position:relative is what the country sits in. */
.pool-card__art {
    position: relative;
    display: flex;
    align-items: flex-end;
    justify-content: center;
    aspect-ratio: 2 / 1;
    /* A portrait mark would otherwise stretch the band and push the price off the card: a flex
       item's intrinsic height wins over aspect-ratio unless the box is told it may shrink. */
    min-height: 0;
    overflow: hidden;
    padding: var(--sp-3) var(--sp-4) var(--sp-2);
    background: color-mix(in srgb, var(--pool-card-color, var(--c-blue)) 12%, var(--c-white));
}
.pool-card__art[hidden] { display: none; }

/* The illustrations are stroke="currentColor" line art. An img resolves currentColor against the
   SVG document rather than against this page, so the file paints black whatever the card is
   coloured - the tint is carried by the panel behind it and the ink is softened into it. */
.pool-card__art-img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: contain;
    opacity: .45;
}
.pool-card__art-img[hidden] { display: none; }

/* The svg drawings above are currentColor line art, deliberately softened into the tinted panel.
   The png marks are finished brand artwork - dimming those turns the blue, green and yellow to
   mud. Selected on the extension because that IS the distinction: one is ink, the other a picture. */
/* A photograph of the waste stream, and it fills the panel edge to edge - the tint behind it is
   the empty state, not a mat. Absolute rather than a width/height pair because the panel keeps
   padding for the marks that still sit inside it.

   Selected on the extension, and here that really is the distinction rather than a convention:
   .jpg is a photograph of the material, .png and .svg are the drawn marks the three materials
   without a photograph still use. A photo has no transparency to soften into a tint and cropping
   one to "contain" would leave it floating in a coloured box. */
.pool-card__art-img[src$=".jpg"] {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    opacity: 1;
}

.pool-card__art-img[src$=".png"] {
    opacity: 1;
    /* These marks are portrait. Height leads and width follows, so the shape is kept and the band
       is not stretched; the SVG line art above is 2:1 and fills the panel as before. */
    width: auto;
    max-width: 100%;
    max-height: 100%;
}

/* --- Material bands ------------------------------------------------------ */

/* The name of the thing being sold, in the colour of the thing being sold. The ink is computed on
   the server by relative luminance and arrives as materialOnColor - white on the brand yellow is
   1.6:1 and would fail WCAG AA, and which ink is readable is a rule, not something an admin edits.
   display:block still needs its [hidden] twin: an author display rule beats the user agent's. */
.pool-card__material-band {
    display: block;
    margin: 0;
    padding: var(--sp-2) var(--sp-4);
    min-height: 2.6rem;
    font-size: var(--fs-400);
    font-weight: 700;
    line-height: 1.25;
    background: var(--pool-card-color, var(--c-blue));
    color: var(--pool-card-ink, var(--c-navy));
}
.pool-card__material-band[hidden] { display: none; }

/* Kept even when the pool has no sub-material, and given a minimum height. Removing it pulled every
   line under it up by a band, so two cards side by side had their prices on different rows - which
   is the whole reason a grid of cards exists. Empty it reads as a tint under the name. */
.pool-card__sub-band {
    display: block;
    margin: 0;
    padding: var(--sp-1) var(--sp-4);
    min-height: 1.75rem;
    font-size: var(--fs-200);
    font-weight: 600;
    color: var(--c-navy);
    background: color-mix(in srgb, var(--pool-card-color, var(--c-blue)) 22%, var(--c-white));
}
.pool-card__sub-band[hidden] { display: none; }

/* --- Body ---------------------------------------------------------------- */

.pool-card__body {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--sp-3);
    flex: 1 1 auto;
    /* No bottom padding: the action bar below carries the whole separation. The two used to stack
       and left 32px of nothing between the tonnage and the button - very visible once the pre-order
       sentence and the offering line were taken off the card. */
    padding: var(--sp-4) var(--sp-4) 0;
    text-align: center;
}
.pool-card__body[hidden] { display: none; }

/* --- Recovery chain ------------------------------------------------------ */

/* Neutral pills joined by a drawn arrow, replacing the interlocking yellow chevrons. The chevrons
   were the loudest colour on the card, and the wireframe asks for lines between the steps. */
.pool-card__chain {
    display: flex;
    align-items: center;
    width: 100%;
    margin: 0;
    padding: 0;
    list-style: none;
}
.pool-card__chain[hidden] { display: none; }

.pool-card__step {
    position: relative;
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--sp-2) var(--sp-1);
    border-radius: var(--radius-sm);
    font-size: var(--fs-100);
    font-weight: 600;
    line-height: 1.2;
}
.pool-card__step[hidden] { display: none; }

/* The connector is a pseudo-element, not an element: an arrow must not be focusable, must not be
   read out, and must not be left behind when the step beside it is hidden. */
.pool-card__step + .pool-card__step { margin-left: 18px; }
.pool-card__step + .pool-card__step::before {
    content: "";
    position: absolute;
    left: -18px;
    top: 50%;
    width: 18px;
    height: 2px;
    transform: translateY(-50%);
    background: var(--color-border);
}
.pool-card__step + .pool-card__step::after {
    content: "";
    position: absolute;
    left: -9px;
    top: 50%;
    width: 6px;
    height: 6px;
    transform: translateY(-50%) rotate(45deg);
    border-top: 2px solid var(--color-border);
    border-right: 2px solid var(--color-border);
}

/* Covered takes the same 22% tint as the sub-material band, so the whole card stays one hue plus
   one green button. Uncovered stays neutral: the distinction survives without a third colour. */
.pool-card__step.is-covered {
    background: color-mix(in srgb, var(--pool-card-color, var(--c-blue)) 22%, var(--c-white));
    color: var(--c-navy);
}
.pool-card__step.is-uncovered { background: var(--color-border); color: var(--color-text-muted); }

.pool-card__step-name { overflow-wrap: anywhere; }

/* --- Who did the work ----------------------------------------------------- */

/* Above the impacts, as drawn. A minimum height for two rows of chips, so a pool with one company
   and a pool with three keep the price on the same line. */
.pool-card__operators { width: 100%; min-height: 4.25rem; }
.pool-card__operators[hidden] { display: none; }

.pool-card__operators-label {
    margin: 0 0 var(--sp-2);
    font-size: var(--fs-100);
    color: var(--color-text-muted);
}

.pool-card__operator-list {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--sp-2);
    margin: 0;
    padding: 0;
    list-style: none;
}
.pool-card__operator-list[hidden] { display: none; }

.pool-card__operator {
    display: flex;
    align-items: center;
    gap: var(--sp-1);
    min-width: 0;
    max-width: 100%;
    padding: var(--sp-1) var(--sp-3);
    border-radius: 999px;
    background: var(--color-bg);
    font-size: var(--fs-100);
    font-weight: 600;
    color: var(--c-navy);
}
.pool-card__operator[hidden] { display: none; }

.pool-card__operator-logo { flex: 0 0 auto; height: 1rem; width: auto; }
.pool-card__operator-logo[hidden] { display: none; }

.pool-card__operator-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pool-card__operator-name[hidden] { display: none; }

.pool-card__operator-more {
    margin: var(--sp-1) 0 0;
    font-size: var(--fs-100);
    color: var(--color-text-muted);
}

/* --- Verified impacts ----------------------------------------------------- */

/* Two rows reserved. Nothing shifts between a pool with one verified impact and one with three. */
.pool-card__impacts-block { width: 100%; min-height: 4rem; }
.pool-card__impacts-block[hidden] { display: none; }

.pool-card__impacts-label {
    margin: 0 0 var(--sp-1);
    font-size: var(--fs-100);
    color: var(--color-text-muted);
}

.pool-card__impacts {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--sp-1);
    margin: 0;
    padding: 0;
    list-style: none;
}
.pool-card__impacts[hidden] { display: none; }

.pool-card__impact-badge {
    display: flex;
    align-items: baseline;
    gap: var(--sp-2);
    font-size: var(--fs-100);
    color: var(--color-text-muted);
}

/* Green only when every contributing proof carries the impact. A partial claim keeps a muted tick,
   because a full-strength tick beside a percentage would say two different things at once.
   The classes sit on the tick and are named is-whole / is-partial, matching what the script writes
   and what the certificate page already uses. The previous pair sat on the badge under a third
   name, is-full, so the green tick had never once rendered here. */
/* The share of the pool a partial claim covers. Muted and behind the claim, because it qualifies
   the claim rather than being one: the percentage inside the label belongs to the claim itself. */
.pool-card__impact-coverage { color: var(--color-text-muted); font-size: var(--fs-100); }
.pool-card__impact-coverage:empty { display: none; }

.pool-card__impact-tick {
    flex: 0 0 auto;
    color: var(--c-gray);
    font-weight: 700;
}
.pool-card__impact-tick.is-whole { color: var(--c-green-dark); }
.pool-card__impact-tick.is-partial { color: var(--c-yellow-dark); }

/* The server's one-sentence summary, the fallback for a response that carries it without the
   per-impact coverage the list above is built from. Both at once would say the same thing twice. */
.pool-card__impact {
    margin: 0;
    font-size: var(--fs-100);
    line-height: 1.4;
    color: var(--color-text-muted);
}
.pool-card__impact[hidden] { display: none; }

/* --- Price, availability, caveats ----------------------------------------- */

/* margin-top:auto pins the price and everything under it to the BOTTOM of the body.
 *
 * Reserving a minimum height for each slot above keeps a card from collapsing, but it cannot make
 * one impact and two impacts occupy the same space forever. Pinning from the bottom can: the grid
 * already stretches every card in a row to the tallest, so the price, the tonnage and the button
 * land on the same line across the row whatever grew above them. */
.pool-card__price-row {
    display: flex;
    align-items: baseline;
    justify-content: center;
    gap: var(--sp-2);
    margin: auto 0 0;
}
.pool-card__price-row[hidden] { display: none; }

.pool-card__price-label { font-size: var(--fs-100); color: var(--color-text-muted); }

.pool-card__price {
    font-size: var(--fs-500);
    font-weight: 700;
    line-height: 1.1;
    color: var(--c-navy);
    font-variant-numeric: tabular-nums;
}
.pool-card__price-unit { font-size: var(--fs-300); font-weight: 600; }
.pool-card__price-unit[hidden] { display: none; }

/* One line each, reserved. Empty they hold their place, so the button below sits at the same
   height on every card in the row. */
.pool-card__available {
    margin: 0;
    min-height: 1.5rem;
    font-weight: 600;
    color: var(--c-navy);
    font-variant-numeric: tabular-nums;
}

/* Shown only below the admin-set threshold. The server decides: the comparison is a business rule,
   and sending the number to every card would put that rule in the browser twelve times over. */

/* --- The button ----------------------------------------------------------- */

/* One button, centred, with the card's padding around it - not a green strip welded to the bottom
   edge, which is what an edge-to-edge bar reads as.
   Three verbs inside it, because they are three genuinely different destinations. */
/* margin-top:auto keeps the bar on the floor of the card, so the button sits on the same line
   across a row however tall the tallest card in it is. The top padding is the ONLY gap above it. */
.pool-card__actions {
    display: flex;
    justify-content: center;
    margin-top: auto;
    padding: var(--sp-3) var(--sp-4) var(--sp-4);
}
.pool-card__actions[hidden] { display: none; }

.pool-card__button {
    display: inline-flex;
    align-items: stretch;
    border-radius: 999px;
    overflow: hidden;
    background: var(--c-green);
    color: var(--c-navy);
    transition: background-color var(--dur-fast) ease;
}
.pool-card__button[hidden] { display: none; }

/* Slimmer than a standard button on purpose: this pill carries three verbs, so at the full button
   height it became a green slab across the bottom of every card and shouted louder than the price
   above it. The line-height is written down rather than inherited so the height is arithmetic
   (15px * 1.4 + 2 * 8px = 37px) and clears the 24px minimum target on its own. */
.pool-card__action {
    padding: var(--sp-2) var(--sp-4);
    font-size: var(--fs-200);
    line-height: 1.4;
    font-weight: 700;
    text-align: center;
    text-decoration: none;
    color: inherit;
    white-space: nowrap;
}
.pool-card__action[hidden] { display: none; }
/* The dividers are a border-left on each following action rather than pipe characters, so a hidden
   action takes its own divider with it and no script has to keep them in step. */
.pool-card__action + .pool-card__action {
    border-left: 1px solid color-mix(in srgb, var(--c-navy) 22%, transparent);
}
/* Navy on the brand green, never white: white on #AFCA0B is 1.6:1 and fails WCAG AA. The same
   choice base.css makes for every filled button. --c-green-hover replaces the brightness() hack
   the cyan button needed, because green has a hover shade in tokens.css and cyan does not. */
.pool-card:hover .pool-card__button { background: var(--c-green-hover); }

/* This is what makes the whole card clickable without wrapping it in a second link. It must stay
   unpositioned itself: the ::after resolves against the nearest positioned ancestor, which has to
   be .pool-card and not the link. */
.pool-card__action--view::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 2;
}
/* Above that overlay, so a specific verb can actually be hit. Three z-indexes on one card is
   exactly how they drift, so they are written down: country 1, overlay 2, specific actions 3. */
.pool-card__action--specific { position: relative; z-index: 3; }

/* ==========================================================================
   Home page sections: the benefits, the partner strip, about Kolekt
   ========================================================================== */

.home-section { padding: var(--sp-7) 0; }
.home-section[hidden] { display: none; }
.home-section__title { text-align: center; margin-bottom: var(--sp-5); }

/* Two audiences side by side, one column below the breakpoint. A class rather than an inline grid,
   because an inline display rule cannot carry the [hidden] twin it needs. */
.benefit-cols {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--sp-6);
}
.benefit-cols[hidden] { display: none; }
@media (max-width: 760px) {
    .benefit-cols { grid-template-columns: minmax(0, 1fr); }
}

.benefit-col__title {
    margin: 0 0 var(--sp-3);
    font-size: var(--fs-400);
    color: var(--c-blue-dark);
}

.benefit-col__list { margin: 0 0 var(--sp-4); padding-left: var(--sp-5); }
.benefit-col__item { margin-bottom: var(--sp-3); line-height: 1.5; }
.benefit-col__item strong { color: var(--c-navy); }

.benefit-col__links {
    display: flex;
    align-items: center;
    gap: var(--sp-4);
}
.benefit-col__links[hidden] { display: none; }

/* --- Partner strip --------------------------------------------------------- */

/* The viewport. overflow:hidden is what turns a doubled track into a loop; under
   prefers-reduced-motion it becomes a plain scrollable row instead. */
.carousel { position: relative; overflow: hidden; }
.carousel[hidden] { display: none; }

/* 0 to -50% moves content to the LEFT, which is the direction asked for: the strip reads like a
   line of companies walking past. The other direction is the sign of this pair, nothing else. The
   duration is set per instance by the component from the item count, so the speed is the same
   however many partners there are. Translating exactly half a track that has been duplicated once
   means the second copy arrives where the first one started - the seam never appears. */
.carousel__track {
    display: flex;
    align-items: stretch;
    width: max-content;
    animation: carousel-left var(--carousel-duration, 40s) linear infinite;
}
.carousel__track[hidden] { display: none; }

/* Hover and focus stop it so a name can be read; there is no control, because the strip is
   decoration and a button under it read as something to operate. */
.carousel:hover .carousel__track,
.carousel:focus-within .carousel__track { animation-play-state: paused; }

@keyframes carousel-left {
    from { transform: translateX(0); }
    to { transform: translateX(-50%); }
}

/* Nothing moves, and nothing is duplicated either - the component skips the clone, so the row is
   simply every partner, once.
 *
 * And it WRAPS rather than becoming a scroller. overflow-x:auto was the old fallback and it turned
 * the strip into something the reader had to drag by hand to see the rest - which is work, and the
 * whole reason this is a drifting strip is that nobody should have to do any. Wrapped, every
 * partner is on screen at once and nothing is hidden. */
@media (prefers-reduced-motion: reduce) {
    .carousel { overflow: visible; }
    .carousel__track {
        animation: none;
        width: auto;
        flex-wrap: wrap;
        justify-content: center;
    }
}

/* The spacing is padding on the tile, not a flex gap. With a gap the doubled track is not exactly
   two repeats - the seam gap counts once - and the loop visibly jumps every cycle. */
.partner-tile {
    flex: 0 0 auto;
    width: 11rem;
    padding-inline-end: var(--sp-6);
    text-align: center;
}
.partner-tile[hidden] { display: none; }

/* Fixed height so tiles line up whatever a logo's aspect ratio is. */
.partner-tile__logo-box {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 4.5rem;
    margin-bottom: var(--sp-2);
    padding: var(--sp-2);
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
}
.partner-tile__logo-box[hidden] { display: none; }

.partner-tile__logo { max-width: 100%; max-height: 100%; object-fit: contain; }
.partner-tile__logo[hidden] { display: none; }

/* The branch that actually renders while logoUrl is null, so it has to look like a decision. */
.partner-tile__initials {
    font-size: var(--fs-400);
    font-weight: 700;
    color: var(--c-blue-dark);
}
.partner-tile__initials[hidden] { display: none; }

/* Two lines reserved: one long company name wrapping pushed its own country down and left the
   whole strip on two different baselines as it drifted past. */
.partner-tile__name {
    min-height: 2.8em;
    margin: 0;
    font-weight: 600;
    line-height: 1.35;
    color: var(--c-navy);
}
.partner-tile__country { margin: 0; font-size: var(--fs-100); color: var(--color-text-muted); }

/* Two columns, defined once rather than repeated on the header row and again on the row template
   where the two copies have to be kept in step by hand. */
.channel-row { grid-template-columns: minmax(0, 1fr) 8rem; }

/* --- To do list and notifications ------------------------------------------
 * One table, two kinds of row: a task with a percentage, and a dated thing that happened. They
 * share a column because they answer the same question - what should I look at now. */

/* Once, here, rather than on the header row and again on the row template, where the two copies
   have to be kept in step by hand. 8rem holds a full date without wrapping; the last column is the
   width of the dismiss control and nothing more. */
.todo__table .data-table__row { grid-template-columns: 8rem minmax(0, 1fr) 3rem; }

/* .todo itself gets no display rule, so it needs no [hidden] twin. If one is ever added, the twin
   becomes mandatory. */
.todo { margin-bottom: var(--sp-6); }

a.todo-row { color: inherit; text-decoration: none; }
a.todo-row[href]:hover { background: var(--color-border); }
a.todo-row[href]:focus-visible { outline: 3px solid var(--c-blue); outline-offset: -3px; }
/* A row with nowhere to go is not a link. Same decision as a.stat-tile:not([href]). */
a.todo-row:not([href]) { cursor: default; }

.todo-row__mark { font-variant-numeric: tabular-nums; font-weight: 600; }
.todo-row.is-done .todo-row__mark { color: var(--c-green-dark); }
/* A date is not a score, so it must not compete with the percentages beside it. */
.todo-row--notice .todo-row__mark { font-weight: 400; color: var(--color-text-muted); }

/* A rejected or blocked account, which is the one row on this list that is bad news rather than
   unfinished work. Only ever set for those two states - a red mark on an ordinary open task would
   be an alarm about nothing, which is how people learn to stop reading red. */
.todo-row--alert .todo-row__mark { color: var(--color-danger); }

/* The company and country after the word Dashboard. Lighter, because the heading is the page and
   the suffix is only which account you are looking at. */
.page-head__subject { font-weight: 400; color: var(--color-text-muted); }
.page-head__subject[hidden] { display: none; }

/* --- Operator logo field ---------------------------------------------------- */

/* The preview sits beside the control rather than above it, so the operator sees what a buyer will
   see while choosing the file. A class and not an inline flex: an inline display rule cannot carry
   the [hidden] twin the sweep in CLAUDE.md section 11 exists to enforce. */
.logo-field {
    display: flex;
    align-items: flex-start;
    gap: var(--sp-4);
    margin-bottom: var(--sp-4);
}
.logo-field[hidden] { display: none; }

.logo-field__preview {
    display: flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 6rem;
    height: 4rem;
    padding: var(--sp-2);
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
}
.logo-field__preview[hidden] { display: none; }

.logo-field__img { max-width: 100%; max-height: 100%; object-fit: contain; }
.logo-field__img[hidden] { display: none; }

.logo-field__empty { font-size: var(--fs-100); color: var(--color-text-muted); }
.logo-field__empty[hidden] { display: none; }

.logo-field__control { flex: 1 1 auto; min-width: 0; }

/* --- About ----------------------------------------------------------------- */

.home-section__text {
    max-width: 46rem;
    margin: 0 auto;
    text-align: center;
    line-height: 1.6;
}

/* ==========================================================================
   Pool detail - the certificate document
   One sheet, banded. Every band is a solid cyan bar plus the body under it,
   all sharing one column and one left rule, so the eye groups what is beneath
   a bar with that bar instead of reading the page as a pile of loose blocks.
   Appended after the marketplace card section; nothing above is touched.
   ========================================================================== */

/* The way back, on the title bar. A square hit area with a drawn arrow rather than a line of text
   above the document: one link does not earn a row of its own, and the arrow beside the title says
   where it goes without spending the words. */
.cert-back {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 1.75rem;
    height: 1.75rem;
    margin-right: var(--sp-1);
    border-radius: var(--radius-sm);
    color: inherit;
    text-decoration: none;
}
.cert-back[hidden] { display: none; }
.cert-back:hover { background: color-mix(in srgb, var(--c-navy) 14%, transparent); }
.cert-back:focus-visible { outline: 2px solid var(--c-navy); outline-offset: 1px; }

/* Two borders rotated into a chevron plus a bar: an arrow that is drawn rather than typed cannot
   be read out by a screen reader, which is right - the link's aria-label says where it goes. */
.cert-back__arrow {
    width: .5rem;
    height: .5rem;
    border-left: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    transform: rotate(45deg);
    margin-left: .15rem;
}

.cert-band__bar--with-back { display: flex; align-items: center; gap: var(--sp-1); }

/* The sheet. Narrower than the container on purpose - a document has an edge, and content that
   runs the full width of the window stops reading as one. */
/* Wider than it was: the evidence table is eight columns and the body is two, and at 64rem both of
   them lived behind a horizontal scrollbar while the window had room on either side. It is still
   narrower than the container, because a document has an edge and content that runs the full width
   of the window stops reading as one. */
.cert-doc {
    max-width: 82rem;
    margin: 0 auto;
    padding-bottom: var(--sp-5);
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sm);
    /* Deliberately not overflow:hidden. A clipped ancestor becomes the scrollport for any
       sticky descendant, and this one never scrolls - which killed the sticky buy box, so a
       reader down at the operators had no price and no Pay button on screen. */
}
.cert-doc[hidden] { display: none; }

/* The buy box is the second thing in the source, so on a phone it follows the masthead directly,
   and it is placed into the right column on a wide screen. Reading order and visual order agree
   either way. align-items:start is what lets the aside stick. */
.cert-doc__columns {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 20rem;
    gap: var(--sp-5);
    align-items: start;
    padding: var(--sp-5) var(--sp-5) 0;
}
.cert-doc__columns[hidden] { display: none; }

/* The gallery and the evidence table live OUTSIDE the two-column grid on purpose, and this wrapper
   exists only to repeat that grid's horizontal inset. The 4px rule is the document's spine and it
   stays owned by .cert-band; without this wrapper the two bands down here hung it on the sheet's
   own edge while every band above hung it --sp-5 further in, so the spine jogged sideways at Photo
   gallery. It is a padding and never a display, so it cannot fight [hidden]. */
.cert-doc__wide { padding: 0 var(--sp-5); }

/* gap:0 is the whole point. Each band carries its own 4px left border, so with no gap between
   them the borders butt together into ONE rule running the full height of the column - which is
   what makes the bands read as sections of a document instead of loose blocks stacked on a sheet.
   A gap here is invisible (band and sheet are both --color-surface) and leaves the rule as a row
   of disconnected stubs. Bands the script hides are display:none, so they leave no gap either.
   No hairline between bands: every band opens with a solid cyan bar, which is the separator. */
.cert-doc__main {
    grid-column: 1;
    grid-row: 1;
    display: grid;
    /* Was 0, so the bands butted together and read as one striped block rather than as sections
       you can open. There is no spine to break any more - .cert-band stopped drawing a border
       when the order panel took over its own green rule - so they can simply breathe. */
    gap: var(--sp-3);
    min-width: 0;
}
.cert-doc__main[hidden] { display: none; }

.cert-doc__aside {
    grid-column: 2;
    grid-row: 1;
    position: sticky;
    /* Clears the sticky site header rather than sliding under it. */
    top: 5.5rem;
}
.cert-doc__aside[hidden] { display: none; }

/* --- The band ------------------------------------------------------------ */

/* No left rule any more. It was the document's spine and it had to be kept aligned across bands
   that sit at different nesting depths; the bars themselves already separate the sections, and one
   fewer thing to keep in step is worth more than the ornament. */
.cert-band {
    background: var(--color-surface);
}
.cert-band[hidden] { display: none; }

/* Light grey strip, black lettering. It was white on the brand cyan, which measures 2.24:1 -
   under the 4.5:1 WCAG AA asks for - and that is how the customer read it: the headings on this
   sheet were the one place the page asked to be squinted at. --color-text on --color-surface-alt
   measures 13.8:1, and it is the SAME black as the body copy each bar introduces, so a section
   heading now reads as part of its section rather than as a coloured label pasted over it.
   The back arrow and the fold caret are both currentColor, so they follow the text without a
   second rule. */
.cert-band__bar {
    margin: 0;
    padding: var(--sp-3) var(--sp-4);
    background: var(--color-surface-alt);
    color: var(--color-text);
    font-size: var(--fs-300);
    font-weight: 700;
    line-height: 1.3;
    letter-spacing: .04em;
    text-transform: uppercase;
}

.cert-band__body { padding: var(--sp-4) var(--sp-4) var(--sp-5); }

/* The masthead is the letterhead of the sheet, not one section among several. It runs edge to
   edge.

   The corners are cut here rather than by the sheet: .cert-doc deliberately does not clip, because
   a clipping ancestor becomes the scrollport for the sticky buy box, so the sheet's radius has to
   be repeated on the bar or a square cyan block overhangs the rounded top of the sheet. The 1px is
   the sheet's own border. (This block replaces a margin-bottom:0 that did nothing - .cert-band
   sets no margin.) */
/* The page h1 lives in this bar, and the shared bar type is --fs-200: 15px, smaller than the 16px
   body text underneath it. A document title has to outrank its own body. Uppercase and tracked is
   kept - that is the letterhead voice - but at a size that reads as the title. */
.cert-band--masthead .cert-band__bar {
    border-radius: calc(var(--radius-lg) - 1px) calc(var(--radius-lg) - 1px) 0 0;
    font-size: var(--fs-400);
    letter-spacing: .02em;
}

/* The one bar that keeps the brand cyan, and the one that keeps white lettering. Every other band
   on this sheet is something to READ and they all wear the same grey on black; this is the one you
   act on, so it is the only one allowed to carry colour - the same argument that already gives
   this band its green rule.
   Written down rather than quietly fixed, because it is the author's call and not an oversight:
   white on #0BBBEF measures 2.24:1, under the 4.5:1 WCAG AA asks for. Black on the same cyan
   measures 7.18:1 and is one line away if that trade is ever re-made. The seven grey bars are
   unaffected - they are 13.76:1 - so the sheet as a whole no longer has to be squinted at. */
.cert-band--order .cert-band__bar {
    background: var(--c-blue);
    color: var(--c-white);
}

.cert-footnote { margin: var(--sp-3) 0 0; }

/* A statement the reader must not skim past - the chain overlap makes the numbers look wrong
   until it is explained. Yellow rather than cyan so it does not read as another band. */
.cert-callout {
    margin: 0 0 var(--sp-4);
    padding: var(--sp-3) var(--sp-4);
    border-left: 4px solid var(--c-yellow);
    border-radius: var(--radius-sm);
    background: var(--color-bg);
    font-size: var(--fs-200);
}
.cert-callout[hidden] { display: none; }

/* --- Masthead ------------------------------------------------------------ */

/* The masthead's own inset, matched to the column grid below it so the picture and the order
   panel share a right edge. They were the same width and eight pixels apart, which is the kind of
   near-miss that reads as sloppy without anyone being able to say why. */
.cert-hero {
    /* The column grid below is inset --sp-5 on both sides and its right-hand track is 20rem with
       an --sp-5 gap. Repeating all three here makes this row's text column exactly the width of
       the section bars below it, and the picture exactly the width of the order panel - so the
       chain lines up with the bands without anyone writing a width down. */
    padding-left: var(--sp-5);
    padding-right: var(--sp-5);
    /* The separation between the masthead and the first band is carried by ONE side. It used to be
       two: this row's --sp-5 bottom plus the column grid's --sp-5 top met in the middle and put
       48px of nothing between the chain and the Operators bar. The grid below keeps its padding;
       this one gives its up. */
    padding-bottom: 0;
    display: grid;
    /* 20rem, which is the width of the order panel in the column below - the two right-hand
       boxes read as one edge rather than two nearly-aligned ones. Widening it also takes the
       "services completed" chain in a little, which was the other half of the same note. */
    grid-template-columns: minmax(0, 1fr) 20rem;
    gap: var(--sp-5);
    align-items: center;
}
.cert-hero[hidden] { display: none; }
/* hidden removes the picture's box, never its track: an empty 12rem column plus a --sp-5 gap sat
   beside the masthead whenever a material had no image. The script sets this when there is none. */
.cert-hero.is-artless { grid-template-columns: minmax(0, 1fr); }

.cert-hero__text { min-width: 0; }

.cert-hero__material {
    margin: var(--sp-3) 0 0;
    font-size: var(--fs-600);
    font-weight: 700;
    line-height: 1.1;
    color: var(--c-navy);
}

/* The darker brand blue rather than --c-blue: cyan text on white is about 2.2:1 and fails AA. */
.cert-hero__sub {
    margin: 0;
    font-size: var(--fs-400);
    font-weight: 600;
    line-height: 1.25;
    color: var(--c-blue-dark);
}
.cert-hero__sub[hidden] { display: none; }

.cert-hero__lead { margin: var(--sp-4) 0 var(--sp-2); color: var(--color-text-muted); }

.cert-hero__art {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--sp-3);
    border-radius: var(--radius);
    background: var(--color-bg);
    background: color-mix(in srgb, var(--c-blue) 12%, var(--c-white));
}
.cert-hero__art[hidden] { display: none; }

/* The illustrations are stroke="currentColor" line art. An <img> resolves currentColor against the
   SVG document rather than against this page, so the file paints black whatever it sits on - the
   brand tint is carried by the panel behind it and the ink is softened into it. */
.cert-hero__art-img {
    display: block;
    width: 100%;
    height: auto;
    max-height: 9rem;
    object-fit: contain;
    opacity: .45;
}
.cert-hero__art-img[hidden] { display: none; }

/* Same distinction the marketplace card draws, and for the same reason: a .png here is finished
   artwork and dimming it to .45 turns the colour to mud, where the .svg line art above is meant to
   sit softened into its panel. Selected on the extension because that IS the difference between
   the two - one is ink, the other a picture. Height leads so a portrait mark keeps its shape. */
/* Same photograph, and the same reason as on the card: it fills its panel rather than sitting in
   it. The negative margin eats the panel's padding, which is there for the drawn marks that still
   use it - a photograph matted inside a tinted border would be the same picture presented two
   different ways on two screens. The hero box has no fixed ratio, so the picture keeps its own. */
.cert-hero__art-img[src$=".jpg"] {
    display: block;
    margin: calc(var(--sp-3) * -1);
    width: calc(100% + var(--sp-3) * 2);
    height: auto;
    max-width: none;
    /* Both of these are the fix, and the second one is the one that was actually wrong. The base
       rule caps the height at 9rem so a PORTRAIT drawn mark cannot stretch the masthead - but a
       landscape photograph then gets a 320x144 box it does not fit, and object-fit: contain draws
       it at 230x144 INSIDE that box with tint down either side. The element measured as filling
       its panel the whole time: contain letterboxes within the element, so a box measurement sees
       nothing. Released from the cap, width leads and the picture keeps its own ratio. */
    max-height: none;
    /* cover rather than fill: with height auto the box already carries the picture's own ratio, so
       today they are the same - but if anything ever constrains the height again, cover crops and
       fill stretches, and a stretched photograph is the harder mistake to notice. */
    object-fit: cover;
    opacity: 1;
}
/* So the picture takes the panel's rounded corners instead of squaring them off. */
.cert-hero__art { overflow: hidden; }

.cert-hero__art-img[src$=".png"] {
    opacity: 1;
    width: auto;
    max-width: 100%;
}

.cert-chip {
    display: inline-block;
    padding: var(--sp-1) var(--sp-3);
    border-radius: 999px;
    font-size: var(--fs-100);
    font-weight: 600;
    color: var(--c-navy);
}
.cert-chip[hidden] { display: none; }
.cert-chip--country { background: var(--c-green); }

/* --- Recovery chain ------------------------------------------------------ */

.cert-chain {
    display: flex;
    align-items: stretch;
    /* Exactly as wide as the section bars underneath it, which is what it was asked to line up
       with. That falls out of the masthead using the same inset as the column grid below (see
       .cert-hero) rather than out of a number written here - a fixed max-width would agree at one
       window size and drift at every other. */
    width: 100%;
    margin: 0;
    padding: 0;
    list-style: none;
}
.cert-chain[hidden] { display: none; }

/* An arrow: a point on the right and the matching notch on the left, so a step visibly feeds the
   next one. The extra left padding clears the notch; the first step has no notch to clear. */
.cert-chain__step {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--sp-2) var(--sp-3) var(--sp-2) calc(var(--sp-3) + 10px);
    font-size: var(--fs-100);
    font-weight: 600;
    line-height: 1.2;
    text-align: center;
    clip-path: polygon(0 0, calc(100% - 10px) 0, 100% 50%, calc(100% - 10px) 100%, 0 100%, 10px 50%);
}
.cert-chain__step[hidden] { display: none; }
.cert-chain__step:first-child {
    padding-left: var(--sp-3);
    clip-path: polygon(0 0, calc(100% - 10px) 0, 100% 50%, calc(100% - 10px) 100%, 0 100%);
}
/* The steps interlock: each one starts inside the point of the step before it. */
.cert-chain__step + .cert-chain__step { margin-left: -10px; }

.cert-chain__step.is-covered { background: var(--c-yellow); color: var(--c-navy); }
.cert-chain__step.is-uncovered { background: var(--color-border); color: var(--color-text-muted); }

.cert-chain__step-name { overflow-wrap: anywhere; }

/* --- Price and order ----------------------------------------------------- */

.cert-price {
    margin: 0;
    font-size: var(--fs-500);
    font-weight: 700;
    line-height: 1.2;
    color: var(--c-navy);
    font-variant-numeric: tabular-nums;
}
/* An operator sets its own price, so the page can only promise a floor. */
.cert-price__from {
    font-size: var(--fs-100);
    font-weight: 600;
    color: var(--color-text-muted);
    vertical-align: middle;
}
.cert-price__from[hidden] { display: none; }

.cert-price__tonne {
    margin: 0;
    font-size: var(--fs-300);
    font-weight: 600;
    color: var(--c-blue-dark);
    font-variant-numeric: tabular-nums;
}
.cert-price__tonne[hidden] { display: none; }

.cert-price__note { margin: var(--sp-3) 0 var(--sp-4); }

.cert-order { display: block; }
.cert-order[hidden] { display: none; }

.cert-order__button { width: 100%; margin-top: var(--sp-2); }

.cert-order__preview {
    margin-top: var(--sp-4);
    padding-top: var(--sp-4);
    border-top: 1px solid var(--color-border);
}
.cert-order__preview[hidden] { display: none; }
.cert-order__preview-title { margin-bottom: var(--sp-3); font-size: var(--fs-300); }
.cert-order__line { margin: 0 0 var(--sp-1); font-size: var(--fs-200); }

/* The two halves of the order. The reserved line takes the same dark yellow the partial-impact
   tick uses - not the brand yellow, which is about 1.6:1 on white - so "not yet in stock" is
   marked with the colour this page already spends on "true, but only in part". */
.cert-order__split {
    margin: var(--sp-2) 0 0;
    font-size: var(--fs-200);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}
.cert-order__split[hidden] { display: none; }
.cert-order__split--reserved { color: var(--c-yellow-dark); }
.cert-order__total { margin: var(--sp-3) 0 0; font-variant-numeric: tabular-nums; }

/* --- Impacts ------------------------------------------------------------- */

/* Companion guard for base.css, where .data-table is display:grid - an author display rule beats
   the user agent's [hidden], so without this a table emptied by the script stays on screen. */
.data-table[hidden] { display: none; }

.data-table--impacts .data-table__row { grid-template-columns: minmax(0, 1fr) 9rem; }

/* The dark green, not --c-green: #AFCA0B on white is about 2.4:1 and a tick is the one mark on
   this table carrying meaning. Yellow-dark marks a claim that covers only part of the pool. */
.impact-tick { margin-right: var(--sp-2); font-weight: 700; }
.impact-tick.is-whole { color: var(--c-green-dark); }
.impact-tick.is-partial { color: var(--c-yellow-dark); }

/* --- Proofs band --------------------------------------------------------- */

.proof-figure {
    display: block;
    margin: var(--sp-5) 0;
    padding: var(--sp-4);
    border-radius: var(--radius);
    background: var(--color-bg);
    text-align: center;
}
.proof-figure[hidden] { display: none; }
.proof-figure__value {
    display: block;
    font-size: var(--fs-600);
    font-weight: 700;
    line-height: 1.1;
    color: var(--c-navy);
    font-variant-numeric: tabular-nums;
}
.proof-figure__value[hidden] { display: none; }
.proof-figure__label {
    display: block;
    margin-top: var(--sp-2);
    font-size: var(--fs-200);
    color: var(--color-text-muted);
}
.proof-figure__label[hidden] { display: none; }

/* --- Operator accordion --------------------------------------------------- */

.op-list { display: grid; gap: var(--sp-3); margin-top: var(--sp-4); }
.op-list[hidden] { display: none; }

.op-row {
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    background: var(--color-surface);
    overflow: hidden;
}
.op-row[hidden] { display: none; }
.op-row.is-open { border-color: var(--c-blue); }

/* A strip the toggle fills. It stays a wrapper rather than collapsing into the button because it
   carries the tinted background that separates a closed row from its panel. */
.op-row__head {
    display: flex;
    align-items: center;
    background: var(--color-bg);
}
.op-row__head[hidden] { display: none; }

.op-row__toggle {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    padding: var(--sp-3) var(--sp-4);
    border: 0;
    background: transparent;
    font: inherit;
    color: var(--color-text);
    text-align: left;
    cursor: pointer;
}
.op-row__toggle[hidden] { display: none; }
.op-row__toggle:focus-visible { outline: 3px solid var(--c-blue); outline-offset: -3px; }

.op-row__caret {
    flex: 0 0 auto;
    width: 0;
    height: 0;
    border-left: 6px solid var(--c-navy);
    border-top: 5px solid transparent;
    border-bottom: 5px solid transparent;
    transition: transform .15s ease;
}
.op-row.is-open .op-row__caret { transform: rotate(90deg); }

.op-row__identity { flex: 1 1 10rem; min-width: 0; display: flex; align-items: center; }
.op-row__identity[hidden] { display: none; }

.op-row__logo { display: block; height: 1.75rem; width: auto; max-width: 8rem; object-fit: contain; }
.op-row__logo[hidden] { display: none; }

/* No operator has a logo yet, so the name is the branch that actually renders. */
.op-row__name { font-weight: 700; color: var(--c-navy); overflow-wrap: anywhere; }
.op-row__name[hidden] { display: none; }

.op-row__country,
.op-row__service { flex: 0 1 auto; font-size: var(--fs-200); color: var(--color-text-muted); }

.op-row__flag { flex: 0 0 auto; }

.op-row__figures { flex: 0 0 auto; margin-left: auto; text-align: right; }
.op-row__tonnes {
    display: block;
    font-weight: 700;
    color: var(--c-navy);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.op-row__tonnes[hidden] { display: none; }
.op-row__price {
    display: block;
    font-size: var(--fs-100);
    color: var(--color-text-muted);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.op-row__price[hidden] { display: none; }

.op-row__panel {
    display: grid;
    gap: var(--sp-5);
    padding: var(--sp-4);
    border-top: 1px solid var(--color-border);
}
.op-row__panel[hidden] { display: none; }

.proof-block { min-width: 0; }
.proof-block[hidden] { display: none; }
.proof-block__heading { margin: 0; }

/* This was a heading that looked like a control and did nothing when clicked. It is now the
   button, wearing the heading's type: full width so the whole strip is the hit area, with the
   caret carrying the open/closed state that the arrow alone cannot say out loud. */
.proof-block__toggle {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    width: 100%;
    padding: var(--sp-2) 0;
    border: 0;
    background: transparent;
    font: inherit;
    font-size: var(--fs-200);
    font-weight: 700;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--c-blue-dark);
    text-align: left;
    cursor: pointer;
}
.proof-block__toggle[hidden] { display: none; }
.proof-block__toggle:focus-visible { outline: 3px solid var(--c-blue); outline-offset: 2px; }

.proof-block__caret {
    flex: 0 0 auto;
    width: 0;
    height: 0;
    border-left: 6px solid var(--c-blue-dark);
    border-top: 5px solid transparent;
    border-bottom: 5px solid transparent;
    transition: transform .15s ease;
}
.proof-block.is-open .proof-block__caret { transform: rotate(90deg); }

/* No clipping here: .table-scroll below is the scrollport for the proofs table, and an extra
   clipping box around it is what made the last columns unreachable once before. */
.proof-block__panel { padding-top: var(--sp-2); }
.proof-block__panel[hidden] { display: none; }

.proof-block__status { margin: var(--sp-3) 0 0; }
.proof-block__status[hidden] { display: none; }
.proof-block__pagination { margin-top: var(--sp-3); }

.data-table--proofs .data-table__row {
    grid-template-columns: 6rem 7rem minmax(9rem, 1fr) 8rem 9rem;
}

/* The audit table is a different shape from the proof table: names and dates, no tonnage.
   Four columns, not five - the firm's country is already in the "Audited by" band above, and a
   fifth column pushed the statement link out of the panel and behind a horizontal scroll.
   The date cells do not wrap: a date broken across two lines reads as two numbers. */
.data-table--audits .data-table__row {
    grid-template-columns: minmax(7rem, 1fr) 6.5rem 6.5rem 6rem;
}
.data-table--audits .data-table__cell { white-space: nowrap; }
.data-table--audits .data-table__cell:first-child { white-space: normal; }

/* --- Auditors ------------------------------------------------------------ */

.auditor-row { padding: var(--sp-3) 0; border-bottom: 1px solid var(--color-border); }
.auditor-row:last-child { border-bottom: 0; }
.auditor-row__name { margin: 0; }
.auditor-row__valid { margin: 0; }
.auditor-row__valid[hidden] { display: none; }
.auditor-row__links { display: flex; flex-wrap: wrap; gap: var(--sp-4); margin: var(--sp-2) 0 0; }
.auditor-row__links[hidden] { display: none; }
.auditor-row__link { display: inline-flex; font-size: var(--fs-200); }
.auditor-row__link[hidden] { display: none; }

/* --- Narrow screens ------------------------------------------------------ */

@media (max-width: 900px) {
    /* Both columns are explicitly placed at row 1 above; without resetting that they would stack
       on top of each other once the grid has a single column. */
    .cert-doc__columns { grid-template-columns: minmax(0, 1fr); }
    .cert-doc__main,
    .cert-doc__aside { grid-column: 1; grid-row: auto; }
    .cert-doc__aside { position: static; margin-bottom: var(--sp-5); }
    .cert-hero { grid-template-columns: minmax(0, 1fr); }
    .cert-hero__art-img { max-height: 7rem; }
}

@media (max-width: 700px) {
    .cert-doc__columns { padding: var(--sp-4) var(--sp-4) 0; }
    /* The wide bands track the grid's inset at every breakpoint, or the spine jogs again here. */
    .cert-doc__wide { padding: 0 var(--sp-4); }
    .cert-band__body { padding: var(--sp-3) var(--sp-3) var(--sp-4); }
    .op-row__toggle { flex-wrap: wrap; }
    .op-row__figures { margin-left: 0; text-align: left; }
}

/* ---------------------------------------------------------------------------
   Marketplace: impact filter and card badges.

   Written after the fact: the agent that added this markup did not get to its
   stylesheet, so these nine selectors existed in the templates and nowhere else.
   --------------------------------------------------------------------------- */

/* A fieldset carries browser defaults that fight the filter bar's grid. */
.filter-bar__impacts {
    border: 0;
    margin: 0;
    padding: 0;
    min-width: 0;
}
.filter-bar__impacts[hidden] { display: none; }

.filter-checks {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2) var(--sp-4);
    padding-top: var(--sp-1);
}
.filter-checks[hidden] { display: none; }

.filter-check {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    font-size: var(--fs-200);
    color: var(--c-navy);
    cursor: pointer;
    white-space: nowrap;
}
.filter-check[hidden] { display: none; }
.filter-check input { accent-color: var(--c-green); }

/* A value an operator typed before the vocabulary existed. Marked so it does not read as an
   ordinary option: it is theirs, it is kept, and unticking it is how it goes. */
.filter-check.is-legacy span {
    font-style: italic;
    text-decoration: underline dotted var(--c-gray);
    text-underline-offset: 3px;
}

/* --- A tick list whose rows own a second tick list -------------------------
 * Materials over their sub-materials, services under the phase they belong to. One level only:
 * the vocabulary is keyed PARENT|CHILD and nothing goes deeper, so this is a shape and not a
 * general tree widget. */
.picker-tree {
    display: flex;
    flex-direction: column;
    gap: var(--sp-3);
    padding-top: var(--sp-1);
}
.picker-tree[hidden] { display: none; }

.picker-tree__group { display: block; }
.picker-tree__group[hidden] { display: none; }

/* The heading of a group that is a heading rather than a choice - the three service phases. An
   operator does not tick "Processing"; they tick what they do and the phase follows. */
.picker-tree__heading {
    margin: 0 0 var(--sp-2);
    font-size: var(--fs-200);
    font-weight: 600;
    color: var(--color-text-muted);
}

/* The children sit indented under their parent so the relationship is visible without a line or a
   box. The rule stays a plain margin: at 390px the indent costs 16px and the ticks still wrap. */
.picker-tree__children {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2) var(--sp-4);
    margin: var(--sp-2) 0 0 var(--sp-5);
}
.picker-tree__children[hidden] { display: none; }

/* Whether a field is published or kept for Kolekt (Corrections 68-73). Beside the label rather
   than under the input: it qualifies the name of the field, not its value. */
.field__hint {
    font-weight: 400;
    font-size: var(--fs-100);
    color: var(--color-text-muted);
}


/* ---------------------------------------------------------------------------
   Pool detail: the order band and the chain stage badges.
   --------------------------------------------------------------------------- */

/* The buy box. Sticky so it stays with the reader through five bands of
   document below it; the top offset clears the sticky site header. No ancestor
   of this may clip its overflow or the stickiness silently dies. */
/* The order panel keeps its rule: it is the one thing on the sheet you act on rather than read,
   and the accordions gave theirs up. Written in full now that .cert-band draws no border of its
   own - a border-left-color alone would have coloured a zero-width edge and shown nothing. */
.cert-band--order {
    position: sticky;
    top: 5.5rem;
    border-left: 4px solid var(--c-green);
}

/* ==========================================================================
   Proof upload: the two-column shell, the chosen-file panel and the AI check.
   Appended section - nothing above is reordered or removed.
   ========================================================================== */

/* Form on the left, AI check on the right. align-items:start is not cosmetic: a grid item
   stretches to the row height by default, which leaves a sticky child with nowhere to travel and
   makes it look as if position:sticky were ignored. Nothing here or above it may clip its
   overflow either - a single overflow:hidden on an ancestor silently kills the stickiness. */
/* Three columns: what the reading found, the form, and the document itself.

   The check used to sit under the document in the right-hand column, which put the operator's own
   answers a full screen away from the lines they were being compared against - the one comparison
   the screen exists to make was the one thing you could not see at once. It is beside the form now.

   The document column is wider than an ordinary sidebar and grows with the window, because the
   judgement being asked for is whether a black box landed on the right line, and at 21rem that was
   a thumbnail. Areas rather than source order: the form comes first in the DOM so a narrow screen,
   or no stylesheet at all, still reads in the order the work happens. */
.proof-layout {
    display: grid;
    grid-template-columns: minmax(0, 17rem) minmax(28rem, 1fr) minmax(20rem, 30rem);
    grid-template-areas: "check form document";
    gap: var(--sp-5);
    align-items: start;
}

.proof-layout > #form { grid-area: form; }

/* Both columns start below the header AND below the strip pinned under it, and BOTH terms come
   from the same measurements the strip itself pins by.
   Writing 5.5rem here instead would be the copied definition this file keeps arguing against: it
   is 88px = the header's CURRENT 72px plus the 16px of air, so the day the header grows the strip
   moves down and these do not, and the columns pin INSIDE the band and disappear behind it (the
   strip carries a z-index and they do not). With --header-h in the expression the 16px is what it
   claims to be - a gap - rather than a coincidence.
   At default settings this still computes to exactly 88px, so nothing moves today.
   --queue-h falls back to 0px, which is exactly right for every page state that has no strip; the
   strip's height is measured rather than written down because the chips wrap, the batch changes
   size, and for a single document there is no strip at all. */
.proof-layout__check {
    grid-area: check;
    position: sticky;
    top: calc(var(--header-h, 4.5rem) + var(--queue-h, 0px) + var(--sp-4));
    max-height: calc(100vh - var(--header-h, 4.5rem) - var(--queue-h, 0px)
                     - var(--sp-6) - var(--bottom-nav-h));
    overflow: auto;
}
.proof-layout__check[hidden] { display: none; }
.proof-layout[hidden] { display: none; }

.proof-layout__aside {
    grid-area: document;
    /* Same expression as .proof-layout__check above, for the same reasons. --bottom-nav-h is in it
       because this column is still sticky between 1001 and 1100px, which is exactly the band where
       the docked tab bar exists - without it the scroller runs behind the bar. */
    top: calc(var(--header-h, 4.5rem) + var(--queue-h, 0px) + var(--sp-4));
    position: sticky;
    /* Its own scrollbar so a long list of checked fields stays reachable on a short viewport.
       Scrolling the sticky element itself is safe; it is a clip on an ANCESTOR that breaks it. */
    max-height: calc(100vh - var(--header-h, 4.5rem) - var(--queue-h, 0px)
                     - var(--sp-6) - var(--bottom-nav-h));
    overflow: auto;
}

/* Below this the panel is no use beside the form, so it drops underneath it - it is second in the
   source, which is also the order a screen reader has always read it in. */
/* Two columns before one: the document has to stay beside the form for as long as possible,
   because reading a box against a field is the whole job. The check drops underneath first, since
   it is the only one of the three that still makes sense read after the fact. */
/* Three columns need about 1120px of grid before the form drops below a usable width, and the
   page is 1400px wide at most - so two columns start earlier than the container does. */
@media (max-width: 1250px) {
    .proof-layout {
        grid-template-columns: minmax(0, 1fr) minmax(20rem, 28rem);
        grid-template-areas:
            "form document"
            "check check";
    }
    .proof-layout__check { position: static; max-height: none; overflow: visible; }
}

@media (max-width: 1000px) {
    .proof-layout {
        grid-template-columns: minmax(0, 1fr);
        grid-template-areas: "form" "document" "check";
    }
    .proof-layout__aside { position: static; max-height: none; overflow: visible; }
}

/* --- One document, or a batch ------------------------------------------- */

/* The mode card. Deliberately outside .proof-layout: it decides what the three columns are
   describing, so it cannot be one of them. */
.upload-mode { margin-bottom: var(--sp-5); }
.upload-mode[hidden] { display: none; }

/* The strip. Prev and next hold their own width, the chip list takes the rest, and the status
   line runs the full width underneath so a long sentence never squeezes the buttons.
   minmax(0, 1fr) on the list, not 1fr: 1fr floors at min-content, and a list of file names is
   exactly the content that then pushes the page sideways. Measured before, on .field-grid. */
/* It STAYS PUT under the site header.
 *
 * This strip is the only thing on the page that answers "which document am I on, and how many are
 * left" - and the moment it is needed is the moment it scrolls away, because filling in the form
 * means scrolling past it and staying there. Under the header rather than over it: --z-sticky is
 * the header's own layer, and two sticky things with no declared order paint in source order,
 * which would put this band over the site nav.
 *
 * It is a SIBLING of .proof-layout, not a child, and that is what makes any of this work: a child
 * of that named-areas grid would need a grid-area, and a sticky grid item takes its own area as
 * containing block - which would leave the strip with almost no travel to stick through.
 *
 * The offset is MEASURED, not typed. The header is 72px wide-screen and 68px at 1100px and below
 * (where the 44px burger, not the brand lockup, sets the height), and it grew once already this
 * month when the greeting was added to it - a number copied here would have gone quietly wrong
 * that day and left the strip either floating or half-covered. */
.doc-queue {
    position: sticky;
    top: var(--header-h, 4.5rem);
    z-index: var(--z-sticky-sub);
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    grid-template-areas:
        "title title title"
        "prev list next"
        "status status status";
    gap: var(--sp-2) var(--sp-3);
    align-items: center;
    margin-bottom: var(--sp-5);
    padding: var(--sp-3) var(--sp-4);
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
}
.doc-queue[hidden] { display: none; }

/* Names what the chips underneath it are saying. They already read reading / waiting / ready, and
   without a heading that is three states with nothing to attach them to. */
.doc-queue__title {
    grid-area: title;
    margin: 0;
    font-size: var(--fs-200);
    font-weight: 700;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--color-text-muted);
}

.doc-queue__nav--prev { grid-area: prev; }
.doc-queue__nav--next { grid-area: next; }
.doc-queue__status { grid-area: status; margin: 0; font-size: var(--fs-100); }

.doc-queue__list {
    grid-area: list;
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2);
    min-width: 0;
    margin: 0;
    padding: 0;
    list-style: none;
}
.doc-queue__list[hidden] { display: none; }

.doc-queue__chip {
    display: flex;
    align-items: baseline;
    gap: var(--sp-2);
    max-width: 16rem;
    padding: var(--sp-1) var(--sp-2);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    background: var(--color-bg);
    font: inherit;
    font-size: var(--fs-100);
    color: var(--color-text);
    cursor: pointer;
}
.doc-queue__chip:hover { border-color: var(--c-blue); }
.doc-queue__chip:focus-visible { outline: 3px solid var(--c-blue); outline-offset: 2px; }

/* The one on screen. A border alone is too quiet next to nine others, so it also takes the
   surface: the chip you are looking at is the same colour as the sheet below it. */
.doc-queue__chip.is-current {
    background: var(--color-surface);
    border-color: var(--c-navy);
    box-shadow: inset 0 0 0 1px var(--c-navy);
    font-weight: 600;
}

.doc-queue__no { flex: 0 0 auto; color: var(--color-text-muted); font-variant-numeric: tabular-nums; }

/* The name is the only part allowed to lose characters, and it loses them at the END where a
   file name is least distinguishing - "weight-ticket-march-02.pdf" and "...-03.pdf" would be
   told apart by the number the strip also prints beside them. */
.doc-queue__name {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.doc-queue__state { flex: 0 0 auto; color: var(--color-text-muted); }
.doc-queue__chip.is-reading .doc-queue__state { color: var(--c-blue-dark); }
.doc-queue__chip.is-failed .doc-queue__state { color: var(--color-danger); }
.doc-queue__chip.is-read .doc-queue__state { color: var(--c-green-dark); }

/* Below this the two nav buttons and a row of chips no longer fit on one line, so the strip
   becomes three stacked rows rather than a squeezed one. */
/* 43.75rem is 700px at the browser's default font size - written in rem, not px, so it also trips
   for someone who has raised only their default text size. That user keeps a 900px viewport while
   every chip, its padding and the number of wrapped rows grow, which is precisely the case a px
   threshold would miss and the pinned band would then take a third of their screen.
   ONE block, not two with the same query: the restack and the un-pin are the same decision about
   the same width, and two blocks are the shape that gets half-edited. */
@media (max-width: 43.75rem) {
    .doc-queue {
        grid-template-columns: 1fr 1fr;
        grid-template-areas:
            "title title"
            "list list"
            "prev next"
            "status status";
        /* It stops being pinned once it costs more screen than it is worth. Measured on a 390px
           phone, where the chips go one per line: 196px for two documents, 253px for three, 346px
           for six - so at a full batch it is over a third of an 844px viewport, permanently.
           Pinning is a trade of a little height for never losing your place, and here the height
           has stopped being little. top and z-index are reset with it rather than left in force:
           dead declarations on a static element are what the next reader has to disprove. */
        position: static;
        top: auto;
        z-index: auto;
    }
}

/* --- Choosing the file -------------------------------------------------- */

/* The native input stays in the box and stays the control: it is what the keyboard reaches, what
   the screen reader announces and what the form submits. The dashed frame around it is the drop
   target and nothing more. */
.file-pick {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-3);
    padding: var(--sp-4);
    border: 2px dashed var(--color-border);
    border-radius: var(--radius-sm);
    background: var(--color-bg);
    transition: border-color .15s ease, background-color .15s ease;
}
.file-pick[hidden] { display: none; }
.file-pick.is-dragging {
    border-color: var(--c-blue);
    background: var(--color-surface);
}

.file-pick__input { flex: 1 1 14rem; cursor: pointer; }
.file-pick__hint { flex: 0 1 auto; font-size: var(--fs-100); color: var(--color-text-muted); }

/* What was actually picked. Without this the file input said nothing at all once the dialog
   closed, and operators could not tell an attached document from a forgotten one. */
.file-chosen {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--sp-3);
    margin-top: var(--sp-3);
    padding: var(--sp-3) var(--sp-4);
    border: 1px solid var(--color-border);
    border-left: 4px solid var(--c-green);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
}
.file-chosen[hidden] { display: none; }

.file-chosen__icon { flex: 0 0 auto; font-size: var(--fs-400); line-height: 1; }
.file-chosen__text { flex: 1 1 10rem; min-width: 0; }
.file-chosen__name {
    display: block;
    font-weight: 600;
    color: var(--c-navy);
    overflow-wrap: anywhere;            /* a long upload name must wrap, not widen the column */
}
.file-chosen__size {
    display: block;
    font-size: var(--fs-100);
    color: var(--color-text-muted);
    font-variant-numeric: tabular-nums;
}
.file-chosen__actions { flex: 0 0 auto; display: flex; gap: var(--sp-2); }
.file-chosen__actions[hidden] { display: none; }

/* --- A read in progress -------------------------------------------------
   The only moving thing on this screen, and it earns it: a Gemini read of one page takes 13-27
   seconds, and before this the page showed a disabled button and a grey block that looked exactly
   like the grey block it shows when it has finished and learned nothing. Static text cannot tell
   those two apart; motion can. */
.reading-overlay {
    position: fixed;
    inset: 0;
    z-index: var(--z-sticky);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--sp-5);
    background: rgba(11, 45, 68, 0.55);
}
.reading-overlay[hidden] { display: none; }

.reading-overlay__card {
    display: grid;
    gap: var(--sp-3);
    width: min(28rem, 100%);
    padding: var(--sp-5) var(--sp-6);
    border-radius: var(--radius);
    background: var(--color-surface);
    box-shadow: 0 12px 32px rgba(11, 45, 68, 0.30);
    text-align: center;
}
.reading-overlay__card:focus-visible { outline: 3px solid var(--c-blue); outline-offset: 2px; }
.reading-overlay__card .reading__label { justify-content: center; font-size: var(--fs-300); }
.reading__slow { color: var(--c-yellow-dark); font-weight: 600; }
.reading__slow[hidden] { display: none; }

.reading__label {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    margin: 0;
    font-weight: 600;
    font-size: var(--fs-200);
    color: var(--c-blue-dark);
}
.reading__spinner {
    flex: 0 0 auto;
    width: 0.9rem;
    height: 0.9rem;
    border: 2px solid rgba(11, 187, 239, 0.35);
    border-top-color: var(--c-blue-dark);
    border-radius: 50%;
    animation: kxc-spin 0.9s linear infinite;
}

/* An indeterminate bar rather than a percentage: we genuinely do not know how far along a read is,
   and a bar that claims 60% when nothing measured it is a lie the user can time. */
.reading__bar {
    height: 3px;
    border-radius: 2px;
    background: rgba(11, 187, 239, 0.25);
    overflow: hidden;
}
.reading__bar > span {
    display: block;
    width: 35%;
    height: 100%;
    border-radius: 2px;
    background: var(--c-blue-dark);
    animation: kxc-slide 1.4s ease-in-out infinite;
}
.reading__hint { margin: 0; font-size: var(--fs-100); }

@keyframes kxc-spin { to { transform: rotate(360deg); } }
@keyframes kxc-slide {
    0%   { transform: translateX(-100%); }
    100% { transform: translateX(340%); }
}

/* Motion is a preference, and a spinner is not worth overriding it. The panel still says what is
   happening; it simply stops moving, and the bar sits filled rather than sliding. */
@media (prefers-reduced-motion: reduce) {
    .reading__spinner { animation: none; border-top-color: rgba(11, 187, 239, 0.35); }
    .reading__bar > span { animation: none; width: 100%; }
}


/* --- The AI check panel ------------------------------------------------- */

.ai-check__title { font-size: var(--fs-400); margin-bottom: var(--sp-3); }
.ai-check__intro { font-size: var(--fs-200); color: var(--color-text-muted); }
.ai-check__intro[hidden] { display: none; }
.ai-check__note[hidden] { display: none; }

/* The verdict. Green when the document and the form agree, brand yellow when they do not, grey
   when the reader could not say. Never red: a disagreement holds the kilos back until somebody
   settles it, it does not reject the upload, and red would say otherwise. */
.ai-check__verdict {
    display: flex;
    flex-direction: column;
    gap: var(--sp-2);
    padding: var(--sp-3) var(--sp-4);
    margin-bottom: var(--sp-4);
    border-left: 4px solid var(--c-gray);
    border-radius: var(--radius-sm);
    background: var(--color-bg);
    font-size: var(--fs-200);
}
.ai-check__verdict[hidden] { display: none; }
.ai-check__verdict.is-ok { border-left-color: var(--c-green); }
.ai-check__verdict.is-query { border-left-color: var(--c-yellow); }
.ai-check__verdict.is-unknown { border-left-color: var(--c-gray); }

.ai-check__verdict-label { font-weight: 700; color: var(--c-navy); }
.ai-check__verdict.is-ok .ai-check__verdict-label { color: var(--c-green-dark); }
/* Brand yellow is unreadable as text on white, so the wording takes the derived gold. */
.ai-check__verdict.is-query .ai-check__verdict-label { color: var(--c-yellow-dark); }
.ai-check__verdict.is-unknown .ai-check__verdict-label { color: var(--color-text-muted); }
.ai-check__verdict-text { color: var(--color-text); }

.ai-check__list {
    display: grid;
    gap: var(--sp-2);
    margin: 0 0 var(--sp-4);
    padding: 0;
    list-style: none;
}
.ai-check__list[hidden] { display: none; }

.ai-check__row {
    display: grid;
    gap: var(--sp-2);
    padding: var(--sp-3);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
}
.ai-check__row[hidden] { display: none; }

.ai-check__field { font-weight: 600; font-size: var(--fs-200); color: var(--c-navy); }

/* Typed value above, extracted value below, each with its own caption: side by side they were
   two numbers with no way to tell which was which. */
.ai-check__values { display: grid; gap: var(--sp-1); }
.ai-check__values[hidden] { display: none; }
.ai-check__value { display: grid; gap: 2px; }
.ai-check__value[hidden] { display: none; }
.ai-check__value-label {
    font-size: var(--fs-100);
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--color-text-muted);
}
.ai-check__value-text { font-size: var(--fs-200); overflow-wrap: anywhere; }

.ai-check__state { justify-self: start; }
.ai-check__state.is-ok { background: var(--c-green); color: var(--c-navy); }
.ai-check__state.is-query { background: var(--c-yellow); color: var(--c-navy); }
.ai-check__state.is-unknown { background: var(--color-border); color: var(--color-text-muted); }

/* --- Audit firm picker ---------------------------------------------------
 * Type a name, pick a row, and the id travels in a hidden input. The admin used to have to know
 * the firm's numeric id, because nothing listed the firms. */
.firm-pick { position: relative; }

.firm-pick__list {
    position: absolute;
    top: 100%; left: 0; right: 0;
    z-index: 5;                 /* only has to clear the date field beside it */
    margin: var(--sp-1) 0 0;
    padding: 0;
    max-height: 14rem;
    overflow-y: auto;
    list-style: none;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow);
}
/* Companion guard: the rule above sets no display, so the UA [hidden] still hides it. Stated so
   nobody adds display:block here and silently breaks the closed state. */

.firm-pick__option {
    display: block;
    width: 100%;
    padding: var(--sp-2) var(--sp-3);
    border: 0;
    background: transparent;
    font: inherit;
    text-align: left;
    cursor: pointer;
    color: var(--color-text);
}
.firm-pick__option[hidden] { display: none; }
.firm-pick__option:hover,
.firm-pick__option:focus-visible { background: color-mix(in srgb, var(--c-blue) 12%, var(--c-white)); }

.firm-pick__country { color: var(--color-text-muted); font-size: var(--fs-100); }

/* The certificate document's two columns.
 *
 * These are grids with an implicit auto track, so the track sizes to the widest band's MIN-content
 * - and .data-table carries min-width:max-content, which propagates straight out through
 * .table-scroll. The impacts band alone measured 368px inside a 324px column and gave the whole
 * page 11px of sideways scroll at 390px. minmax(0, 1fr) lets the track be narrower than its
 * content, which is what puts the overflow back inside the scroller where it belongs. */
.cert-doc__main,
.cert-doc__aside { grid-template-columns: minmax(0, 1fr); }

/* --- Info tip ------------------------------------------------------------
 * The "?" beside a heading or a field label. It carries a sentence that used to sit on the page
 * as a paragraph.
 *
 * The sentence has not left the product: info-tip.js wires aria-describedby from the button to
 * the bubble, and a description is computed even from a display:none target - so a screen reader
 * still hears it without opening anything. It just stops costing every sighted reader a paragraph
 * they mostly already knew. */
/* vertical-align:middle is defined as "box centre on the baseline plus HALF THE X-HEIGHT", but the
   eye centres much higher than that - so the icon reads low, and the taller the heading the more it
   shows. The 0.22em is MEASURED, not derived: against the real line box of a 24px h2 the icon sat
   5.3px low, which is 0.22em, and it is expressed in em so the same rule holds next to an h1, an h2
   and a small field label without a second number. Re-measure with scripts/browser/info-tip.js if
   the type scale or the font ever changes. */
/* ⚠ The lift is on the BUTTON and must stay there. On the wrapper it made .info-tip the containing
   block for position:fixed descendants - a non-none transform does that - so the phone rule below
   resolved left/right against the 20px question mark instead of the viewport: width came out
   negative, the bubble collapsed to a ~34px strip and the sentence wrapped one character per line.
   The customer reported it as "the hover text is not readable". Moving it down one rule optically
   centres the same mark and leaves the wrapper an ordinary box. */
.info-tip {
    position: relative;
    display: inline-block;
    vertical-align: middle;
    margin-left: var(--sp-2);
}
/* Companion guard: the rule above is display:inline-block. */
.info-tip[hidden] { display: none; }

.info-tip__button {
    transform: translateY(-0.22em);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.25rem;
    height: 1.25rem;
    padding: 0;
    border: 1px solid var(--color-border);
    border-radius: 999px;
    background: var(--color-surface);
    color: var(--color-text-muted);
    font: inherit;
    font-size: var(--fs-100);
    font-weight: 700;
    line-height: 1;
    cursor: pointer;
    transition: background-color var(--dur-fast) ease, color var(--dur-fast) ease;
}
/* Companion guard: the rule above is display:inline-flex. */
.info-tip__button[hidden] { display: none; }
/* The circle reads better small, but 20px is under the 24px minimum target, so the button claims
   the missing pixels with an invisible overlay instead of growing and unbalancing the heading. */
.info-tip__button { position: relative; }
.info-tip__button::after { content: ""; position: absolute; inset: -2px; border-radius: 999px; }
.info-tip__button:hover { background: var(--c-yellow); color: var(--c-navy); }
.info-tip__button:focus-visible { outline: 3px solid var(--c-blue); outline-offset: 2px; }
.info-tip.is-open .info-tip__button { background: var(--c-yellow); color: var(--c-navy); }

/* NO [hidden] companion here, and that is deliberate: this rule sets no display at all, so the
   user agent's [hidden]{display:none} is what hides it. Adding display:block would silently break
   every tip on the site. Said out loud because every other block in this file needs a companion
   and the absence would otherwise read as an oversight. */
.info-tip__bubble {
    position: absolute;
    top: calc(100% + var(--sp-2));
    left: 0;
    z-index: var(--z-tip);
    width: max-content;
    max-width: 22rem;
    padding: var(--sp-3) var(--sp-4);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    box-shadow: var(--shadow);
    font-size: var(--fs-200);
    font-weight: 400;
    line-height: 1.5;
    color: var(--color-text);
    text-align: left;
    /* The tip often sits in a heading row that does not wrap; the bubble must. */
    white-space: normal;
}

/* Near the right edge the bubble would leave the viewport. Chosen per call site rather than
   measured: there is no layout pass to hook here and a hand-picked anchor is honest. */
.info-tip--end .info-tip__bubble { left: auto; right: 0; }

/* ⚠ A tip inside a scroll container cannot open downward.
 *
 * .table-scroll is overflow-x:auto, and per the overflow spec one axis that is not `visible` makes
 * the other `auto` too - so it is a vertical clipper as well. Every tip on the stock-balance header
 * and every tip on a to-do row lives inside one, opens downward, and was cut off at the bottom edge
 * of the table; the browser grew a scrollbar inside the table instead of showing the sentence. Same
 * family as the clipped-ancestor notes on .data-table and on the sticky header.
 *
 * The fix cannot be CSS alone - an absolutely positioned box is clipped by any such ancestor
 * whatever it is offset from - so info-tip.js detects the clipper, adds is-pinned and hands the
 * measured viewport coordinates in as custom properties. A VALUE through a custom property, not a
 * display rule: the [hidden] companion above still does the hiding.
 *
 * Scoped to min-width 481px so the phone rule below, which is the better answer on a small screen,
 * is not outranked by this one's higher specificity. */
@media (min-width: 481px) {
    .info-tip.is-pinned .info-tip__bubble {
        position: fixed;
        top: var(--tip-y, 0);
        left: var(--tip-x, 0);
        right: auto;
        max-width: min(22rem, calc(100vw - 2 * var(--sp-4)));
    }
}

@media (max-width: 480px) {
    /* On a narrow screen the bubble is pinned to the VIEWPORT, not to the tip.
     *
     * A tip can sit anywhere along a heading that has wrapped, and left:0 anchored to the tip with
     * a near-full-viewport max-width put nearly every bubble past the right edge - which gave the
     * whole page a horizontal scrollbar the moment one was opened. Measured at 390px it was up to
     * 296px of overflow on the auditor screen. .info-tip--end does not save it either: a
     * right-anchored bubble then overflows the LEFT edge instead.
     *
     * Anchoring to the bottom of the viewport also puts it where a thumb is, rather than under a
     * heading the keyboard may have scrolled away.
     *
     * Which is exactly where the docked tab bar now is. The bubble's z-index (--z-tip) is above the
     * bar's (--z-sticky), so it would have covered the navigation instead of disappearing under it
     * - the worse of the two failures, because a bubble that is dismissed by tapping elsewhere
     * would have eaten the taps meant for the tabs. It sits ON TOP of the bar instead:
     * --bottom-nav-h is the bar's height, and 0px on any screen that has no bar. */
    .info-tip__bubble,
    .info-tip--end .info-tip__bubble {
        position: fixed;
        top: auto;
        bottom: calc(var(--sp-4) + var(--bottom-nav-h));
        left: var(--sp-4);
        right: var(--sp-4);
        width: auto;
        max-width: none;
    }
}

/* --- Named rows the primitives do not cover ------------------------------
 * Three shapes that were inline `style="display:flex"` and are specific enough to deserve a name
 * rather than a generic row class. Each one gets the [hidden] companion an inline rule cannot. */

/* One operator on a public page: mark on the left, name and country beside it. */
.operator-tile {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    padding: var(--sp-3);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
}
.operator-tile[hidden] { display: none; }

/* The buyer page puts these on the page background rather than on a card, so they need to lift
   themselves off it; the certificate page is already a white sheet and must not paint again. */
.operator-tile--surface { background: var(--color-surface); }

/* A photo in the operator's gallery, with what it shows and its remove button under it. */
.photo-tile { margin: 0; }
.photo-tile__caption {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-2);
    margin-top: var(--sp-2);
}
.photo-tile__caption[hidden] { display: none; }

/* The recovery path across the top of a public certificate: the process steps, centred under
   their caption because the whole sheet is centred. */
.path-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: var(--sp-2);
}
.path-row[hidden] { display: none; }


/* ==========================================================================
   The public company page, /company/{slug}
   Everything on it is anonymous, and everything that classifies something is a
   label the vocabularies supplied - so this file styles shapes, never meanings.
   ========================================================================== */

.company[hidden] { display: none; }

.company__head {
    display: flex;
    align-items: center;
    gap: var(--sp-4);
    margin-bottom: var(--sp-5);
}
.company__head[hidden] { display: none; }

/* Fixed size, so the heading does not shift sideways the day a logo is uploaded. */
.company__logo-box {
    display: flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 5.5rem;
    height: 5.5rem;
    padding: var(--sp-2);
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
}
.company__logo-box[hidden] { display: none; }

/* Beside a name in a tile rather than at the head of a page, so the box comes down to the height
   of the two lines next to it - the same reason the certificate card sizes its own. */
.company__logo-box--sm { width: 3rem; height: 3rem; padding: var(--sp-1); }
.company__logo-box--sm .company__initials { font-size: var(--fs-200); }

.company__logo { max-width: 100%; max-height: 100%; object-fit: contain; }
.company__logo[hidden] { display: none; }

/* The branch that renders while logoUrl is null, so it has to look like a decision. */
.company__initials { font-size: var(--fs-500); font-weight: 700; color: var(--c-blue-dark); }
.company__initials[hidden] { display: none; }

.company__identity { min-width: 0; }

.company__meta {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--sp-3);
    margin: var(--sp-1) 0 0;
    color: var(--color-text-muted);
}
.company__meta[hidden] { display: none; }
.company__meta [hidden] { display: none; }

.company__description { max-width: 46rem; line-height: 1.6; }
.company__description[hidden] { display: none; }

/* As many columns as fit, with no breakpoint to keep in step with the number of facts. */
.company__facts {
    display: grid;
    gap: var(--sp-5);
    grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
    margin: var(--sp-6) 0;
}
.company__facts[hidden] { display: none; }

.company__fact[hidden] { display: none; }
.company__fact-title { font-size: var(--fs-300); margin-bottom: var(--sp-2); }

.company__section { margin-bottom: var(--sp-6); }
.company__section[hidden] { display: none; }

.company__audit-status { color: var(--color-text-muted); }

.company__audits { list-style: none; margin: var(--sp-3) 0 0; padding: 0; }
.company__audits[hidden] { display: none; }

.company__audit {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--sp-3);
    padding: var(--sp-3) 0;
    border-top: 1px solid var(--color-border);
}
.company__audit[hidden] { display: none; }

.company__audit-link { font-weight: 600; }
.company__audit-link[hidden] { display: none; }

/* A list of words, wrapping. Used for phases, materials, services and equipment - four lists whose
   only difference is where the words came from. */
.chip-list {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2);
    margin: 0;
    padding: 0;
    list-style: none;
}
.chip-list[hidden] { display: none; }

.chip {
    padding: var(--sp-1) var(--sp-3);
    border-radius: 999px;
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    font-size: var(--fs-100);
    color: var(--c-navy);
}
.chip[hidden] { display: none; }

.photo-block { margin-bottom: var(--sp-5); }
.photo-block[hidden] { display: none; }
.photo-block__title { font-size: var(--fs-300); margin-bottom: var(--sp-3); }

/* The tiles have no caption on a public page - the category heading above them says it once. */
.photo-tile__img {
    display: block;
    width: 100%;
    aspect-ratio: 4 / 3;
    object-fit: cover;
    border-radius: var(--radius);
    background: var(--color-bg);
}
.photo-tile__img[hidden] { display: none; }


/* ==========================================================================
   Certificate summary - the parts the rebuild added
   Folding bands, the operator columns, the pricing breakdown and the seven-
   column evidence table. The band, hero, chain, price and order styles above
   are unchanged and still do their job.
   ========================================================================== */

/* --- Folding bands ---------------------------------------------------------
 * Four independent disclosures, one of them open from the start. Deliberately
 * NOT window.KXC.tabs: tabs show one panel at a time, force mutual exclusivity
 * and own location.hash, and none of those is wanted here. Deliberately not a
 * <details> either - that cannot be driven by el.hidden, which is the only way
 * a panel in this codebase is allowed to disappear. */
/* A closed fold is a bar with nothing under it, and three of them stacked read as one solid block
   of colour rather than three things you can open. The hairline is what separates them. */
.fold + .fold > .fold__bar { border-top: 1px solid var(--color-surface); }
.fold[hidden] { display: none; }

.fold__bar { padding: 0; }

/* The button IS the bar, so the whole coloured strip is the hit area rather
   than the few words on it. */
.fold__toggle {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    width: 100%;
    padding: var(--sp-3) var(--sp-4);
    border: 0;
    background: none;
    color: inherit;
    font: inherit;
    text-align: left;
    cursor: pointer;
}
.fold__toggle[hidden] { display: none; }
.fold__toggle:focus-visible { outline: 3px solid var(--c-navy); outline-offset: -3px; }

.fold__caret {
    flex: 0 0 auto;
    width: 0;
    height: 0;
    border-top: 5px solid transparent;
    border-bottom: 5px solid transparent;
    border-left: 7px solid currentColor;
    transition: transform var(--dur-fast) ease;
}
.fold.is-open .fold__caret { transform: rotate(90deg); }

/* The Info fold on a work screen has to read as something you can press, not as a heading that
   happens to have a mark beside it (Corrections 84/85). Green is the colour of an action here.
   NOT --c-green: measured against the card surface the brand green gives 1.86:1 and the caret all
   but disappears, where --c-green-dark gives 5.34:1. The label goes with it, so the whole bar is
   one affordance rather than a green mark next to black text. */
.fold--boxed > .fold__bar > .fold__toggle { color: var(--c-green-dark); font-weight: 600; }
.fold--boxed > .fold__bar > .fold__toggle:hover { color: var(--c-navy); }

/* A count on the bar, so a closed fold still says how much is behind it. */
.fold__count { margin-left: auto; font-weight: 400; font-size: var(--fs-200); }
.fold__count[hidden] { display: none; }

.fold__panel[hidden] { display: none; }

/* A fold that stands on its own rather than sitting inside a band, so it draws its own box.
   The padding lives on the toggle and on the panel and NEVER on the root: a padded root would
   inset the bar from the edge the button is supposed to span, and two paddings meeting in the
   middle is how the gap under a card's last line got to be 32px once already. */
.fold--boxed {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    margin-bottom: var(--sp-5);
}
.fold--boxed[hidden] { display: none; }
.fold--boxed > .fold__panel { padding: 0 var(--sp-4) var(--sp-4); }

/* Three blocks side by side rather than one narrow column.
   A single 62ch column inside a 1240px card left the right half of the card empty, which reads as
   text that has been cut off rather than as a deliberate measure. Columns solve both: each block
   keeps a readable line length AND the card is filled. auto-fit rather than a fixed three, so the
   same rule gives two columns on a tablet and one on a phone with nothing to override. */
.proof-info {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
    gap: var(--sp-4) var(--sp-6);
    align-items: start;
}
.proof-info[hidden] { display: none; }
.proof-info__col > :first-child { margin-top: 0; }
.proof-info h3 { margin: var(--sp-4) 0 var(--sp-2); font-size: var(--fs-300); }
.proof-info ol { margin: 0; padding-left: var(--sp-5); }
.proof-info li { margin-bottom: var(--sp-2); }

/* Seven counters across. Fixed enough to stay a table, elastic enough to take a long header;
   "Approved Output Doc T" wraps rather than pushing the row into a scrollbar. */
/* Nine columns since Corrections 118/119 split "the document is wrong" from "the fields
   disagree" and added the score. The floor drops from 5.5rem to 4.75rem so nine still fit the
   1088px container without a scroller: 9 x 76 + 8 gaps = 692px, well inside it. The floor is what
   decides when .table-scroll takes over, and every one of these cells holds a short number or a
   percentage - the widest thing in the row is a heading, and headings wrap. */
.proof-summary-row { grid-template-columns: repeat(9, minmax(4.75rem, 1fr)); }
.proof-summary-row .data-table__cell { overflow-wrap: anywhere; }

/* Red is the wireframe's, and it means "this needs you", not "this is broken". Applied only when
   the figure is above zero - a red 0 would be an alarm about nothing. */
.stat-tile__value.is-alert,
.data-table__cell.is-alert { color: var(--color-danger); }

/* --- Operator columns ------------------------------------------------------
 * Columns rather than an accordion. On a chain certificate these are the
 * parties to ONE claim and belong side by side; and because the tonnage they
 * print comes from a single field on the response, two columns cannot disagree
 * the way the old per-operator rows did. */
.cert-ops {
    display: grid;
    gap: var(--sp-4);
    grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
}
.cert-ops[hidden] { display: none; }

/* Mark on the left, identity beside it. align-items: start so a tall logo does not drag the name
   down to its own middle, and min-width: 0 on the text so a long company name wraps rather than
   pushing the logo out of the card. */
/* The approval banner, shared by the profile and the dashboard. The inline styles it carried on
   the profile page came with it into a fragment two screens read, and an inline rule cannot have
   a [hidden] twin - so they are classes now. */
.kyc-status__title { font-size: var(--fs-400); margin-bottom: var(--sp-2); }
.kyc-status__message { margin: 0; }
.kyc-status__reason { margin: var(--sp-3) 0 0; }
.kyc-status__reason[hidden] { display: none; }
.kyc-status__submit { margin-top: var(--sp-3); }
.kyc-status__submit[hidden] { display: none; }

/* Grid rather than flex, and that is not a style choice.
   The box has to be square AND as tall as the two lines beside it, so its height comes from a
   sibling and its width from that height. In a flex row an item stretched to the cross size still
   sizes its width from its own content, so aspect-ratio had nothing definite to work from and the
   box came out 32x54. A grid row gives the item a definite block size, and aspect-ratio resolves
   the inline size from it - measured 54x54. */
.cert-op__head {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: stretch;
    gap: var(--sp-3);
    margin-bottom: var(--sp-2);
}
.cert-op__head[hidden] { display: none; }
.cert-op__ident { min-width: 0; }
.cert-op__ident > :first-child { margin-top: 0; }
.cert-op__ident > :last-child { margin-bottom: 0; }

/* Contained rather than cropped: a logo is someone's mark and must not be trimmed to fit a box.
   Fixed track, so two cards side by side line their names up instead of each starting wherever
   its own logo happened to end. */
/* Square, and as tall as the two lines beside it. aspect-ratio rather than a width, so the box
   follows the text: if a long company name wraps, the box grows with it and stays square.
   A logo of its own is letterboxed inside - the seeded wordmarks are 2.5:1, which is why the mark
   looked thin in a box that was the right height all along. */
.cert-op__logo-box {
    display: flex;
    align-items: center;
    justify-content: center;
    position: relative;
    align-self: stretch;
    height: 100%;
    aspect-ratio: 1;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    overflow: hidden;
}
.cert-op__logo-box[hidden] { display: none; }

/* A mark brings its own shape. The frame belongs to the empty state, and drawn behind a logo it
   showed as a rim past the logo's corners - something sitting behind it rather than a box it is
   sitting in. */
.cert-op__logo-box.is-filled {
    background: none;
    border-color: transparent;
    padding: 0;
}

/* A link only while it has somewhere to go, which is the same rule the stat tiles use. */
a.cert-op__logo-box:not([href]) { cursor: default; }
a.cert-op__logo-box[href]:hover { border-color: var(--c-blue); }
a.cert-op__logo-box[href]:focus-visible { outline: 3px solid var(--c-blue); outline-offset: 2px; }

/* Taken out of flow on purpose. In flow, a 180px mark made the grid row 180px tall and the box
   then sized itself to that - the height was supposed to come from the two lines of text, and
   instead the picture was deciding it. Absolute, the image contributes nothing to the row and the
   text is left as the only thing that sets the height; contain still letterboxes any ratio. */
.cert-op__logo {
    position: absolute;
    inset: var(--sp-1);
    width: auto;
    height: auto;
    object-fit: contain;
}
.cert-op__logo[hidden] { display: none; }

/* The branch that renders while logoUrl is null, so it has to look like a decision. */
.cert-op__initials { font-size: var(--fs-300); font-weight: 700; color: var(--c-blue-dark); }
.cert-op__initials[hidden] { display: none; }

.cert-op {
    padding: var(--sp-4);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    background: var(--color-surface);
}
.cert-op[hidden] { display: none; }

.cert-op__role {
    margin: 0 0 var(--sp-1);
    font-size: var(--fs-100);
    color: var(--color-text-muted);
}
.cert-op__name { margin: 0 0 var(--sp-1); font-size: var(--fs-400); font-weight: 700; }

/* The darker brand blue: a company name is a link here, and cyan on white is
   about 2.2:1 while the brand green is 2.4 - both fail AA at any weight. */
.cert-op__link { color: var(--c-blue-dark); }
.cert-op__link[hidden] { display: none; }

.cert-op__country { margin: 0; color: var(--color-text-muted); }
.cert-op__tonnes { margin: var(--sp-2) 0 0; font-weight: 600; font-variant-numeric: tabular-nums; }
.cert-op__tonnes[hidden] { display: none; }
.cert-op__price { margin: 0; font-variant-numeric: tabular-nums; }
.cert-op__price[hidden] { display: none; }
.cert-op__outputs { margin: var(--sp-3) 0 0; }
.cert-op__outputs[hidden] { display: none; }
.cert-op__outputs-label { display: block; font-size: var(--fs-100); color: var(--color-text-muted); }

/* --- Price box additions ---------------------------------------------------- */

/* Per kilo, under the per-tonne headline. Four decimals, so two offers a
   hundredth of a cent apart stay two offers. */
.cert-price__unit {
    margin: 0 0 var(--sp-4);
    color: var(--color-text-muted);
    font-variant-numeric: tabular-nums;
}
.cert-price__unit[hidden] { display: none; }

.input-pair__suffix { align-self: center; color: var(--color-text-muted); white-space: nowrap; }

/* Appears only once a quantity is typed, as drawn. The unit price, never a
   running total - only the server prices an order. */
.cert-order__live {
    margin: var(--sp-2) 0 0;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}
.cert-order__live[hidden] { display: none; }

/* --- Pricing breakdown ------------------------------------------------------ */

.cert-pricing__nominal,
.cert-pricing__line {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--sp-4);
    margin: var(--sp-3) 0 0;
}
.cert-pricing__nominal[hidden],
.cert-pricing__line[hidden] { display: none; }

.cert-pricing__line--total {
    padding-top: var(--sp-3);
    border-top: 2px solid var(--color-border);
    font-size: var(--fs-400);
}

/* Beside the order rather than inside the Pricing fold: that fold explains what a tonne COSTS, and
   this answers a different question - where the money goes. --fit rather than the global
   min-width:max-content, because two columns in a narrow aside would otherwise scroll. */
.cert-split { margin-top: var(--sp-5); }
.cert-split[hidden] { display: none; }
.cert-split__title { margin: 0 0 var(--sp-3); font-size: var(--fs-300); }

/* Named for this page, not for their shape. .split-row was already taken - layout.css uses it for
   "identity on the left, action on the right", which is display:flex, and because layout.css loads
   after base.css it beat .data-table__row's display:grid on equal specificity. The table looked
   like a table and laid out like a stack, silently. A shape class is a shared name; a table's
   column widths are not a shape. */
.cert-premium-row { grid-template-columns: minmax(0, 1fr) 6rem 6rem; }
.cert-split-row { grid-template-columns: minmax(0, 1fr) 5rem; }

/* --- Evidence --------------------------------------------------------------- */

.proof-operator { margin-bottom: var(--sp-5); }
.proof-operator[hidden] { display: none; }
.proof-operator__name {
    margin: 0 0 var(--sp-2);
    font-size: var(--fs-400);
    color: var(--c-blue-dark);
}

.proof-block__ask { margin: 0 0 var(--sp-2); }
.proof-block__ask[hidden] { display: none; }

/* Eight columns, and they FIT - the band moved into the left column of the sheet, so the full
   sheet width is no longer there to fall back on. It used to keep the global min-width:max-content
   and scroll inside .table-scroll; measured after the move, that was 1041px of table in an 886px
   column, 155px of sideways scrolling at 1440 and 219px at 1280.

   Four columns hold something whose width is known - an ISO date, a weight, two more dates, a
   button - so they are fixed, and the three that hold names and types share what is left. Floors
   total 47.5rem, so the table hands over to .table-scroll below about 770px rather than breaking
   a date across two lines. */
.proof-doc-row {
    grid-template-columns:
        7.5rem                /* Date of doc. - "2026-04-30" on one line */
        minmax(5.5rem, 1fr)   /* Supplier or buyer */
        minmax(5.5rem, 1fr)   /* Doc type */
        minmax(6.5rem, 1.3fr) /* Material - "Cardboard" is one word and was breaking */
        5.5rem                /* KG */
        6.5rem                /* Period from */
        6.5rem                /* Period to */
        8rem;                 /* View - holds "view" over "request", both unbroken */
}

/* A fixed track does not stop a long unbroken token from being drawn past its edge, and the cell
   backgrounds are opaque, so the overflow lands under the next column instead of scrolling into
   view. "anywhere" rather than "break-word": only anywhere also lowers the min-content
   contribution, which is the half that keeps the track at the width it was given. */
.data-table--proof-docs .data-table__cell { overflow-wrap: anywhere; }

/* The total belongs to the table, not to a row of data: it is the sum over the
   whole predicate rather than over the rows on screen, and it has to look like
   a different kind of number. */
.data-table__foot .data-table__cell {
    background: var(--color-bg);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}
.data-table__foot[hidden] { display: none; }

/* ---------------------------------------------------------------------------
   Operator profile: the material-flow tables and the in-card sub-heading.
   --------------------------------------------------------------------------- */

/* Column widths, named for the screen they belong to rather than for their shape.
   A shared shape name (.split-row) once landed on a table row and beat
   .data-table__row's display:grid at equal specificity - the table stacked. */
.flow-row { grid-template-columns: 1.2fr 1.2fr 1.3fr 1.4fr 0.9fr; }
.public-flow-row { grid-template-columns: 1.2fr 1.2fr 1.3fr 1.4fr; }

/* A heading inside a card, below the card's own <h2>. Was an inline font-size; a
   class so the three that now use it cannot drift apart. */
.subhead {
    font-size: var(--fs-400);
    margin: 0 0 var(--sp-3);
}

/* ==========================================================================
   Column widths for the operator's two work queues
   ==========================================================================

   These two tables were the only ones in the product that still needed sideways scrolling on a
   1440px desktop: +743px on the proof queue, +1020px on payouts. Measured, not estimated.

   The cause is worth writing down, because the shape is easy to reproduce by accident.
   .data-table carries min-width:max-content, and a row is ITSELF a grid - so measuring the row at
   max-content makes the grid algorithm pick ONE flex fraction, the worst ratio in the row
   (max-content / fr), and then hand that fraction to every track. The payout row holds 1113px of
   content across five columns and demanded 2108px: a 618px sentence in a 2fr column priced 1fr at
   309px, so a 115px date was given 371px. It is not a floor, it is a multiplier.

   The fix is per table, deliberately, and NOT a change to the global default: .data-table--audits
   sets white-space:nowrap on every cell and .data-table--proof-docs says in its own comment that it
   relies on max-content and scrolls instead - flipping the default would break both silently. So
   these two tables opt out with the existing --fit modifier and then declare real columns.

   Fixed rem for anything whose width is known - a badge, a date, a sum, a button. minmax(floor, Nfr)
   for text that should absorb the slack. The floor is what hands the table over to .table-scroll on
   a genuinely narrow screen instead of crushing a column into one letter per line. */

/* Status, Amount and Date hold a badge, a sum and an ISO date; every pixel above the widest of
   those is stolen from the note, which is the only cell that explains why a payout is stuck.
   The order follows the customer's own wireframe (Corrections 140): state first, then the number.
   The Stripe column's heading is now four words rather than two, but its FLOOR still comes from the
   transfer id underneath it - the heading wraps at its spaces, an id cannot. */
.payout-queue-row {
    grid-template-columns: 8rem 9rem 7.5rem minmax(11rem, 1fr) minmax(10rem, 2fr);
}

/* Eight columns. In or Out, kg, Status and View Doc are the width of the widest thing they can
   ever hold; Material / Stage, Date, Counterparty and Invoice share what is left.
   The tracks are NOT sized by the headings - three of these got shorter in Corrections 107-112 and
   not one track moved, because in every one of those columns the widest thing is the row rather
   than the word above it. The last one DID move: it now holds two buttons instead of one. */
.proof-queue-row {
    grid-template-columns:
        6.5rem                /* In or Out - badge */
        minmax(8.5rem, 1.6fr) /* Material / Stage - the one heading longer than its own floor */
        7.5rem                /* kg - sized by a DECIMAL(14,3) value, never by the heading */
        minmax(7rem, 1.1fr)   /* Date */
        minmax(7rem, 1.6fr)   /* Counterparty - absorbs the slack */
        minmax(6rem, 1fr)     /* Invoice */
        10rem                 /* Status - a badge that must not wrap inside itself */
        9rem;                 /* View Doc - the two actions, stacked; see below */
}

/* The row's actions stack rather than sit side by side, and that is MEASURED rather than
   preferred: View is 71px and Withdraw is 108px, so a row needs 185px of content to hold both -
   a 13.5rem track. That pushes the eight floors to 1063px inside a 1088px container, which is
   inside it by 25px at 1440 and NOT inside it at 1280, where the widest table in the product
   would start scrolling sideways for the sake of one button. Stacked, the cell needs 132px and
   9rem is enough for both the buttons and the heading above them.
   The cost is written down too: a row that offers Withdraw is 105px tall instead of 72px. Only
   an unreviewed document offers it, so most rows keep their height. */
.proof-queue-row__actions {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--sp-1);
}
.proof-queue-row__actions[hidden] { display: none; }

/* The access-request table under it. Same widths it already carried inline, moved here so the
   header row and the row template cannot drift apart - they were two hand-synchronised strings. */
.proof-queue-request-row {
    grid-template-columns: minmax(8rem, 1.2fr) minmax(8rem, 1.4fr) 7rem 9rem 11rem;
}

/* A rejection reason or an open mismatch spans the row. minmax(0, 1fr) rather than 1fr: 1fr floors
   at min-content, so one unbroken word would widen the whole table again. */
.proof-queue-note-row { grid-template-columns: minmax(0, 1fr); }

/* ==========================================================================
   The enlarged photograph
   ==========================================================================

   A modal over the whole viewport. Every display rule below has its [hidden] twin, and the dialog
   itself is driven by el.hidden - there is no display toggle in the script. */

.lightbox {
    position: fixed;
    inset: 0;
    z-index: var(--z-modal, 250);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--sp-4);
    background: color-mix(in srgb, var(--c-navy) 93%, transparent);
}
.lightbox[hidden] { display: none; }

/* The frame is a column and it OWNS the height budget: the picture is told to fit whatever the bar
   and the caption leave, rather than being given a vh figure of its own. A hard max-height on the
   image is what pushes a caption off a short screen. */
/* fit-content, so the bar and the caption are the width of the PICTURE rather than of the
   viewport - a close button floating a few hundred pixels to the right of the photograph reads as
   part of the page behind, not as part of the dialog. */
.lightbox__frame {
    display: flex;
    flex-direction: column;
    gap: var(--sp-3);
    width: fit-content;
    max-width: min(96vw, 72rem);
    max-height: 100%;
    min-height: 0;
}
.lightbox__frame[hidden] { display: none; }

.lightbox__bar {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: var(--sp-2);
}
.lightbox__bar[hidden] { display: none; }

.lightbox__counter {
    margin-right: auto;
    color: #fff;
    font-size: var(--fs-200);
    font-variant-numeric: tabular-nums;
}
.lightbox__counter[hidden] { display: none; }

.lightbox__nav,
.lightbox__close {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.5rem;
    height: 2.5rem;
    padding: 0;
    border: 1px solid color-mix(in srgb, #fff 45%, transparent);
    border-radius: 999px;
    background: transparent;
    color: #fff;
    font-size: var(--fs-400);
    line-height: 1;
    cursor: pointer;
}
.lightbox__nav[hidden],
.lightbox__close[hidden] { display: none; }
.lightbox__nav:hover,
.lightbox__close:hover { background: color-mix(in srgb, #fff 18%, transparent); }
.lightbox__nav:focus-visible,
.lightbox__close:focus-visible { outline: 3px solid var(--c-yellow); outline-offset: 2px; }

/* min-height:0 on the flex item is what lets the picture actually shrink; without it a flex item's
   automatic minimum is its content size and a tall photograph pushes the caption off screen. */
.lightbox__img {
    min-height: 0;
    max-width: 100%;
    object-fit: contain;
    border-radius: var(--radius);
    background: var(--c-navy);
}
.lightbox__img[hidden] { display: none; }

.lightbox__caption {
    margin: 0;
    color: #fff;
    font-size: var(--fs-200);
    text-align: center;
}
.lightbox__caption[hidden] { display: none; }

/* The page behind must not scroll under an open dialog. */
body.is-lightbox-open { overflow: hidden; }

/* --- the trigger ---------------------------------------------------------
   A real button around the thumbnail, so the keyboard reaches it and a screen reader announces it.
   The magnifier is drawn ALWAYS, not on hover: a hover affordance tells a touch screen nothing, and
   this is the only sign that the picture opens. */
.photo-tile__zoom {
    display: block;
    position: relative;
    width: 100%;
    padding: 0;
    border: 0;
    border-radius: var(--radius);
    background: none;
    cursor: zoom-in;
}
.photo-tile__zoom[hidden] { display: none; }
.photo-tile__zoom:focus-visible { outline: 3px solid var(--c-blue); outline-offset: 2px; }

.photo-tile__zoom::after {
    content: "\2315";
    position: absolute;
    right: var(--sp-2);
    bottom: var(--sp-2);
    display: flex;
    align-items: center;
    justify-content: center;
    width: 1.75rem;
    height: 1.75rem;
    border-radius: 999px;
    background: color-mix(in srgb, var(--c-navy) 72%, transparent);
    color: #fff;
    font-size: var(--fs-300);
    line-height: 1;
}
.photo-tile__zoom:hover::after { background: var(--c-navy); }

/* --- Dismissing something from the dashboard ------------------------------
   A small x that takes the item off the list. The state is per viewer and lives in localStorage:
   it is a display preference, not a fact about the business, and the checklist itself is derived
   live on every request precisely so that no row can go stale. Storing a dismissal in the ledger
   would put a second, drifting copy of the truth next to the first. */
.dismiss {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.75rem;
    height: 1.75rem;
    padding: 0;
    border: 0;
    border-radius: 999px;
    background: transparent;
    color: var(--color-text-muted);
    font-size: var(--fs-300);
    line-height: 1;
    cursor: pointer;
}
.dismiss[hidden] { display: none; }
.dismiss:hover { background: var(--color-border); color: var(--c-navy); }
.dismiss:focus-visible { outline: 3px solid var(--c-blue); outline-offset: 2px; }

/* Top right of a banner. Its anchor is .gate-banner's own position:relative, added there for this;
   .card sets no position, so without it this would climb to whatever ancestor happened to be
   positioned and land somewhere else on the page entirely. */
.dismiss--corner {
    position: absolute;
    top: var(--sp-2);
    right: var(--sp-2);
}

.todo-row__dismiss { display: flex; justify-content: flex-end; align-items: center; }
.todo-row__dismiss[hidden] { display: none; }

/* --- The dashboard's two indicator rows -----------------------------------
   A heading over each row of tiles, one step down from the page title: it labels a group rather
   than opening a section, and at full h2 size it competed with "Dashboard" right above it. */
.dash-row__heading {
    margin: var(--sp-5) 0 var(--sp-3);
    font-size: var(--fs-300);
    letter-spacing: .02em;
}

/* Six across on a wide screen, wrapping down to two on a phone. auto-fit with a floor rather than
   a fixed six, so the row never produces a tile too narrow to read its own label. */
.stat-grid--six { grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr)); }

/* Seven in a row, which is what the proofs page needs and --six cannot give it: MEASURED,
   the grid there is 1088px inside a 1120px container with a 16px gap, so seven tiles have
   1088 - 6*16 = 992px between them, 141px each. A 9.5rem (152px) floor is over that, so the
   seventh tile dropped to a row of its own - one orphan under six, which is not a row of
   indicators. 8.5rem is 136px: seven fit with room to spare and 1fr spends the rest. */
.stat-grid--seven { grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr)); }

/* The marketplace cards this operator is on. Material, sub-material and the chain partners absorb
   the slack; the service is a single vocabulary word and the two figures are known widths. */
.cards-row {
    grid-template-columns:
        minmax(7rem, 1.4fr)   /* Material */
        minmax(6rem, 1.2fr)   /* Sub-material */
        minmax(6rem, 1fr)     /* Service */
        7rem                  /* Avail. T */
        7rem                  /* Sold T */
        8rem                  /* Price $/kg */
        minmax(7rem, 1.4fr);  /* Chain with - company names, so it needs room to wrap */
}

/* Eight columns, and every one of them fits at 1280: the four figures and the button are widths
   that are known in advance, so only the three text columns compete for what is left. Measured
   rather than guessed - a fixed track does not stop a long word being drawn outside it, which is
   why the cells wrap anywhere as well. */
.stock-balance-row {
    grid-template-columns:
        minmax(6.5rem, 1.3fr) /* Material */
        minmax(6rem, 1.1fr)   /* Sub-material */
        minmax(5.5rem, 1fr)   /* Process */
        5.5rem                /* Input Ts */
        6.5rem                /* Output Ts - measured: the header wrapped at 5.5rem */
        8.5rem                /* Back orders - measured: the "?" dropped to a second line at 7.5rem */
        minmax(7.5rem, 1fr)   /* Shortage - "20.5 Output Ts", and its own "?" */
        11.5rem;              /* Action - measured: "Add output docs" wrapped at 10rem */
}

/* The button states the whole action or it states nothing useful; wrapping it to two lines made
   the row twice as tall as the ones without one. */
.stock-balance-row [data-role="sb-action"] { white-space: nowrap; }

/* What buyers are waiting for. Seven columns; the four fixed tracks are things whose width is
   known - a tonnage, a date, a button - and the three that carry a name get a floor and a share. */
.backorder-row {
    grid-template-columns:
        minmax(6.5rem, 1.3fr) /* Material */
        minmax(6rem, 1.1fr)   /* Sub-material */
        minmax(5.5rem, 1fr)   /* Process */
        minmax(5.5rem, 1fr)   /* Country */
        6.5rem                /* Wanted Ts */
        6.5rem                /* Since - an ISO date */
        11.5rem;              /* Action - measured on the stock table's identical button */
}
.data-table--fit .backorder-row .data-table__cell { overflow-wrap: anywhere; }
.backorder-row [data-role="bo-action"] { white-space: nowrap; }

/* One yard or depot. The map cell holds a link labelled "Open", not the URL - a pasted Google Maps
   link is 60-odd characters of nothing anyone reads, and it drove the whole table into a scroll. */
.location-row {
    grid-template-columns:
        minmax(9rem, 2fr)   /* Name */
        minmax(7rem, 1fr)   /* Type */
        5rem                /* Map - one short link */
        7rem;               /* Actions - measured: the Remove button needs 108px in its cell */
}

.data-table--stock-balance .data-table__cell { overflow-wrap: anywhere; }

/* Headers are short and known, and their widths were measured against these tracks; only the body
   needs to break. Without this the "?" beside "Back orders" dropped to a second line and made the
   whole header row two lines tall. */
.data-table--stock-balance .data-table__head .data-table__cell { overflow-wrap: normal; white-space: nowrap; }

/* The missing half is the point of the row, so it does not fade into the figures beside it. */
.stock-balance-row__shortage { color: var(--color-danger); font-weight: 600; }

/* Four counts, evenly. Nothing here is wide, so nothing needs a floor above the figures. */
.proof-status-row { grid-template-columns: repeat(4, minmax(6rem, 1fr)); }

/* A count is a link to the list it counts, and at zero it is not a link at all - same rule as the
   indicator tiles: an <a> with no href is not focusable and offers nothing to click. */
.proof-status-row a[href] { color: var(--c-blue-dark); }
.proof-status-row a:not([href]) { color: inherit; text-decoration: none; cursor: default; }
.proof-status-row .is-issue { color: var(--color-danger); font-weight: 600; }

/* Section headings on the operator dashboard sit one step below the page title.
   At the shared h2 size they read as competing with "Dashboard" directly above them; this page has
   six of them and the title has to stay the loudest thing on it. Scoped to the page, so no other
   screen's heading rhythm moves. */
#operatorDashboard h2 { font-size: var(--fs-400); }

/* Air above each section. The two headings that had it carried an inline margin, and it went with
   the sections CLAUDE.md section 6 had removed; a heading welded to the table above it reads as
   part of that table. The page title supplies its own gap, so the first one is exempt. */
#operatorDashboard h2:not(:first-of-type) { margin-top: var(--sp-6); }

/* ------------------------------------------------------------------ proof upload: page viewer

   The document sits beside the form while the operator checks what was read off it, and the boxes
   that will be burned into the shared copy are drawn on top of it. Every rule that sets a display
   here has a [hidden] counterpart, because an inline display rule cannot have one and these are
   all elements that get hidden. */

.doc-viewer { display: block; }
.doc-viewer[hidden] { display: none; }

.doc-viewer__bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-2);
    margin-bottom: var(--sp-2);
}
.doc-viewer__bar[hidden] { display: none; }

/* The positioning anchor for every box. Learned the hard way elsewhere in this file: .card does
   NOT carry position:relative, so a control that assumes an ancestor establishes a containing
   block lands somewhere else entirely. This one says so itself. */
.doc-viewer__stage {
    position: relative;
    display: block;
    background: var(--c-surface-2, #f3f3f1);
    border: 1px solid var(--c-border);
    border-radius: var(--radius-sm);
    overflow: hidden;
    touch-action: none; /* the stage is a drawing surface; panning would fight every drag */
    cursor: crosshair;
}
.doc-viewer__stage[hidden] { display: none; }

.doc-viewer__img {
    display: block;
    width: 100%;
    height: auto;
    user-select: none;
    -webkit-user-drag: none;
}
.doc-viewer__img[hidden] { display: none; }

.doc-viewer__boxes {
    position: absolute;
    inset: 0;
    display: block;
}
.doc-viewer__boxes[hidden] { display: none; }

.doc-viewer.is-loading .doc-viewer__stage { opacity: 0.5; }

/* Positioned from custom properties the editor sets per box, in PERCENT rather than pixels: the
   same rectangle then holds its place when the window is resized or the page is drawn at another
   width. A pixel value would pin the box to whatever size this browser happened to use. */
.redaction-box {
    position: absolute;
    left: var(--box-x, 0);
    top: var(--box-y, 0);
    width: var(--box-w, 0);
    height: var(--box-h, 0);
    background: rgba(11, 45, 68, 0.88);
    border: 2px solid var(--c-navy);
    border-radius: 2px;
    cursor: move;
    touch-action: none;
}
.redaction-box[hidden] { display: none; }

.redaction-box.is-selected {
    outline: 2px solid var(--c-yellow);
    outline-offset: 1px;
}

.redaction-box.is-drawing {
    background: rgba(11, 45, 68, 0.45);
    border-style: dashed;
    pointer-events: none;
}

/* Under the box rather than inside it: a label drawn on top of the fill would be the one piece of
   text on a redacted area, and the point of the area is that it carries none. */
.redaction-box__label {
    position: absolute;
    left: 0;
    top: 100%;
    /* Decoration, not a control. It hangs below its own box and therefore lands on top of whatever
       is beneath - on a dense invoice that is the next box's remove cross, which then cannot be
       clicked at all. Found by a browser run, where Chrome reported the label as intercepting the
       pointer; nothing about the rendering looked wrong. */
    pointer-events: none;
    /* Shown only while this box is selected or under the pointer. Fifty-five boxes on one page of
       the sample set meant fifty-five yellow captions, and they covered more of the document than
       the redactions did - the operator could no longer read the thing they were being asked to
       check. Seen in a screenshot; no assertion would have caught it. */
    display: none;
    font-size: 0.6875rem;
    line-height: 1.2;
    white-space: nowrap;
    color: var(--c-navy);
    background: var(--c-yellow);
    padding: 0 0.25rem;
    border-radius: 0 0 2px 2px;
}

.redaction-box__remove {
    position: absolute;
    right: -0.5rem;
    top: -0.5rem;
    width: 1.25rem;
    height: 1.25rem;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    font-size: 0.75rem;
    line-height: 1;
    color: var(--c-navy);
    background: var(--c-yellow);
    border: 1px solid var(--c-navy);
    border-radius: 50%;
    cursor: pointer;
}
.redaction-box__remove[hidden] { display: none; }

/* One cross at a time. Every box carried its own, positioned just outside its corner, and on a
   dense invoice - fifty-five boxes on a single page of the sample set - each cross sat on top of a
   neighbouring box, so the one you aimed at was never the one that received the click. Showing it
   only on the selected box means there is exactly one delete target on the page, which is also the
   only way the control is unambiguous to describe. */
.redaction-box:not(.is-selected) .redaction-box__remove { display: none; }

.redaction-box.is-selected .redaction-box__label,
.redaction-box:hover .redaction-box__label { display: block; }
.redaction-box .redaction-box__label[hidden] { display: none; }

/* The reader's verdict, when it has one worth interrupting for. */
/* A column, so the headline sits above the reader's sentence rather than beside it. With the
   label hidden - the accepted and unchecked cases - this draws exactly as the single-child row
   did. */
.staging-verdict {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--sp-1);
    padding: var(--sp-3);
    margin-bottom: var(--sp-3);
    border-radius: var(--radius-sm);
    border-left: 4px solid var(--c-yellow-dark);
    background: rgba(253, 195, 0, 0.12);
}
.staging-verdict[hidden] { display: none; }

.staging-verdict.is-ok {
    border-left-color: var(--c-green-dark);
    background: rgba(175, 202, 11, 0.12);
}

.staging-verdict.is-unknown {
    border-left-color: var(--c-gray);
    background: rgba(157, 157, 156, 0.12);
}

.staging-verdict__text { margin: 0; }

.staging-verdict__label {
    margin: 0;
    font-weight: 700;
    color: var(--c-yellow-dark);
}
.staging-verdict__label[hidden] { display: none; }
.staging-verdict.is-ok .staging-verdict__label { color: var(--c-green-dark); }
.staging-verdict.is-unknown .staging-verdict__label { color: var(--color-text-muted); }

/* --- The impact claims on a proof document -------------------------------
 * One flat list of four; the pull-down that asked post-consumer / post-industrial / both is gone
 * (Corrections 122-127). Each row carries what the claim is worth, when an admin has priced it. */
.impact-list {
    display: grid;
    gap: var(--sp-2);
    margin-bottom: var(--sp-4);
}
.impact-list[hidden] { display: none; }

/* Three tracks: the box, the words, the premium hard right. minmax(0, 1fr) rather than 1fr so a
   long claim WRAPS instead of pushing the premium out of the column - this is the middle of three
   columns and the narrowest place on the page. align-items: start rather than center because these
   labels are sentences, and centring a wrapped one would drag the tick box into the middle of it. */
.impact-row {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: start;
    gap: var(--sp-2);
    cursor: pointer;
}
.impact-row[hidden] { display: none; }

.impact-row__label { color: var(--c-navy); }

/* A figure, not a badge: it is money, and four coloured pills down one column would read as four
   statuses. Tabular numerals so 10% and 50% line up under each other. */
.impact-row__premium {
    color: var(--c-green-dark);
    font-weight: 700;
    font-size: var(--fs-200);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.impact-row__premium[hidden] { display: none; }

.redaction-legend {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2);
    margin-top: var(--sp-2);
}
.redaction-legend[hidden] { display: none; }

/* What to look for, from config_items / REDACTION_FIELD. The operator can untick a category, and
   unticking removes its boxes - so what is on the page is always what will be burned in. */
/* The category of the box currently selected. Its own row under the page rather than a control
   inside the rectangle, which on a dense document is a few millimetres tall and sits under a
   neighbour's label. */
.redaction-selected {
    display: grid;
    gap: var(--sp-1);
    margin-top: var(--sp-3);
}
.redaction-selected[hidden] { display: none; }

.redaction-cats {
    display: grid;
    gap: var(--sp-1);
    margin-top: var(--sp-2);
}
.redaction-cats[hidden] { display: none; }

.redaction-cats__row {
    display: flex;
    align-items: flex-start;
    gap: var(--sp-2);
}
.redaction-cats__row[hidden] { display: none; }

/* The holder's name and, beside it, the QR through to their own page. A row that wraps rather than
   a fixed pair: the name is a heading and can be two lines wide on a narrow screen, and at that
   point the code belongs underneath it rather than squeezed alongside. */
.holder-row {
    display: flex;
    align-items: center;
    /* Centred, because everything else in this masthead is. The holder used to be a lone centred
       h1; turning it into a row left it hanging on the left of a centred column, which reads as a
       layout fault rather than a name. */
    justify-content: center;
    gap: var(--sp-4);
    flex-wrap: wrap;
    margin: 0 0 var(--sp-5);
}
.holder-row[hidden] { display: none; }
.holder-row__qr {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    max-width: 11rem;
    text-align: left;
    text-decoration: none;
    color: var(--color-text-muted);
    font-size: var(--fs-100);
    line-height: 1.3;
}
.holder-row__qr[hidden] { display: none; }
.holder-row__qr img { display: block; border-radius: var(--radius-sm); }
.holder-row__qr:hover span,
.holder-row__qr:focus-visible span { text-decoration: underline; }

/* A proportion drawn as a bar. The width is the datum and is set per instance from a script; only
   the track and the ink are a rule, which is the line CLAUDE.md 6 draws - a VALUE may be inline, a
   display rule may not, because an inline display rule can have no [hidden] twin. */
.meter {
    height: 8px;
    border-radius: 999px;
    background: var(--color-border);
    overflow: hidden;
    margin: var(--sp-2) 0;
}
.meter[hidden] { display: none; }
.meter__fill { display: block; height: 100%; width: 0; background: var(--c-green); }

/* The published certificates on a buyer's public page. The number and the date are fixed widths -
   a certificate number is always KXC-YYYY-######## and a date is three short words - and the two
   descriptive columns take what is left. Tonnes is a figure, so it gets the narrowest track. */
.buyer-cert-row {
    grid-template-columns:
        minmax(13rem, 1.2fr)  /* Certificate number, plus a Revoked badge when there is one */
        minmax(9rem, 1.4fr)   /* Material - em dash and a sub-material make this the longest */
        minmax(8rem, 1fr)     /* Recovery path */
        7.5rem                /* Issued */
        6rem;                 /* Tonnes */
}

/* A whole row is the link, so it has to answer to hover and to the keyboard like one. */
a.data-table__row.buyer-cert-row { color: inherit; text-decoration: none; }
a.data-table__row.buyer-cert-row:hover { background: var(--color-surface-alt); }
a.data-table__row.buyer-cert-row:focus-visible {
    outline: 3px solid var(--c-blue);
    outline-offset: -3px;
}
/* The number carries the link colour so a reader can see the row is followable while standing
   still. A hover background alone only tells somebody who is already pointing at it. */
.buyer-cert-row__number {
    font-family: var(--font-mono);
    font-size: var(--fs-100);
    color: var(--c-blue-dark);
    font-weight: 600;
}
a.data-table__row.buyer-cert-row:hover .buyer-cert-row__number { text-decoration: underline; }
/* A withdrawn claim is still listed; it just stops looking like one that stands. */
.buyer-cert-row.is-revoked .buyer-cert-row__number { text-decoration: line-through; }

/* The credited companies sit right under a table, and a heading butted against a table edge reads
   as part of it. The gap is on the heading rather than on the table, so the table keeps whatever
   spacing it has everywhere else it is used. */
.buyer-page__operators-head { margin-top: var(--sp-7); }

/* A credited company's tile is a link when that company has a public page. Colour is inherited -
   the name is already the strongest thing in the tile and turning it blue would fight the mark
   beside it - so the affordance is the hover lift and the focus ring. */
a.operator-tile { color: inherit; text-decoration: none; }
a.operator-tile[href]:hover { border-color: var(--c-blue); }
a.operator-tile[href]:hover [data-role="name"] { text-decoration: underline; }
a.operator-tile[href]:focus-visible { outline: 3px solid var(--c-blue); outline-offset: 2px; }
/* No page, no pointer: the tile still renders, it simply is not a link. */
a.operator-tile:not([href]) { cursor: default; }


/* --- Waste Footprint demand (admin) -------------------------------------------------------- */

/* Nine columns, sized against the widest thing each one actually holds rather than against its
   heading. Fixed rem only where the width is known - a date, a badge, a tonnage, a select and a
   button; the requester block, the country and the note absorb the slack. Measured at the
   container--wide width: the fixed tracks come to 48.5rem and the three floors to 27rem, so the
   row asks for 1208px and the 1280px viewport gives its container 1232px. */
.wfs-demand-row {
    grid-template-columns:
        7.75rem               /* Asked - an ISO date. Measured: the text is 93px and 7.5rem left
                                 96px of content after the cell's own 24px of padding, so a
                                 subpixel difference decided whether a row broke in half */
        minmax(11rem, 2fr)    /* Who asked - name over e-mail over company, and an e-mail is one word */
        8.5rem                /* Type - "Organisation" inside a badge, which must not wrap in itself.
                                 Measured: the badge needs 111px and 8.25rem offered 108 */
        minmax(7.5rem, 1fr)   /* Country - "Netherlands" broke in half at 6.5rem */
        8.25rem               /* Uncollected t - sized by its HEADING, which measures 105px; the
                                 figures underneath would have fitted in half of it */
        8rem                  /* Unrecycled t */
        minmax(11.5rem, 2fr)  /* What we did, and the note about it - a native select reserves room
                                 for its arrow, so "Not looked at" needs more than its own width */
        9rem;                 /* Invite as buyer - the button wraps to two lines by design, so
                                 its floor is its longest word rather than its whole label */
}

/* An e-mail address is a single unbreakable token: without this it draws straight through the
   neighbouring cell, whose background is opaque, so it does not even reach the scroller. Scoped to
   the one cell that holds one: breaking anywhere is wrong for a date and pointless for a figure. */
.wfs-demand-row__who { overflow-wrap: anywhere; }

/* A date is one value and never breaks. The track is wide enough for it; this stops a rounding
   difference from being the thing that decides. */
.wfs-demand-row [data-role="asked"] { white-space: nowrap; }

/* Three facts about one person, stacked. They are one cell because they are one answer to "who
   asked", and three columns would have cost the note its width. */
.wfs-demand-row__who > * { display: block; }
.wfs-demand-row__who .muted { font-size: var(--fs-100); }

/* The controls sit inside a table cell and must not push it wider than its track. */
.wfs-demand-row .select,
.wfs-demand-row .input { width: 100%; min-width: 0; }

/* Stacked, not side by side: at this width they would each get half of not enough. */
.wfs-demand-row__decision { display: grid; gap: var(--sp-2); align-content: start; }
.wfs-demand-row__decision[hidden] { display: none; }

.wfs-material-row {
    grid-template-columns:
        minmax(12rem, 2fr)    /* Material, with the No mapping badge beside it */
        7rem                  /* Requests */
        8rem                  /* Uncollected t */
        8rem                  /* Unrecycled t */
        12rem;                /* Open in marketplace */
}
.wfs-material-row .data-table__cell { overflow-wrap: anywhere; }
