﻿/* =========================================================================
   Scrum Boards (2026-08-07; retrofitted onto the house design system
   2026-08-09 "designsystem"; the wall's sticky-note look restored 2026-08-10).

   Generalized from oc-feature-board.css. Prefix is ocsb- (the Feature Board's
   ocfb- rules stay in place until that page is retired, so the two never fight).

   Two surfaces share these tokens:
     .ocsb-wall-*   the pan/zoom canvas (RenderMode.Wall)
     .ocsb-col-*    plain kanban columns (RenderMode.Columns, the default)

   WHAT IS AND IS NOT IN THIS FILE. The page header, buttons, icon buttons,
   badges, chips, empty states, spinners, skeletons, cards and tables all come
   from the Components/Oc house set now, so their rules are gone from here. What
   remains is the board's OWN geometry — the wall canvas, the column layout, the card face
   and the small editor-row scaffolding the settings screens sit on. If you find
   yourself writing a button or a pill here, reach for OcButton / OcBadge first.

   THE WALL CARD IS THE ONE DELIBERATE EXCEPTION, and it is not negotiable: it is
   the Feature Board's CEO-approved realistic sticky-note design (2026-06-14),
   restored 2026-08-10 after the phase-1 port flattened it — handwriting fonts,
   per-paper gradients, tape, curled corners, tilt, and the time-in-section decay
   stages. A house component on that face would undo the one part of this feature
   the CEO signed off by eye. The paper/ink pairs are FROZEN tokens defined in
   oc-theme.css (--oc-note-N-top/-bot/-ink, each pair >= 4.5:1, identical in both
   themes — physical paper does not recolour when the room goes dark). The
   remaining raw values in the card rules are TRANSLUCENT EFFECT LAYERS only —
   alpha blacks/whites/sepias for shadow, sheen, stains, mold and cobwebs that
   composite onto the frozen paper and are exempt from theming for the same
   physical-paper reason (see the ocsb-card kind=leak entry in
   PartnerPortal.E2E/Resources/dark-audit-allowlist.json). No opaque colour may
   be added here — opaque colour belongs in oc-theme.css.
   Handwriting fonts (Caveat/Gochi/Kalam/Patrick/Permanent Marker/Reenie/Shadows)
   load via the head-global <link> in index.html (NOT @import — the deployed CSP).

   Colour rule everywhere else: --oc-* tokens only, never a raw hex — section
   accents come from ScrumBoardSection.ColorToken, which stores a token key for
   exactly this reason.
   ========================================================================= */

/* Sticky-paper effect tokens (structure, not colour). */
:root {
    /* THE "YOU CANNOT PUT THAT THERE" WALLPAPER — a tile of nineteen little warning
       signs taking the mickey out of the attempt (CEO 2026-08-21: "a wallpaper of all
       sorts of warning and comical phrases on signs that make fun at the idea of
       putting something there"). It replaced a single vertical stripe of skulls,
       which the CEO rightly called dumb.

       Used as a MASK — alpha only, the colour comes from the rule that applies it, so
       it follows the theme rather than being one fixed ink. Consequences of that, both
       load-bearing: the signs are LINE ART (stroked outlines with filled lettering)
       because in a mask anything opaque shows, so a "white" label on a solid sign
       would have been a blob; and the skulls' eye sockets are holes cut with
       fill-rule=evenodd for the same reason.

       ⚠️ THE TILE IS 280 WIDE AND EVERY SIGN SITS INSIDE x = 16..264. A column is 18rem
       (288px), so the second tile across contributes only its empty margin and no word
       is ever sliced in half. Widen a sign past that inset and it will be chopped at
       the column edge — which reads as a bug, not a pattern. */
    --ocsb-refused-wallpaper:
        url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 280 340'%3E%3Cg stroke='%23000' stroke-width='2.3' fill='none' stroke-linejoin='round'%3E%3Cg transform='translate(18,10) rotate(-5)'%3E%3Crect x='0' y='0' width='86' height='29' rx='4'/%3E%3Ctext x='43' y='20.5' font-family='Verdana,DejaVu Sans,sans-serif' font-size='13' font-weight='bold' text-anchor='middle' fill='%23000' stroke='none'%3ENOPE%3C/text%3E%3C/g%3E%3Cg transform='translate(130,8) rotate(6)'%3E%3Cpath d='M22 2 L43 38 H1 Z'/%3E%3Cpath d='M22 14 v11' stroke-linecap='round'/%3E%3Ccircle cx='22' cy='31' r='1.7' fill='%23000' stroke='none'/%3E%3C/g%3E%3Cg transform='translate(174,14) rotate(4)'%3E%3Crect x='0' y='0' width='88' height='29' rx='4'/%3E%3Ctext x='44' y='20.5' font-family='Verdana,DejaVu Sans,sans-serif' font-size='12' font-weight='bold' text-anchor='middle' fill='%23000' stroke='none'%3ENOT HERE%3C/text%3E%3C/g%3E%3Cg transform='translate(16,58) rotate(3)'%3E%3Crect x='0' y='0' width='118' height='29' rx='4'/%3E%3Ctext x='59' y='20.5' font-family='Verdana,DejaVu Sans,sans-serif' font-size='13' font-weight='bold' text-anchor='middle' fill='%23000' stroke='none'%3ENICE TRY%3C/text%3E%3C/g%3E%3Cg transform='translate(154,54)'%3E%3Ccircle cx='15' cy='15' r='14'/%3E%3Cpath d='M6 15 h18' stroke-linecap='round' stroke-width='4.5'/%3E%3C/g%3E%3Cg transform='translate(190,62) rotate(-6)'%3E%3Crect x='0' y='0' width='72' height='29' rx='4'/%3E%3Ctext x='36' y='20.5' font-family='Verdana,DejaVu Sans,sans-serif' font-size='12' font-weight='bold' text-anchor='middle' fill='%23000' stroke='none'%3EAS IF%3C/text%3E%3C/g%3E%3Cg transform='translate(20,104) rotate(-2)'%3E%3Crect x='0' y='0' width='190' height='29' rx='4'/%3E%3Ctext x='95' y='20.5' font-family='Verdana,DejaVu Sans,sans-serif' font-size='12' font-weight='bold' text-anchor='middle' fill='%23000' stroke='none'%3ECOMPUTER SAYS NO%3C/text%3E%3C/g%3E%3Cg transform='translate(230,104)'%3E%3Cpath d='M2 13 L11 2 L20 13' stroke-linecap='round'/%3E%3Cpath d='M2 22 L11 11 L20 22' stroke-linecap='round'/%3E%3C/g%3E%3Cg transform='translate(18,148) rotate(-4)'%3E%3Cpath d='M14 1 H38 L51 14 V38 L38 51 H14 L1 38 V14 Z'/%3E%3Ctext x='26' y='31' font-family='Verdana,DejaVu Sans,sans-serif' font-size='11' font-weight='bold' text-anchor='middle' fill='%23000' stroke='none'%3ESTOP%3C/text%3E%3C/g%3E%3Cg transform='translate(84,156) rotate(5)'%3E%3Crect x='0' y='0' width='138' height='29' rx='4'/%3E%3Ctext x='69' y='20.5' font-family='Verdana,DejaVu Sans,sans-serif' font-size='13' font-weight='bold' text-anchor='middle' fill='%23000' stroke='none'%3EWRONG WAY%3C/text%3E%3C/g%3E%3Cg transform='translate(232,146) scale(0.44)'%3E%3Cpath fill='%23000' stroke='none' fill-rule='evenodd' d='M32 6C19 6 9 15.4 9 27.2c0 7 3.7 13.1 9.4 16.6V50a5 5 0 0 0 5 5h17.2a5 5 0 0 0 5-5v-6.2C51.3 40.3 55 34.2 55 27.2 55 15.4 45 6 32 6Zm-9 15.9a5.3 5.3 0 1 0 0 10.6 5.3 5.3 0 0 0 0-10.6Zm18 0a5.3 5.3 0 1 0 0 10.6 5.3 5.3 0 0 0 0-10.6ZM32 35.5l4 8.5h-8l4-8.5Z'/%3E%3Cpath fill='%23000' stroke='none' d='M6 62h52v7H6z' transform='rotate(34 32 65)'/%3E%3Cpath fill='%23000' stroke='none' d='M6 62h52v7H6z' transform='rotate(-34 32 65)'/%3E%3C/g%3E%3Cg transform='translate(16,206) rotate(3)'%3E%3Crect x='0' y='0' width='176' height='29' rx='4'/%3E%3Ctext x='88' y='20.5' font-family='Verdana,DejaVu Sans,sans-serif' font-size='12' font-weight='bold' text-anchor='middle' fill='%23000' stroke='none'%3EABSOLUTELY NOT%3C/text%3E%3C/g%3E%3Cg transform='translate(200,202) rotate(-7)'%3E%3Crect x='0' y='0' width='62' height='29' rx='4'/%3E%3Ctext x='31' y='20.5' font-family='Verdana,DejaVu Sans,sans-serif' font-size='13' font-weight='bold' text-anchor='middle' fill='%23000' stroke='none'%3ENAH%3C/text%3E%3C/g%3E%3Cg transform='translate(22,252) rotate(-4)'%3E%3Crect x='0' y='0' width='112' height='29' rx='4'/%3E%3Ctext x='56' y='20.5' font-family='Verdana,DejaVu Sans,sans-serif' font-size='13' font-weight='bold' text-anchor='middle' fill='%23000' stroke='none'%3EKEEP OUT%3C/text%3E%3C/g%3E%3Cg transform='translate(152,246) rotate(-6)'%3E%3Cpath d='M22 2 L43 38 H1 Z'/%3E%3Cpath d='M22 14 v11' stroke-linecap='round'/%3E%3Ccircle cx='22' cy='31' r='1.7' fill='%23000' stroke='none'/%3E%3C/g%3E%3Cg transform='translate(196,256) rotate(6)'%3E%3Crect x='0' y='0' width='66' height='29' rx='4'/%3E%3Ctext x='33' y='20.5' font-family='Verdana,DejaVu Sans,sans-serif' font-size='13' font-weight='bold' text-anchor='middle' fill='%23000' stroke='none'%3ENO.%3C/text%3E%3C/g%3E%3Cg transform='translate(18,296) rotate(5)'%3E%3Crect x='0' y='0' width='130' height='29' rx='4'/%3E%3Ctext x='65' y='20.5' font-family='Verdana,DejaVu Sans,sans-serif' font-size='13' font-weight='bold' text-anchor='middle' fill='%23000' stroke='none'%3ENOT TODAY%3C/text%3E%3C/g%3E%3Cg transform='translate(160,292) scale(0.38)'%3E%3Cpath fill='%23000' stroke='none' fill-rule='evenodd' d='M32 6C19 6 9 15.4 9 27.2c0 7 3.7 13.1 9.4 16.6V50a5 5 0 0 0 5 5h17.2a5 5 0 0 0 5-5v-6.2C51.3 40.3 55 34.2 55 27.2 55 15.4 45 6 32 6Zm-9 15.9a5.3 5.3 0 1 0 0 10.6 5.3 5.3 0 0 0 0-10.6Zm18 0a5.3 5.3 0 1 0 0 10.6 5.3 5.3 0 0 0 0-10.6ZM32 35.5l4 8.5h-8l4-8.5Z'/%3E%3Cpath fill='%23000' stroke='none' d='M6 62h52v7H6z' transform='rotate(34 32 65)'/%3E%3Cpath fill='%23000' stroke='none' d='M6 62h52v7H6z' transform='rotate(-34 32 65)'/%3E%3C/g%3E%3Cg transform='translate(188,302) rotate(-5)'%3E%3Crect x='0' y='0' width='74' height='29' rx='4'/%3E%3Ctext x='37' y='20.5' font-family='Verdana,DejaVu Sans,sans-serif' font-size='12' font-weight='bold' text-anchor='middle' fill='%23000' stroke='none'%3EDENIED%3C/text%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
    --ocsb-grain:
        url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
    /* The board's handwriting. Defined here rather than as a global --oc-font-* because
       it is NOT "the app's hand font": the notes deliberately use DIFFERENT faces per
       paper (see --hand further down) and the WIP chips lead with Kalam. This one stack
       is the BAND LABEL's hand — its name, its count and its "+" — and those three must
       match each other or the heading stops reading as one piece of writing. */
    --ocsb-font-hand: "Caveat", "Comic Sans MS", cursive;
    /* The per-band "+" glyph box, and the gutter the label reserves for it. Tied together
       so the reserve and the button can never drift apart. */
    --ocsb-zone-add-size: 34px;
    --ocsb-zone-add-reserve: calc(22px + var(--ocsb-zone-add-size) + 12px);
    --ocsb-note-radius: 3px 5px 4px 6px;
    --ocsb-note-lift: 0 1px 0 rgba(0,0,0,.04),
                      8px 12px 16px -8px rgba(40,30,10,.30),
                      2px 3px 5px -2px rgba(40,30,10,.20);
}

.ocsb-page {
    display: flex;
    flex-direction: column;
    gap: .75rem;
    height: 100%;
    min-height: 0;
    font-family: var(--oc-font-sans);
}

/* Pushes what follows to the right inside any of the board's flex rows. */
.ocsb-toolbar-spacer { flex: 1 1 auto; }

.ocsb-subtitle {
    font-size: var(--oc-fs-sm);
    color: var(--oc-text-muted);
    margin: .125rem 0 0;
}

/* A trailing explanatory paragraph under an editor block. */
.ocsb-note { margin-top: .5rem; }

/* The view tabs (Board / Reporting / Activity, and the settings screen's five)
   used to be a hand-rolled .ocsb-tab strip. They are OcFilterChips now — the
   house component already emits role="tablist" with role="tab" + aria-selected,
   so it WAS the tab strip all along; there was never a second thing to build. */

/* ⚠️ THE VIEW TABS MUST NOT MOVE WHEN THE VIEW CHANGES (CEO 2026-08-15: "it is
   confusing when buttons move around on the screen").

   OcPageHeader stretches every child of its extra slot (.oc-page-head-extra > *
   { width: 100% }) so a lone KPI strip fills the gap between the name and the
   actions. On this page that slot holds TWO things and one of them — the zoom
   bar — exists only on the wall, so the tabs were re-sharing the space every
   time the view changed. Here the tabs keep their NATURAL width at the start of
   the slot and the zoom bar takes whatever is left, right-aligned. The zoom bar
   coming and going now changes nothing to the left of it.

   Scoped to .ocsb-page: this is a board-page override of a shared component, and
   no other page's extra slot should inherit it. Paired with a subtitle that no
   longer varies by view (BoardView.SubtitleText) — a longer sentence there grew
   the title block and moved these too. */
.ocsb-page .oc-page-head-extra {
    gap: .75rem;
}
.ocsb-page .oc-page-head-extra > .oc-chips {
    width: auto;
    flex: 0 0 auto;
}
.ocsb-page .oc-page-head-extra > .ocsb-zoombar {
    width: auto;
    flex: 1 1 auto;
    justify-content: flex-end;
}

/* ------------------------------------------------------------ search results */

/* ⚠️ THESE EXIST BECAUSE THEY DID NOT. The first version of the search page used
   .ocsb-search-row / .ocsb-search-title / .ocsb-search-results as if they were
   styled, and nothing anywhere defined them — so every result rendered as inline
   text and the whole page read as one run-on paragraph. The results are an
   OcDataTable per group now, which brings its own grid; what is left here is only
   the heading that separates the groups.

   The lesson, and it is the second time this month: a class name in markup is not
   a style. Check the stylesheet, or use a house component that carries its own. */

/* ⚠️ THE BOARD PICKER NEEDS A WIDTH FROM ITS HOST, OR IT DISAPPEARS.
   MultiSelectDropdown's button is width:100% of .multiselect-container, and the
   container has no width of its own — so in a flex row, where width comes from the
   content, the two size each other to nothing and the control renders as an
   invisible sliver. It looked like the board filter had simply not been built
   (CEO 2026-08-16: "i dont see a board selection"). Its other callers sit in grid
   cells that already have a width, which is why this only shows up here. */
.ocsb-filterbar .multiselect-container {
    min-width: 14rem;
    flex: 0 0 auto;
}

.ocsb-search-group {
    display: flex;
    align-items: baseline;
    gap: .5rem;
    margin: 1.5rem 0 .5rem;
    font-size: var(--oc-fs-h3);
    font-weight: 700;
    color: var(--oc-text);
}

/* The count beside the heading: same muted treatment as the subtitle it borrows. */
.ocsb-search-group .ocsb-subtitle {
    font-size: var(--oc-fs-sm);
    font-weight: 400;
}

/* Long titles are the norm on a to-do board, so the table scrolls sideways inside
   its own box rather than pushing the page wider. */
.ocsb-search-table {
    margin-bottom: 1rem;
}

/* The search page's date bounds. Narrow on purpose: four of them share a row with
   two labels, and a full-width date input would wrap the row on a laptop. */
.ocsb-search-date {
    width: auto;
    min-width: 9.5rem;
    flex: 0 0 auto;
}

/* The per-view sentence, moved out of the header's subtitle so that saying
   something longer pushes the board down instead of shifting the controls. */
.ocsb-view-note {
    margin: 0 0 .5rem;
    color: var(--oc-text-muted);
    font-size: var(--oc-fs-sm);
    max-width: 90ch;
}

/* --------------------------------------------------------------- zoom bar */

.ocsb-zoombar {
    display: inline-flex;
    align-items: center;
    gap: .25rem;
}

/* The drag engine writes the live percentage into this element by class name
   (scrum-board.js) — do not rename it. */
.ocsb-zoom-readout {
    min-width: 3.25rem;
    text-align: center;
    font-size: var(--oc-fs-xs);
    color: var(--oc-text-muted);
    font-variant-numeric: tabular-nums;
}

/* ------------------------------------------------------------- filter bar */

.ocsb-filterbar {
    display: flex;
    align-items: center;
    gap: .5rem;
    flex-wrap: wrap;
    padding: .5rem .625rem;
    border: 1px solid var(--oc-border);
    border-radius: var(--oc-radius-md);
    background: var(--oc-gray-50);
    font-size: var(--oc-fs-sm);
}

/* Names the group of chips that follows it. Not a <label>: it labels a chip
   strip, not one control, so associating it with an input would be a lie. */
.ocsb-filter-label {
    font-size: var(--oc-fs-xs);
    color: var(--oc-text-muted);
}

/* --------------------------------------------------------- board list page */

.ocsb-board-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
    gap: .75rem;
}

/* Each tile is an <a> wrapped around an OcCard, so a board is a real link
   (middle-click, open-in-new-tab) while the chrome stays the house card. */
.ocsb-board-link {
    display: block;
    height: 100%;
    text-decoration: none;
    color: inherit;
}

.ocsb-board-card {
    height: 100%;
    transition: box-shadow var(--oc-dur-fast) var(--oc-ease);
}

.ocsb-board-link:hover .ocsb-board-card { box-shadow: var(--oc-shadow-lg); }

.ocsb-board-card-meta {
    display: flex;
    gap: .375rem;
    flex-wrap: wrap;
}

.ocsb-section-group {
    margin: 0 0 .5rem;
    font-size: var(--oc-fs-sm);
    font-weight: 600;
    color: var(--oc-text-muted);
}

/* The "New board" form inside an OcCard: a stacked column of fields. */
.ocsb-inline-form .oc-card-body {
    display: flex;
    flex-direction: column;
    gap: .5rem;
}

/* ------------------------------------------------------------ wall surface */

.ocsb-wall-viewport {
    position: relative;
    flex: 1 1 auto;
    min-height: 24rem;
    overflow: hidden;
    /* Required: the engine handles all gestures itself, so the browser must not
       claim pan/zoom or the drag fights native scrolling on touch. */
    touch-action: none;
    border: 1px solid var(--oc-border);
    border-radius: var(--oc-radius-lg);
    background: var(--oc-gray-50);
    cursor: grab;
}

.ocsb-wall-viewport.is-panning { cursor: grabbing; }

.ocsb-wall-canvas {
    position: absolute;
    top: 0;
    left: 0;
    transform-origin: 0 0;
    will-change: transform;
}

/* Lane zones — coloured vertical bands. The accent comes from the section's
   ColorToken via an inline --ocsb-accent (BoardView emits it; sections without a
   saved colour get a deterministic default cycle), so every board has coloured
   columns out of the box, like the Feature Board did. */
.ocsb-zone {
    position: absolute;
    top: 0;
    --ocsb-accent: var(--oc-gray-500);
    /* Clear divider line between stages — you can still stick a note across it
       (notes float above the zones). */
    border-left: 2px solid color-mix(in srgb, var(--oc-text) 16%, transparent);
    background: color-mix(in srgb, var(--ocsb-accent) 6%, transparent);
    transition: background var(--oc-dur-fast) var(--oc-ease);
}

/* Drop highlight deepens the lane's OWN colour rather than flashing generic blue. */
.ocsb-zone.is-drop-target { background: color-mix(in srgb, var(--ocsb-accent) 18%, transparent); }

/* A band the card IN HAND cannot land in (unified My view wall: the home board has
   no column of that status). Dimmed while dragging, and a release over it is a
   CANCEL — the wall engine reverts before any server call, matching the columns
   engine's .ocsb-col.is-drop-disallowed contract. */
.ocsb-zone.is-drop-disallowed { opacity: .4; }

/* ⛔ THE REFUSAL, on the one column the hand is actually over (CEO 2026-08-21).
   The dimming above says "not these"; this papers THIS one — the column the pointer
   is actually on — with warning signs, so the answer arrives where the hand is
   asking rather than as a general wash across the board.

   Set by BOTH engines while dragging, and only ever on the unified My view: only
   that surface writes data-allowed-sections onto its cards, so on a per-board board
   every column is legal and this never fires.

   ⚠️ THE HOVERED COLUMN IS DELIBERATELY NOT DIMMED. It carries .is-drop-disallowed
   as well, so without the override below it would inherit that rule's opacity .4 and
   the wallpaper would multiply down into a smudge. The two-class selector wins on
   SPECIFICITY rather than on source order, so this cannot be broken by moving the
   block around. The other illegal columns keep their dim, and that contrast is the
   whole point: the rest recede, the one you are pointing at answers you.

   pointer-events:none so the overlay can never eat the drop it is describing, and
   ::after so the cards' own layout is untouched. */
.ocsb-col.is-drop-refused { position: relative; }
.ocsb-col.is-drop-disallowed.is-drop-refused,
.ocsb-zone.is-drop-disallowed.is-drop-refused { opacity: 1; }

.ocsb-zone.is-drop-refused::after,
.ocsb-col.is-drop-refused::after {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    opacity: .26;
    background-color: var(--oc-red-600);
    -webkit-mask-image: var(--ocsb-refused-wallpaper);
            mask-image: var(--ocsb-refused-wallpaper);
    -webkit-mask-repeat: repeat;
            mask-repeat: repeat;
    -webkit-mask-position: center top;
            mask-position: center top;
    -webkit-mask-size: 280px 340px;
            mask-size: 280px 340px;
}

/* Section labels are real canvas children at logical coordinates, so they pan and
   scale WITH the board — no per-frame overlay tracking. Handwritten straight on
   the board like the Feature Board's; the soft surface-coloured halo keeps the
   marker legible when a note slides underneath.

   ⚠️ THE SERVER RESERVES THIS BAND. `top` and `font-size` below are mirrored by
   ScrumAutoPlacement.ZoneLabelTop / .ZoneLabelHeight in the PartnerPortalAPI repo
   (Services/ScrumBoards/ScrumAutoPlacement.cs), which is how auto-placed cards
   know to start BELOW the heading instead of on top of it. The box is exactly
   one font-size tall because the children below set line-height: 1 — keep that,
   or the two sides stop agreeing. Move either number and the server's TopInset
   has to move with it; ScrumAutoPlacementTests pins the pair.
   A HAND-DRAGGED note may still be parked over the label — that is what the halo
   is for, and nothing here or on the server prevents it. */
.ocsb-zone-label {
    position: absolute;
    top: 18px;                  /* MUST match ScrumAutoPlacement.ZoneLabelTop */
    box-sizing: border-box;
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 24px;
    /* The bottom padding is the DESCENDER RESERVE. Caveat paints well below the
       baseline ("In Progress" has a g), and with line-height: 1 on the children the
       line box ends AT the baseline — so `overflow: hidden` sliced the tails off
       (reported 2026-08-15). This grows the clipping box DOWNWARD only: `top` is
       untouched, the children still set line-height: 1, so the text does not move
       and the 44px mirror below still describes where the glyphs sit.
       It costs ~10 logical px of height, which lands inside the 48px
       ScrumAutoPlacement.ZoneLabelClearance already reserved under the heading, so
       no auto-placed card moves and TopInset (110) is unchanged. */
    /* ⚠️ HORIZONTAL ONLY. The right padding reserves room for the sibling .ocsb-zone-add;
       `top` and `font-size` are the two numbers ScrumAutoPlacement mirrors and neither moves,
       and the .24em bottom is the descender reserve documented above. Widening the right
       gutter costs the NAME some ellipsis budget and nothing else. */
    padding: 0 var(--ocsb-zone-add-reserve) .24em 22px;
    z-index: 0;                 /* board chrome: above the zone tint, below the notes */
    pointer-events: none;
    --ocsb-accent: var(--oc-gray-500);
    color: var(--oc-text);
    font-size: 44px;            /* logical px, scales with the wall like a note.
                                   MUST match ScrumAutoPlacement.ZoneLabelHeight */
    overflow: hidden;
    text-shadow: 0 0 6px var(--oc-surface), 0 0 6px var(--oc-surface), 0 1px 1px var(--oc-surface);
}

.ocsb-zone-name {
    font-family: var(--ocsb-font-hand);
    font-size: inherit;
    line-height: 1;
    font-weight: 700;
    /* Accent hue anchored in --oc-text so the marker keeps its depth (and its
       contrast) in both themes — the label is board chrome, so it themes. */
    color: color-mix(in srgb, var(--ocsb-accent) 45%, var(--oc-text));
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0;
    /* THIS is the box that was slicing the glyphs, not the label around it: the
       ellipsis needs overflow: hidden, and with line-height: 1 the box hugs the
       text on all four sides. Measured at 44px in the real Caveat
       (ZoneLabelClipMeasurementTests): the "e" of "Done" overhung its right edge
       by 3.3px and "In Progress" by 4.6px, and the descender cleared the bottom
       edge by 1.4px — a hair, and visibly nothing on a rasterised swash. Both are
       what the CEO reported on 2026-08-15.

       Each reserve is cancelled by an equal negative margin, so the box grows
       where it clips but the heading does not move and the flex line does not get
       taller: the label's own descender reserve is sized to cover exactly this
       overhang. After: 2.9px / 1.6px to the right, 12.0px below.

       The cost is a few px of width, visible only on a name long enough to
       ellipsis, which truncates a hair earlier. */
    padding: 0 .14em .24em;
    margin: 0 -.14em -.24em;
}

/* The section's card count. WipBadge renders it — as a bare number when the
   section has no WIP limit (which is this handwritten treatment, the restored
   look) and as an "n/limit" pill when it does. .ocsb-zone-count is kept as the
   name of the look so a caller that wants the plain count can still ask for it;
   oc-scrum-wip.css only restyles the LIMIT states. */
.ocsb-zone-count,
.ocsb-zone-label .ocsb-wip {
    font-family: var(--ocsb-font-hand);
    font-size: inherit;
    line-height: 1;
    font-weight: 700;
    color: var(--oc-text);
    opacity: .8;
    flex: 0 0 auto;
}

/* ---------------------------------------------------------------------------
   THE PER-BAND "+" (CEO 2026-08-19: "all columns ... should [have] a + sign that is
   a button but looks hand written like the rest of the header").

   ⚠️ A SIBLING OF .ocsb-zone-label, NEVER A CHILD, on three independent counts:
     * the label is `overflow: hidden`, so a child would be CLIPPED;
     * the label is `position: absolute` WITH `z-index: 0`, which makes it a stacking
       context — a child can never rise above the notes no matter what z-index it is
       given, and the label deliberately sits UNDER them; and
     * the label's flex line is the box ScrumAutoPlacement.ZoneLabelHeight mirrors in
       the API repo, so a third flex item could move a number two repos apart.

   ⚠️ RAW <button>, NOT OcButton, and not by preference. OcButton's styles live in a
   Blazor SCOPED stylesheet (OcButton.razor.css), which compiles to `.oc-btn[b-xxxxxxxx]`
   — higher specificity — and ships in PartnerPortal.styles.css, linked AFTER this file.
   `.oc-btn` sets font-family: var(--oc-font-sans), so the handwriting, which IS the
   feature, would lose on both axes. The telemetry OcButton would have carried is fired
   from the click handler instead, so nothing is silently given up.
   --------------------------------------------------------------------------- */
.ocsb-zone-add {
    position: absolute;
    top: 18px;                  /* the label's own top: the two share a baseline */
    width: var(--ocsb-zone-add-size);
    /* Placed at the band's RIGHT EDGE by the markup, then pulled back inside by its own
       width plus the label's 22px gutter. Pure arithmetic off numbers already in the
       render loop — no new constant, and nothing for the server to mirror. */
    transform: translateX(calc(-100% - 22px));
    padding: 0; border: 0; background: none; cursor: pointer;
    /* THE HAND. Same family, size and line-height as .ocsb-zone-name so the glyph sits on
       the same baseline and reads as part of the writing rather than beside it. */
    font: 700 44px/1 var(--ocsb-font-hand);
    color: color-mix(in srgb, var(--ocsb-accent) 45%, var(--oc-text));
    /* The same halo the label wears, for the same reason: a note may be parked here. */
    text-shadow: 0 0 6px var(--oc-surface), 0 0 6px var(--oc-surface), 0 1px 1px var(--oc-surface);
    /* ABOVE EVERY RESTING NOTE, BELOW A NOTE IN HAND. A resting note's z-index is its
       ZOrder — a MAX+1 counter with no upper bound, so a small number would eventually
       lose. The engine lifts a dragged card to 100000 and a hovered stack to 200000, and
       staying under those is deliberate: a note you are holding should pass over the
       heading. Confined to .ocsb-wall-canvas, which is its own stacking context
       (will-change: transform plus the engine's inline transform), so this cannot reach
       the shell overlays or the modal layer.
       A CONTROL IN A HEADER MUST NEVER BE OCCLUDABLE — that is the whole reason it
       outranks the label, which sits under the notes on purpose and relies on its halo. */
    z-index: 90000;
    line-height: 1;
}

.ocsb-zone-add:hover { color: var(--oc-text); }

/* A raw <button> inherits none of OcButton's focus ring. Restoring it is not optional. */
.ocsb-zone-add:focus-visible {
    outline: none;
    box-shadow: var(--oc-shadow-focus);
    border-radius: var(--oc-radius-sm);
}

/* ---------------------------------------------------------------------------
   THE COLUMN-HEAD "+" — the same control on the plain surfaces.

   PLAIN, NOT HANDWRITTEN, and that is the CEO's rule rather than an omission: the "+"
   matches the header it sits in, and .ocsb-col-name has no font-family at all — it
   inherits --oc-font-sans from .ocsb-page. A Caveat glyph would be the only handwriting
   on the surface.
   --------------------------------------------------------------------------- */
.ocsb-col-add {
    display: inline-flex; align-items: center; justify-content: center;
    width: 1.25rem; height: 1.25rem;
    padding: 0; border: 0; border-radius: var(--oc-radius-sm);
    background: none; cursor: pointer;
    color: color-mix(in srgb, var(--ocsb-accent) 45%, var(--oc-text));
    font: 700 1rem/1 var(--oc-font-sans);
    /* The head is align-items: baseline; a button's baseline is its own text box, which
       would hang the glyph a couple of pixels low against the column name. */
    align-self: center;
}

.ocsb-col-add:hover { background: var(--oc-gray-50); color: var(--oc-text); }
.ocsb-col-add:focus-visible { outline: none; box-shadow: var(--oc-shadow-focus); }

/* ⚠️ LOAD-BEARING, and it is why the "+" can be appended LAST in the head.
   .ocsb-col-head is justify-content: space-between, which with three children would
   strand the "+" in the middle. An `auto` margin absorbs all the free space BEFORE
   justify-content gets to distribute any, so the name pins left and the count and "+"
   pack right — and with only two children (read-only, or a column that offers no "+")
   the result is identical to what space-between did on its own. */
.ocsb-col-name { margin-right: auto; }



/* ------------------------------------------------------------------- cards */

/* A wall card IS a sticky note — the Feature Board's CEO-approved realistic
   paper design, restored. A physical-paper CONSTANT: bright dyed paper with dark
   ink, reading the same in light and dark. The paper/ink pairs are the FROZEN
   --oc-note-* tokens in oc-theme.css (each >= 4.5:1 against the darker gradient
   stop — ratios documented there). The engine owns position/left/top/z-index
   inline; the structural bits it needs (position/width/cursor/touch-action/
   user-select) head the rule. Per-note tilt comes from an inline --tilt; tape,
   curl and per-age decay come from state classes emitted by BoardCardNote. */
.ocsb-card {
    /* --- structural (engine contract) --- */
    position: absolute;
    width: 240px;              /* MUST match ScrumBoardLayout.CardWidth */
    min-height: 140px;         /* MUST match ScrumBoardLayout.CardHeight */
    box-sizing: border-box;
    /* A COLUMN so the foot can be pinned to the bottom (CEO 2026-08-19: the
       priority number "should always be in the same place in the bottom left").
       min-height means a short note has slack under its meta row; in normal flow
       the foot floated wherever the content ended, so "bottom left" landed at a
       different height on every card. `margin-top: auto` on .ocsb-card-meta
       below spends that slack ABOVE the foot instead. The absolutely-positioned
       children (.ocsb-wm, .ocsb-bulk-badge, .ocsb-card-corner) are not flex
       items and are unaffected. */
    display: flex;
    flex-direction: column;
    cursor: grab;
    touch-action: none;        /* the engine owns finger gestures on a card */
    user-select: none; -webkit-user-select: none;
    /* --- paper design --- */
    --paper-top: var(--oc-note-0-top);
    --paper-bot: var(--oc-note-0-bot);
    --ink:       var(--oc-note-0-ink);
    --tilt:      -1.4deg;
    --hand: 'Caveat', 'Patrick Hand', cursive;
    padding: 16px 16px 12px;
    color: var(--ink);
    font-family: var(--hand);
    font-size: 17px;
    line-height: 1.25;
    background:
        var(--ocsb-grain),
        linear-gradient(118deg, rgba(255,255,255,.45) 0%, rgba(255,255,255,0) 34%),
        radial-gradient(120% 90% at 50% 0%, rgba(255,255,255,.30), rgba(255,255,255,0) 55%),
        linear-gradient(176deg, var(--paper-top) 0%, var(--paper-bot) 100%);
    background-size: 180px 180px, cover, cover, cover;
    background-blend-mode: soft-light, normal, normal, normal;
    border: 1px solid rgba(0,0,0,.05);
    border-radius: var(--ocsb-note-radius);
    box-shadow:
        inset 0 6px 10px -8px rgba(0,0,0,.28),
        inset 0 -10px 14px -12px rgba(0,0,0,.10),
        var(--ocsb-note-lift);
    transform: rotate(var(--tilt));
    transition: transform .18s ease, box-shadow .18s ease, filter .18s ease;
    will-change: transform;
    isolation: isolate;
}

.ocsb-card .ocsb-card-title,
.ocsb-card .ocsb-card-meta { color: inherit; }

/* Curled bottom corner. Alternating papers curl on the left so a wall of notes
   doesn't look machine-stamped. */
.ocsb-card::after {
    content:""; position:absolute; right:-1px; bottom:-1px; width:34px; height:34px;
    background:
        linear-gradient(135deg,
            rgba(0,0,0,0) 46%, rgba(0,0,0,.06) 47%, rgba(255,255,255,.55) 52%,
            var(--paper-bot) 58%, color-mix(in srgb, var(--paper-bot) 70%, var(--oc-black) 12%) 100%);
    border-bottom-right-radius: var(--ocsb-note-radius);
    box-shadow: -3px -3px 6px -3px rgba(0,0,0,.35);
    clip-path: polygon(100% 0, 100% 100%, 0 100%);
    transform: rotate(.5deg);
    pointer-events:none;
}
.ocsb-paper-1::after,
.ocsb-paper-3::after {
    right:auto; left:-1px;
    border-bottom-right-radius:0; border-bottom-left-radius: var(--ocsb-note-radius);
    background:
        linear-gradient(225deg,
            rgba(0,0,0,0) 46%, rgba(0,0,0,.06) 47%, rgba(255,255,255,.55) 52%,
            var(--paper-bot) 58%, color-mix(in srgb, var(--paper-bot) 70%, var(--oc-black) 12%) 100%);
    box-shadow: 3px -3px 6px -3px rgba(0,0,0,.35);
    clip-path: polygon(0 0, 100% 100%, 0 100%);
}

/* Tape strip across the top of ~half the notes (deterministic per card). */
.ocsb-card.fix-tape::before {
    content:""; position:absolute; top:-9px; left:50%; width:86px; height:24px;
    transform: translateX(-50%) rotate(-2.5deg);
    background:
        linear-gradient(180deg, rgba(255,255,255,.42), rgba(245,242,225,.20)),
        repeating-linear-gradient(90deg, rgba(255,255,255,.10) 0 6px, rgba(0,0,0,.03) 6px 7px);
    border-left: 1px solid rgba(255,255,255,.35);
    border-right:1px solid rgba(255,255,255,.35);
    box-shadow: 0 2px 4px rgba(0,0,0,.10);
    opacity:.85; pointer-events:none;
}
.ocsb-paper-2.fix-tape::before { transform: translateX(-60%) rotate(3deg); }
.ocsb-paper-4.fix-tape::before { transform: translateX(-40%) rotate(-4deg); }

/* Paper variants — frozen pairs from oc-theme.css, each with its own hand font,
   base tilt and irregular hand-cut radius. */
.ocsb-paper-0 { --paper-top:var(--oc-note-0-top); --paper-bot:var(--oc-note-0-bot); --ink:var(--oc-note-0-ink); --tilt:-1.6deg; --hand:'Caveat', cursive; border-radius: 4px 6px 3px 5px; }
.ocsb-paper-1 { --paper-top:var(--oc-note-1-top); --paper-bot:var(--oc-note-1-bot); --ink:var(--oc-note-1-ink); --tilt:1.8deg;  --hand:'Shadows Into Light', cursive; border-radius: 6px 3px 6px 4px; }
.ocsb-paper-2 { --paper-top:var(--oc-note-2-top); --paper-bot:var(--oc-note-2-bot); --ink:var(--oc-note-2-ink); --tilt:-2.6deg; --hand:'Gochi Hand', cursive; border-radius: 5px 4px 6px 3px; }
.ocsb-paper-3 { --paper-top:var(--oc-note-3-top); --paper-bot:var(--oc-note-3-bot); --ink:var(--oc-note-3-ink); --tilt:2.4deg;  --hand:'Patrick Hand', cursive; border-radius: 3px 6px 4px 6px; }
.ocsb-paper-4 { --paper-top:var(--oc-note-4-top); --paper-bot:var(--oc-note-4-bot); --ink:var(--oc-note-4-ink); --tilt:-0.9deg; --hand:'Kalam', cursive; border-radius: 6px 4px 5px 4px; }

/* ── Rock watermark (CEO 2026-08-16) ─────────────────────────────────────────
   "Rock-related post-its carry a rock watermark over the existing paper design."
   The paper design is CEO-approved and its top/bottom/ink triples are FROZEN —
   deliberately identical in light and dark — so this mark:
     * adds NO opaque colour and no --oc-* colour token. It is a black line wash
       at ~8% over paper that is light in both themes, which is why it does not
       need (and must not have) a dark-mode override. A themed colour here would
       self-invert on paper that does not, which is the exact break the frozen
       pairs exist to prevent;
     * is an INLINE data: SVG. The deployed CSP blocks external images, so a
       hosted asset would render on localhost and silently vanish on the site;
     * paints BETWEEN the paper and the text. .ocsb-card sets `isolation:isolate`,
       so a z-index:-1 child paints above the card's own background and below its
       in-flow content — a watermark, by definition. Without the negative z-index
       an absolutely-positioned ::before would paint OVER the title instead.
   Wall only, like the decay visuals: the columns surface draws BoardColumnCard,
   which has no paper to watermark. */
/* ⛔ THE WATERMARK IS A REAL CHILD (.ocsb-wm), NEVER ::before (CEO 2026-08-19:
   "the rock watermark is showing black on some of my cards"). The card's
   ::before belongs to the PAPER EFFECTS — fix-tape paints the tape strip there
   and age-6 paints the cobweb — and the aged-tape rules carry THREE classes
   (.ocsb-card.age-5.fix-tape::before { filter: sepia(...); opacity:.7 }), so
   they out-ranked this two-class rule per property: an aged, taped rock
   rendered its mark at 0.7 opacity under a sepia wash — near black — and a
   taped rock lost its tape entirely. A pseudo-element is single-occupancy;
   a real span collides with nothing, and the paper effects keep the pseudo.
   Everything else is the 2026-08-16 design unchanged: a black line wash at
   ~8% over paper that is light in both themes (no themed colour, no
   dark-mode override), an inline data: SVG (the deployed CSP blocks hosted
   images), z-index:-1 under the card's in-flow text (isolation:isolate on
   .ocsb-card). ONE mark per card — ScrumBoardUi.WatermarkClass resolves
   precedence (rock > to-do > feature). */
.ocsb-card .ocsb-wm {
    position: absolute;
    right: 12px;
    top: 50%;
    width: 92px;
    height: 92px;
    transform: translateY(-50%) rotate(-6deg);
    background-repeat: no-repeat;
    background-position: center;
    background-size: contain;
    opacity: .085;
    pointer-events: none;
    z-index: -1;
}

.ocsb-card.is-rock .ocsb-wm,
.ocsb-card.is-rock-milestone .ocsb-wm {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cg fill='none' stroke='%23000' stroke-width='6' stroke-linejoin='round' stroke-linecap='round'%3E%3Cpath d='M12 74 L27 37 L49 25 L73 34 L88 63 L80 79 Z'/%3E%3Cpath d='M27 37 L47 52 L73 34'/%3E%3Cpath d='M47 52 L41 79'/%3E%3Cpath d='M47 52 L80 79'/%3E%3C/g%3E%3C/svg%3E");
}

/* The To-Do and Feature Board marks (CEO 2026-08-17): same wash, their own
   drawings. The Feature Board's mark is a SNOWFLAKE (CEO 2026-08-19, replacing
   the original spark) — six arms on three crossing axes, each carrying one pair
   of branches that open OUTWARD from a point two-thirds along. Barbs drawn the
   other way (a V closing back toward the centre) read as arrowheads, not ice. */
.ocsb-card.is-eos-todo .ocsb-wm {
    width: 88px;
    height: 88px;
    transform: translateY(-50%) rotate(-5deg);
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cg fill='none' stroke='%23000' stroke-width='6' stroke-linejoin='round' stroke-linecap='round'%3E%3Crect x='18' y='20' width='54' height='54' rx='10'/%3E%3Cpath d='M33 49 L46 62 L84 22'/%3E%3C/g%3E%3C/svg%3E");
}

.ocsb-card.is-feature .ocsb-wm {
    width: 90px;
    height: 90px;
    transform: translateY(-50%) rotate(7deg);
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cg fill='none' stroke='%23000' stroke-width='6' stroke-linejoin='round' stroke-linecap='round'%3E%3Cpath d='M50 12 L50 88'/%3E%3Cpath d='M17 31 L83 69'/%3E%3Cpath d='M17 69 L83 31'/%3E%3Cpath d='M41 17 L50 26 L59 17'/%3E%3Cpath d='M74 25 L71 38 L83 41'/%3E%3Cpath d='M83 59 L71 62 L74 75'/%3E%3Cpath d='M41 83 L50 74 L59 83'/%3E%3Cpath d='M26 75 L29 62 L17 59'/%3E%3Cpath d='M17 41 L29 38 L26 25'/%3E%3C/g%3E%3C/svg%3E");
}

/* The face labels. Frozen ink like every other mark on the paper — see the
   .ocsb-card-watch note below for why a themed colour is wrong here. */
.ocsb-card .ocsb-chip-rock,
.ocsb-card .ocsb-chip-rock-todo { color: var(--ink); }
.ocsb-card .ocsb-chip-rock { font-weight: 800; letter-spacing: .03em; }

/* A Rock with no milestones has nothing to work yet, so it asks for them. */
.ocsb-card .ocsb-chip-rock-todo { font-style: italic; }

/* Compact milestone strip — unused on the wall now (full notes), kept for columns if needed. */
.ocsb-card.is-title-only {
    min-height: 44px;
    padding: 8px 12px;
}
.ocsb-card.is-title-only .ocsb-card-title {
    display: -webkit-box;
    -webkit-line-clamp: 1;
    -webkit-box-orient: vertical;
    overflow: hidden;
}
.ocsb-card.is-title-only::after { display: none; }

/* Visual grouping header — not a stored card. Height MUST match
   ScrumRockStacks.HeaderHeight (72). Drag writes the first milestone. */
.ocsb-card.ocsb-rock-stack-header {
    min-height: 72px;
    padding: 12px 14px 10px;
}

/* NATIVE CARD FAMILIES (2026-08-19) — the card dialog's parent checkbox, the list of
   cards under it, and the board-settings wording pair. Themed tokens throughout:
   unlike the note faces (frozen paper, frozen ink), these are ordinary dialog and
   settings chrome and must follow light/dark like the rest of the form. */
.ocsb-family-flag {
    display: flex;
    flex-direction: column;
    gap: .25rem;
}
.ocsb-family-parent {
    display: flex;
    align-items: center;
    gap: .35rem;
    flex-wrap: wrap;
}
.ocsb-family-panel {
    margin: .75rem 0;
    padding-top: .5rem;
    border-top: 1px solid var(--oc-border);
}
.ocsb-family-list {
    list-style: none;
    margin: 0 0 .5rem;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: .15rem;
}
.ocsb-family-row {
    display: flex;
    align-items: center;
    gap: .4rem;
    flex-wrap: wrap;
}
.ocsb-family-add {
    display: flex;
    align-items: center;
    gap: .4rem;
    margin-top: .35rem;
}
.ocsb-family-add .form-control,
.ocsb-family-add .form-select { flex: 1 1 auto; min-width: 0; }

.ocsb-family-labels {
    display: flex;
    gap: .75rem;
    flex-wrap: wrap;
    margin-bottom: .5rem;
}
.ocsb-family-labels .ocsb-field { flex: 1 1 12rem; min-width: 0; }

.ocsb-rock-col-stack {
    display: flex;
    flex-direction: column;
    gap: .25rem;
    margin-bottom: .35rem;
}
.ocsb-rock-col-header {
    display: flex;
    align-items: center;
    gap: .4rem;
    width: 100%;
    text-align: left;
    border: 1px dashed var(--oc-border);
    border-radius: var(--oc-radius-sm);
    background: var(--oc-surface);
    color: var(--oc-text);
    padding: .4rem .55rem;
    cursor: pointer;
}
.ocsb-rock-col-header .ocsb-chip-rock {
    font-weight: 800;
    letter-spacing: .03em;
    font-size: var(--oc-fs-xs);
}
.ocsb-rock-col-header-title {
    font-weight: 700;
    font-size: var(--oc-fs-sm);
}
.ocsb-col-card.is-title-only { padding: .4rem .55rem; }
.ocsb-col-card.is-title-only .ocsb-col-card-title { font-weight: 500; }


/* ── Priority number (CEO 2026-08-19) ─────────────────────────────────────────
   "The number only, bottom-left of the card, in line with the due date and age",
   bold, one colour per level: 1 red, 2 orange, 3 blue, 4 black, 5 grey.

   It LEADS .ocsb-card-meta, which is the row carrying the reference, the owner,
   the team and the age — so it lands bottom-left with the age pushed to the far
   right by its own margin-left:auto. No extra row, no extra height on the note.

   TWO COLOUR SETS, and the split is load-bearing:
     * the WALL note is frozen paper, so it takes the frozen --oc-note-prio-*
       inks (see oc-theme.css). A themed token here would go pale in dark mode on
       paper that stayed bright.
     * the COLUMN card is an ordinary themed surface, so it takes ordinary ramp
       tokens and flips with the theme like everything else on it.
   Using one set for both is wrong in one direction or the other. */
.ocsb-card-prio {
    flex: 0 0 auto;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: 1;
    cursor: default;
}

/* The wall note: frozen ink, and in the note's own hand so the number reads as
   written on the paper rather than typed onto it. */
.ocsb-card .ocsb-card-prio {
    font-family: var(--hand);
    font-size: 1.15em;
}

.ocsb-card .ocsb-card-prio.ocsb-prio-1 { color: var(--oc-note-prio-1); }
.ocsb-card .ocsb-card-prio.ocsb-prio-2 { color: var(--oc-note-prio-2); }
.ocsb-card .ocsb-card-prio.ocsb-prio-3 { color: var(--oc-note-prio-3); }
.ocsb-card .ocsb-card-prio.ocsb-prio-4 { color: var(--oc-note-prio-4); }
.ocsb-card .ocsb-card-prio.ocsb-prio-5 { color: var(--oc-note-prio-5); }

/* The column card: a themed surface, so themed tokens. --oc-orange-text rather
   than --oc-primary for 2, per the standing orange-as-text rule (--oc-primary is
   ~3.5:1 as text). */
.ocsb-col-card .ocsb-card-prio.ocsb-prio-1 { color: var(--oc-danger); }
.ocsb-col-card .ocsb-card-prio.ocsb-prio-2 { color: var(--oc-orange-text); }
.ocsb-col-card .ocsb-card-prio.ocsb-prio-3 { color: var(--oc-info); }
.ocsb-col-card .ocsb-card-prio.ocsb-prio-4 { color: var(--oc-text); }
.ocsb-col-card .ocsb-card-prio.ocsb-prio-5 { color: var(--oc-text-muted); }

/* Hover: lift off the wall and straighten slightly, like reaching for it. */
.ocsb-card:hover {
    transform: rotate(calc(var(--tilt) * .35)) translateY(-3px);
    box-shadow:
        inset 0 6px 10px -8px rgba(0,0,0,.22),
        12px 18px 26px -10px rgba(40,30,10,.38),
        3px 5px 8px -3px rgba(40,30,10,.22);
}

.ocsb-card.is-dragging {
    cursor: grabbing;
    box-shadow:
        inset 0 6px 10px -8px rgba(0,0,0,.18),
        26px 40px 50px -18px rgba(20,14,4,.45),
        8px 12px 18px -6px rgba(20,14,4,.30);
    scale: 1.05;
    filter: saturate(1.05) brightness(1.02);
    transition: none;          /* never lerp a card away from the finger */
}

.ocsb-card.is-readonly { cursor: default; }

@keyframes ocsb-arrive {
    0%, 100% { opacity: 1; }
    40% { opacity: .35; }
}
.ocsb-card.is-arriving {
    animation: ocsb-arrive .7s ease-in-out 4;
}

.ocsb-card-title {
    margin: 2px 0 10px;
    font-family: var(--hand);
    font-weight: 700;
    font-size: 1.32em;
    line-height: 1.12;
    letter-spacing: .2px;
    display: -webkit-box;
    -webkit-line-clamp: 4;
    -webkit-box-orient: vertical;
    overflow: hidden;
    text-wrap: balance;
}

/* Layout only — deliberately NO `opacity`. This rule used to carry a blanket
   `opacity:.8`, and BOTH of its consumers turned it straight back off: the wall
   card needs full-strength frozen ink, and the column card states a computable
   --oc-text-muted instead. So the fade was already dead, and it was worse than
   clutter — an opacity composites the line toward whatever is behind it, which
   makes the effective contrast unknowable from the stylesheet, and on paper it
   drops the orange and pink pairs to ~3.6:1 (documented in oc-theme.css). A
   future consumer of .ocsb-card-meta would have inherited that silently. */
.ocsb-card-meta {
    display: flex;
    align-items: center;
    gap: .375rem;
    flex-wrap: wrap;
    font-size: var(--oc-fs-xs);
}

/* On PAPER the meta is FULL-strength ink. Hierarchy comes from the smaller size
   and the dashed rule, exactly like the Feature Board's note foot. */
.ocsb-card .ocsb-card-meta {
    gap: 8px;
    margin-top: auto;          /* pinned to the note's bottom — see .ocsb-card */
    padding-top: 7px;
    border-top: 1px dashed color-mix(in srgb, var(--ink) 35%, transparent);
    font-size: .82em;
    font-family: var(--hand);
    font-weight: 700;
    /* The priority number is lifted OUT of this row's flow and pinned to its
       bottom-left corner, so the row reserves a gutter for it. The gutter is
       padding, not a margin, so the dashed rule above still spans the full
       width of the foot exactly as it did before. */
    position: relative;
    padding-left: 15px;
}

/* ⛔ THE PRIORITY NUMBER IS PINNED, NOT ORDERED (CEO 2026-08-19: "the priority
   lever is being moved and it should always be in the same place in the bottom
   left"). It used to rely on being FIRST in a `flex-wrap: wrap` row, which only
   put it bottom-left while the whole foot fitted one line — add an owner and the
   row wrapped, carrying the number up to the first line and away from the due
   date and age it is meant to sit beside. Absolute positioning is the only thing
   that makes the position independent of what else the card carries.

   It STAYS the first child of .ocsb-card-meta in the markup: BoardCardPriorityTests
   pins that, and a card whose foot is an empty div throws rather than failing
   cleanly. z-index is load-bearing — on papers 1 and 3 the .ocsb-card::after curl
   relocates to the BOTTOM-LEFT (34x34 from the corner) and, being the last pseudo
   in tree order with z-index:auto, paints straight over an unlifted number; the
   age-3..6 decay wash is a full-card ::after with the same problem. */
.ocsb-card .ocsb-card-prio,
.ocsb-col-card .ocsb-card-prio {
    position: absolute;
    left: 0;
    bottom: 0;
    z-index: 1;
}

/* Checklist / comment / attachment counts (phase 2). A row of its OWN above the
   foot, so the note keeps exactly one dashed separator — the restored design's
   rule — and the counts read as part of the note's handwriting rather than as
   portal chrome stuck on paper. Full-strength ink, for the reason above. */
.ocsb-card .ocsb-card-chips {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    margin-top: 6px;
    font-family: var(--hand);
    font-weight: 700;
    font-size: .78em;
    color: inherit;
    opacity: 1;
}

/* The due date, centred in the foot line beside the clock (CEO 2026-08-19).
   Flex auto-margins on both sides split the row's free space, so it reads
   centred between the left meta items and the age pill and can never overlap
   either. Frozen ink, like everything written on the paper. */
.ocsb-card .ocsb-card-due {
    margin-left: auto;
    margin-right: auto;
    display: inline-flex; align-items: center; gap: 4px;
    color: color-mix(in srgb, var(--ink) 90%, var(--oc-black));
    font-family: 'Kalam', var(--hand);
    font-size: .92em;
    white-space: nowrap;
}
.ocsb-card .ocsb-card-due i { font-size: .9em; opacity: .85; }

/* ── The card status chip (CEO 2026-08-19) ───────────────────────────────────
   Up to 3 CAPS letters + an emoji, in a palette colour, from the card's
   COLUMN's status group. On the WALL NOTE it is coloured bold TEXT ("DEV in
   bold black"), absolutely positioned into the blank top-right corner so the
   title never moves. The palette is FIXED HEX inks, not themed tokens: the
   papers are light in both themes by design (the watermark rule), so a themed
   colour would self-invert on paper that does not. On the COLUMN CARDS —
   which ARE themed surfaces — the same chip renders as a small filled pill
   with white text, which reads in both themes. */
/* THE NOTE'S TOP-RIGHT CORNER (CEO 2026-08-19: "the eye should be in the top
   right and should not move anything that is already there because we dont want
   to make the card bigger"). The corner was already occupied by the status chip,
   so the eye does not take it — the two share ONE right-anchored cluster.

   Right-anchored and RIGHT-MOST-FIRST in effect: the chip is LAST in the markup,
   so with a chip alone the cluster's right edge is the chip's right edge and the
   chip renders at exactly the top:7px/right:12px it has always had. Watching a
   card adds the eye to its LEFT. Nothing that was already on the card moves, on
   any combination.

   It is absolutely positioned, so it is out of flow and cannot push the title
   down or grow the note — that was the CEO's stated reason. It carries NO
   z-index on purpose: the chip sits UNDER the aged-card decay wash today, and a
   lift here would quietly change how every status chip renders on an old card. */
.ocsb-card-corner {
    position: absolute;
    top: 7px;
    right: 12px;
    max-width: 62%;
    display: flex;
    align-items: center;
    gap: 5px;
}

.ocsb-card .ocsb-card-cst {
    position: static;
    min-width: 0;
    display: inline-flex;
    align-items: center;
    gap: 3px;
    white-space: nowrap;
    text-transform: uppercase;
    font-family: var(--hand);
    font-size: .78rem;
    font-weight: 800;
    letter-spacing: .06em;
    pointer-events: none;
}
.ocsb-card .ocsb-cst-emoji { font-size: .9rem; }

/* The fixed palette inks, ON A WALL NOTE ONLY.
   ⚠️ SCOPED TO .ocsb-card DELIBERATELY, like .ocsb-card-prio above. These are frozen
   values because the paper they sit on is frozen; unscoped, they also applied to the
   card dialog's status picker, whose unpicked pill is --oc-surface - and black ink on
   the dark-mode surface computes about 1:1, i.e. an invisible control, with the
   default colour ("black") being the common case. */
.ocsb-card .ocsb-cst-black  { color: var(--oc-note-cst-black); }
.ocsb-card .ocsb-cst-gray   { color: var(--oc-note-cst-gray); }
.ocsb-card .ocsb-cst-red    { color: var(--oc-note-cst-red); }
.ocsb-card .ocsb-cst-green  { color: var(--oc-note-cst-green); }
.ocsb-card .ocsb-cst-blue   { color: var(--oc-note-cst-blue); }
.ocsb-card .ocsb-cst-orange { color: var(--oc-note-cst-orange); }
.ocsb-card .ocsb-cst-purple { color: var(--oc-note-cst-purple); }

/* Column cards: a filled pill, theme-safe. */
.ocsb-col-card-title .ocsb-card-cst {
    position: static;
    float: right;
    margin-left: .4rem;
    padding: 0 6px;
    border-radius: 999px;
    font-family: inherit;
    font-size: .68rem;
    color: #fff;
    pointer-events: auto;
}
.ocsb-col-card-title .ocsb-cst-black  { background: var(--oc-note-cst-black); }
.ocsb-col-card-title .ocsb-cst-gray   { background: var(--oc-note-cst-gray); }
.ocsb-col-card-title .ocsb-cst-red    { background: var(--oc-note-cst-red); }
.ocsb-col-card-title .ocsb-cst-green  { background: var(--oc-note-cst-green); }
.ocsb-col-card-title .ocsb-cst-blue   { background: var(--oc-note-cst-blue); }
.ocsb-col-card-title .ocsb-cst-orange { background: var(--oc-note-cst-orange); }
.ocsb-col-card-title .ocsb-cst-purple { background: var(--oc-note-cst-purple); }

/* The card dialog's picker: pill buttons; the picked one fills. */
.ocsb-cst-picker { display: flex; flex-wrap: wrap; gap: .4rem; }
.ocsb-cst-choice {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 2px 10px;
    border: 1px solid var(--oc-border);
    border-radius: 999px;
    background: var(--oc-surface);
    font-size: .8rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .05em;
    cursor: pointer;
}
.ocsb-cst-choice.is-picked { background: var(--oc-border); }   /* the None pill */

/* UNPICKED: the label stays on the themed text colour so it is readable in both
   themes, and the palette colour rides on the BORDER instead. The colour cue has to
   live somewhere that is not text, because these values are frozen for paper and
   several of them are unreadable on a dark surface. */
.ocsb-cst-choice.ocsb-cst-black  { border-color: var(--oc-note-cst-black); }
.ocsb-cst-choice.ocsb-cst-gray   { border-color: var(--oc-note-cst-gray); }
.ocsb-cst-choice.ocsb-cst-red    { border-color: var(--oc-note-cst-red); }
.ocsb-cst-choice.ocsb-cst-green  { border-color: var(--oc-note-cst-green); }
.ocsb-cst-choice.ocsb-cst-blue   { border-color: var(--oc-note-cst-blue); }
.ocsb-cst-choice.ocsb-cst-orange { border-color: var(--oc-note-cst-orange); }
.ocsb-cst-choice.ocsb-cst-purple { border-color: var(--oc-note-cst-purple); }

/* PICKED: filled with the palette colour, #fff on top - legible in both themes,
   which is why the fill is the one place the frozen value carries text. */
.ocsb-cst-choice.is-picked.ocsb-cst-black  { background: var(--oc-note-cst-black); color: #fff; }
.ocsb-cst-choice.is-picked.ocsb-cst-gray   { background: var(--oc-note-cst-gray); color: #fff; }
.ocsb-cst-choice.is-picked.ocsb-cst-red    { background: var(--oc-note-cst-red); color: #fff; }
.ocsb-cst-choice.is-picked.ocsb-cst-green  { background: var(--oc-note-cst-green); color: #fff; }
.ocsb-cst-choice.is-picked.ocsb-cst-blue   { background: var(--oc-note-cst-blue); color: #fff; }
.ocsb-cst-choice.is-picked.ocsb-cst-orange { background: var(--oc-note-cst-orange); color: #fff; }
.ocsb-cst-choice.is-picked.ocsb-cst-purple { background: var(--oc-note-cst-purple); color: #fff; }
.ocsb-cst-choice:disabled { opacity: .6; cursor: default; }

/* The settings editor's small inputs. */
.ocsb-cst-code-input  { max-width: 5.5rem; text-transform: uppercase; }
.ocsb-cst-emoji-input { max-width: 5.5rem; }

/* Handwritten age pill (time in this section). Ink-tinted so it stays a part of
   the note; worst pair computes 4.86:1 (documented in oc-theme.css). */
.ocsb-card .ocsb-card-age {
    flex: 0 0 auto;
    margin-left: auto;
    display: inline-flex; align-items: center; gap: 4px;
    padding: 1px 7px; border-radius: 999px;
    background: color-mix(in srgb, var(--ink) 12%, transparent);
    color: color-mix(in srgb, var(--ink) 90%, var(--oc-black));
    font-family: 'Kalam', var(--hand);
    font-size: .92em;
    white-space: nowrap;
}
.ocsb-card .ocsb-card-age i { font-size: .9em; opacity: .85; }

/* The watch marker keeps the note's ink (a themed blue would break the frozen
   pair in dark mode); the filled eye glyph itself is the signal. It lives in the
   top-right cluster (.ocsb-card-corner) rather than the foot, so it is sized
   against the chip beside it rather than against the meta row's handwriting. */
.ocsb-card .ocsb-card-watch {
    color: var(--ink);
    display: inline-flex;
    align-items: center;
    font-size: .74rem;
    line-height: 1;
    opacity: .75;
}

/* On a column card the eye floats at the title's right, immediately left of the
   status pill for the same reason as on the note: the pill is first in the
   markup, so it keeps the right edge and does not move when a card is watched. */
.ocsb-col-card-title .ocsb-card-watch {
    float: right;
    margin-left: .3rem;
    color: var(--oc-text-muted);
    font-size: .8em;
}

/* The short card reference (e.g. feature-board-42) — the handle people paste into
   a chat message or a commit. It rendered in My Work and in the Activity feed but
   nowhere on the card itself, which is where someone actually reads it off.
   Monospace, muted, sized like the meta line so it never competes with the title;
   the look matches .ocsb-activity-ref so the same string reads the same anywhere.
   This is the COLUMN-card treatment; the wall override follows. */
.ocsb-card-ref {
    font-family: var(--oc-font-mono);
    font-size: var(--oc-fs-xs);
    white-space: nowrap;
    color: var(--oc-text-muted);
}

/* The card's HOME BOARD, on cross-board columns (My Board). A quiet bordered chip
   in the meta line's muted grey: the board is context, and must never outweigh
   the title or read as a status. */
.ocsb-card-board {
    font-size: var(--oc-fs-xs);
    color: var(--oc-text-muted);
    border: 1px solid var(--oc-border);
    border-radius: .5rem;
    padding: 0 .35rem;
    white-space: nowrap;
}

/* On the wall it sits ON the paper, so it takes the paper's frozen ink — the
   theme's muted grey self-inverts and would go near-white on bright paper. It
   keeps the monospace face (that is what makes a reference scannable) but at the
   note's own scale. */
.ocsb-card .ocsb-card-ref {
    color: inherit;
    font-size: .9em;
    opacity: .85;
}

/* In the card dialog's title bar, beside "Card". */
.ocsb-modal-ref {
    margin-left: .5rem;
    font-family: var(--oc-font-mono);
    font-size: var(--oc-fs-sm);
    font-weight: 400;
    color: var(--oc-text-muted);
}

/* Aging glow: a card past its section's AgingThresholdDays. The threshold is per
   section in the DB, so the client and the server reporting cannot disagree. */
.ocsb-card.is-aging {
    box-shadow:
        0 0 0 2px rgba(255,176,32,.85),
        0 0 0 6px rgba(255,176,32,.28),
        inset 0 6px 10px -8px rgba(0,0,0,.28),
        var(--ocsb-note-lift);
    animation: ocsb-pulse 2.6s ease-in-out infinite;
}
@keyframes ocsb-pulse {
    0%,100% { box-shadow:0 0 0 2px rgba(255,176,32,.85),0 0 0 6px rgba(255,176,32,.28),
                         inset 0 6px 10px -8px rgba(0,0,0,.28),var(--ocsb-note-lift); }
    50%     { box-shadow:0 0 0 2px rgba(255,176,32,.95),0 0 0 10px rgba(255,176,32,.12),
                         inset 0 6px 10px -8px rgba(0,0,0,.28),var(--ocsb-note-lift); }
}

/* Decay scale — the "fun aging appearances": one class per note, advanced by time
   IN ITS CURRENT SECTION (SectionEnteredUtc vs the server clock; a card that moves
   gets fresh paper). Same stages as the Feature Board: settling > crinkled > dusty
   > coffee-stained > moldy > ancient. */
.ocsb-card.age-1 {
    transform: rotate(calc(var(--tilt) + .6deg));
    box-shadow:
        inset 0 6px 10px -8px rgba(0,0,0,.30),
        inset 0 40px 40px -46px rgba(0,0,0,.20),
        var(--ocsb-note-lift);
}
.ocsb-card.age-2 {
    transform: rotate(calc(var(--tilt) + 1deg)) skewX(-.6deg);
    filter: contrast(1.06);
    border-radius: 5px 7px 4px 8px;
    box-shadow:
        inset 32px 22px 26px -30px rgba(0,0,0,.30),
        inset -28px 38px 26px -30px rgba(0,0,0,.26),
        inset 24px -34px 24px -30px rgba(0,0,0,.22),
        inset -10px -6px 16px -14px rgba(255,255,255,.45),
        inset 14px 10px 16px -14px rgba(255,255,255,.40),
        var(--ocsb-note-lift);
}
.ocsb-card.age-3 {
    transform: rotate(calc(var(--tilt) + 1.2deg)) skewX(-.7deg);
    filter: contrast(1.04) saturate(.72) brightness(.97) sepia(.06);
    border-radius: 5px 7px 4px 8px;
    box-shadow:
        inset 30px 20px 26px -30px rgba(0,0,0,.28),
        inset -26px 36px 26px -30px rgba(0,0,0,.24),
        inset 0 -26px 30px -22px rgba(120,110,90,.40),
        var(--ocsb-note-lift);
}
.ocsb-card.age-3::after {
    content:""; position:absolute; inset:0; width:auto; height:auto;
    clip-path:none; transform:none; box-shadow:none; border-radius: inherit;
    background:
        radial-gradient(circle at 18% 26%, rgba(90,84,70,.16) 0 1.3px, transparent 2px),
        radial-gradient(circle at 62% 18%, rgba(90,84,70,.13) 0 1.1px, transparent 2px),
        radial-gradient(circle at 80% 44%, rgba(90,84,70,.15) 0 1.4px, transparent 2px),
        radial-gradient(circle at 34% 64%, rgba(90,84,70,.12) 0 1.1px, transparent 2px),
        radial-gradient(circle at 70% 78%, rgba(90,84,70,.15) 0 1.3px, transparent 2px),
        radial-gradient(circle at 46% 88%, rgba(90,84,70,.12) 0 1px,   transparent 2px),
        linear-gradient(180deg, rgba(150,144,128,.05), rgba(120,112,96,.16));
    opacity:.9; pointer-events:none;
}
/* age-4 — coffee/food stained (drop positions echoed by age-5 mold) */
.ocsb-card.age-4 {
    transform: rotate(calc(var(--tilt) + 1.3deg)) skewX(-.8deg);
    filter: contrast(1.03) saturate(.86) brightness(.96) sepia(.10);
    border-radius: 8px 11px 6px 10px;
    box-shadow:
        inset 26px 18px 24px -28px rgba(0,0,0,.24),
        inset -24px 30px 24px -28px rgba(0,0,0,.20),
        inset 0 -30px 30px -24px rgba(96,70,40,.34),
        inset 0  22px 26px -26px rgba(96,70,40,.22),
        var(--ocsb-note-lift);
}
.ocsb-card.age-4::after {
    content:""; position:absolute; inset:0; width:auto; height:auto;
    clip-path:none; transform:none; box-shadow:none; border-radius: inherit;
    background:
        radial-gradient(ellipse 30% 24% at 26% 30%, rgba(150,110,64,.05) 0 60%, rgba(116,80,42,.32) 70%, rgba(116,80,42,.10) 80%, transparent 90%),
        radial-gradient(ellipse 22% 18% at 70% 62%, rgba(150,110,64,.05) 0 58%, rgba(108,74,40,.28) 68%, rgba(108,74,40,.08) 79%, transparent 90%),
        radial-gradient(ellipse 18% 14% at 48% 82%, rgba(96,58,30,.44) 0 52%, rgba(96,58,30,.18) 72%, transparent 86%),
        radial-gradient(ellipse 10% 13% at 86% 36%, rgba(120,80,40,.36) 0 56%, transparent 80%),
        radial-gradient(circle 5px at 40% 50%, rgba(80,50,26,.42), transparent 72%),
        radial-gradient(circle 3px at 58% 44%, rgba(80,50,26,.38), transparent 72%),
        radial-gradient(circle 2px at 33% 64%, rgba(80,50,26,.40), transparent 70%),
        linear-gradient(0deg, rgba(112,80,44,.20), rgba(112,80,44,.04) 34%, transparent 56%);
    mix-blend-mode: multiply; pointer-events:none;
}
.ocsb-card.age-4.fix-tape::before { filter: sepia(.35) saturate(.85) brightness(.92); opacity:.78; }

/* age-5 — moldy base */
.ocsb-card.age-5 {
    transform: rotate(calc(var(--tilt) + 1.6deg)) skewX(-.9deg);
    filter: contrast(1.02) saturate(.55) brightness(.86) sepia(.12);
    border-radius: 6px 9px 5px 10px;
    box-shadow:
        inset 30px 20px 26px -30px rgba(0,0,0,.34),
        inset -26px 36px 26px -30px rgba(0,0,0,.30),
        inset 0 -28px 34px -22px rgba(60,66,40,.55),
        var(--ocsb-note-lift);
}
/* MOLD PATTERN 1 (default, age-5/age-6): mold blooms out of the brown drops */
.ocsb-card.age-5::after,
.ocsb-card.age-6::after {
    content:""; position:absolute; inset:0; width:auto; height:auto;
    clip-path:none; transform:none; box-shadow:none; border-radius: inherit;
    background:
        radial-gradient(ellipse 27% 22% at 24% 72%, rgba(96,64,34,.36) 0 60%, transparent 82%),
        radial-gradient(ellipse 22% 18% at 70% 40%, rgba(96,64,34,.30) 0 58%, transparent 82%),
        radial-gradient(circle at 22% 74%, rgba(34,46,24,.80) 0 6px, rgba(54,68,34,.34) 11px, rgba(54,68,34,.12) 17px, transparent 26px),
        radial-gradient(circle at 31% 66%, rgba(58,72,38,.55) 0 4px, rgba(58,72,38,.18) 9px, transparent 16px),
        radial-gradient(circle at 70% 42%, rgba(28,36,22,.76) 0 5px, rgba(48,60,32,.30) 10px, rgba(48,60,32,.12) 16px, transparent 24px),
        radial-gradient(circle at 78% 34%, rgba(60,70,40,.5) 0 4px, transparent 13px),
        radial-gradient(circle at 46% 84%, rgba(30,28,20,.55) 0 2px, transparent 3px),
        radial-gradient(circle at 40% 50%, rgba(30,28,20,.45) 0 1.4px, transparent 2px),
        radial-gradient(circle at 62% 60%, rgba(30,28,20,.45) 0 1.4px, transparent 2px),
        radial-gradient(circle at 50% 60%, rgba(60,56,36,.10), transparent 65%);
    mix-blend-mode: multiply; pointer-events:none;
}
/* MOLD PATTERN 2 — corner clusters, blacker */
.ocsb-card.mold-2.age-5::after,
.ocsb-card.mold-2.age-6::after {
    background:
        radial-gradient(ellipse 24% 20% at 30% 26%, rgba(90,60,32,.34) 0 60%, transparent 82%),
        radial-gradient(ellipse 20% 24% at 82% 70%, rgba(90,60,32,.30) 0 58%, transparent 82%),
        radial-gradient(circle at 28% 24%, rgba(26,32,20,.82) 0 7px, rgba(46,58,32,.32) 13px, rgba(46,58,32,.12) 20px, transparent 30px),
        radial-gradient(circle at 18% 34%, rgba(50,62,34,.5) 0 4px, transparent 14px),
        radial-gradient(circle at 84% 68%, rgba(24,30,18,.78) 0 6px, rgba(44,56,30,.30) 12px, rgba(44,56,30,.12) 18px, transparent 27px),
        radial-gradient(circle at 90% 80%, rgba(52,62,36,.48) 0 4px, transparent 12px),
        radial-gradient(circle at 55% 18%, rgba(30,28,20,.5) 0 2px, transparent 3px),
        radial-gradient(circle at 60% 46%, rgba(30,28,20,.42) 0 1.4px, transparent 2px),
        radial-gradient(circle at 35% 70%, rgba(30,28,20,.42) 0 1.4px, transparent 2px),
        radial-gradient(circle at 50% 50%, rgba(58,54,34,.10), transparent 65%);
}
/* MOLD PATTERN 3 — big central bloom, greener */
.ocsb-card.mold-3.age-5::after,
.ocsb-card.mold-3.age-6::after {
    background:
        radial-gradient(ellipse 32% 22% at 52% 58%, rgba(92,64,34,.30) 0 60%, transparent 82%),
        radial-gradient(circle at 50% 56%, rgba(40,56,28,.72) 0 5px, rgba(60,78,40,.30) 11px, rgba(60,78,40,.12) 18px, transparent 27px),
        radial-gradient(circle at 64% 66%, rgba(30,40,22,.74) 0 5px, rgba(54,70,36,.28) 10px, transparent 18px),
        radial-gradient(circle at 40% 64%, rgba(58,74,38,.5) 0 4px, transparent 13px),
        radial-gradient(circle at 80% 80%, rgba(26,34,20,.76) 0 5px, rgba(48,62,32,.28) 10px, transparent 17px),
        radial-gradient(circle at 30% 40%, rgba(56,70,38,.45) 0 3px, transparent 11px),
        radial-gradient(circle at 56% 48%, rgba(30,28,20,.45) 0 1.6px, transparent 2px),
        radial-gradient(circle at 70% 56%, rgba(30,28,20,.45) 0 1.6px, transparent 2px),
        radial-gradient(circle at 48% 70%, rgba(30,28,20,.45) 0 1.6px, transparent 2px),
        radial-gradient(circle at 50% 58%, rgba(58,60,36,.12), transparent 62%);
}
.ocsb-card.age-5.fix-tape::before { filter: sepia(.5) saturate(.7) brightness(.85); opacity:.7; }

/* age-6 — ANCIENT (1yr+ in one section): full mold (inherits the age-5 ::after,
   incl. mold-2/3) PLUS a corner cobweb + spider on ::before, on deader paper.
   BoardCardNote never emits fix-tape with age-6 — the ::before is the cobweb. */
.ocsb-card.age-6 {
    transform: rotate(calc(var(--tilt) + 1.7deg)) skewX(-1deg);
    filter: contrast(1.0) saturate(.46) brightness(.78) sepia(.16);
    border-radius: 6px 10px 5px 11px;
    box-shadow:
        inset 30px 20px 26px -30px rgba(0,0,0,.40),
        inset -26px 36px 26px -30px rgba(0,0,0,.34),
        inset 0 -28px 34px -22px rgba(50,54,34,.60),
        var(--ocsb-note-lift);
}
.ocsb-card.age-6::before {
    content:""; position:absolute; top:-6px; right:-6px; left:auto; bottom:auto;
    width:122px; height:122px; transform:none; border:0; box-shadow:none; opacity:.92;
    background: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%20160%20160%27%3E%3Cg%20fill%3D%27none%27%20stroke%3D%27%23a9a9a9%27%20stroke-opacity%3D%270.6%27%20stroke-width%3D%271%27%3E%3Cpath%20d%3D%27M160%200%20L0%2012%27%2F%3E%3Cpath%20d%3D%27M160%200%20L10%2042%27%2F%3E%3Cpath%20d%3D%27M160%200%20L30%2095%27%2F%3E%3Cpath%20d%3D%27M160%200%20L70%20140%27%2F%3E%3Cpath%20d%3D%27M160%200%20L120%20155%27%2F%3E%3Cpath%20d%3D%27M160%200%20L156%2092%27%2F%3E%3Cpath%20d%3D%27M104%204%20L108%2014%20L115%2033%20L129%2049%20L146%2054%20L158%2032%27%2F%3E%3Cpath%20d%3D%27M56%207%20L63%2026%20L76%2062%20L102%2091%20L134%20101%20L157%2059%27%2F%3E%3Cpath%20d%3D%27M16%2010%20L25%2036%20L43%2086%20L79%20126%20L124%20140%20L156%2082%27%2F%3E%3C%2Fg%3E%3Cpath%20d%3D%27M160%200%20L111%2064%27%20stroke%3D%27%23a9a9a9%27%20stroke-opacity%3D%270.55%27%20stroke-width%3D%271%27%20fill%3D%27none%27%2F%3E%3Cg%20stroke%3D%27%23211c18%27%20stroke-width%3D%271.6%27%20fill%3D%27none%27%20stroke-linecap%3D%27round%27%3E%3Cpath%20d%3D%27M104%2071%20q-18%20-7%20-31%20-3%27%2F%3E%3Cpath%20d%3D%27M105%2074%20q-21%200%20-33%209%27%2F%3E%3Cpath%20d%3D%27M105%2078%20q-18%206%20-28%2017%27%2F%3E%3Cpath%20d%3D%27M106%2081%20q-14%2011%20-20%2023%27%2F%3E%3Cpath%20d%3D%27M117%2071%20q15%20-8%2027%20-6%27%2F%3E%3Cpath%20d%3D%27M118%2074%20q19%20-2%2031%204%27%2F%3E%3Cpath%20d%3D%27M118%2078%20q16%206%2027%2015%27%2F%3E%3Cpath%20d%3D%27M117%2081%20q12%2011%2016%2023%27%2F%3E%3C%2Fg%3E%3Cellipse%20cx%3D%27111%27%20cy%3D%2779%27%20rx%3D%278.5%27%20ry%3D%2710.5%27%20fill%3D%27%23211c18%27%2F%3E%3Ccircle%20cx%3D%27111%27%20cy%3D%2767%27%20r%3D%275%27%20fill%3D%27%23211c18%27%2F%3E%3Ccircle%20cx%3D%27109%27%20cy%3D%2766%27%20r%3D%271%27%20fill%3D%27%23d9d2c4%27%2F%3E%3Ccircle%20cx%3D%27113%27%20cy%3D%2766%27%20r%3D%271%27%20fill%3D%27%23d9d2c4%27%2F%3E%3C%2Fsvg%3E") no-repeat top right / contain;
    pointer-events:none;
}

/* ── Aging fades on hover (CEO 2026-08-19: "on hover aging should fade away to
   make the ticket more readable"). The pointer over a note lifts every decay
   artifact — the card-level filter/inset-shadow wash, the stain/mold ::after,
   the cobweb+spider ::before, and the aged tape's sepia — back to the fresh
   look, and eases back when the pointer leaves. Only the AGING dressings
   revert; paper, tilt, tape and watermark stay, because they are identity,
   not age. The base transition lives on .ocsb-card so both directions ease. */
.ocsb-card { transition: filter .18s ease, box-shadow .18s ease; }
.ocsb-card::after, .ocsb-card::before { transition: opacity .18s ease, filter .18s ease; }

.ocsb-card.age-1:hover, .ocsb-card.age-2:hover, .ocsb-card.age-3:hover,
.ocsb-card.age-4:hover, .ocsb-card.age-5:hover, .ocsb-card.age-6:hover {
    filter: none;
    box-shadow: var(--ocsb-note-lift);
}
.ocsb-card.age-3:hover::after, .ocsb-card.age-4:hover::after,
.ocsb-card.age-5:hover::after, .ocsb-card.age-6:hover::after {
    opacity: 0;
}
.ocsb-card.age-6:hover::before { opacity: 0; }   /* the cobweb and its spider */
.ocsb-card.age-4.fix-tape:hover::before,
.ocsb-card.age-5.fix-tape:hover::before {
    filter: none;
    opacity: 1;   /* the tape itself stays — only its aged sepia lifts */
}

/* --------------------------------------------------------- column surface */

/* The horizontal scroll box AND the drag-to-pan surface. `cursor: grab` matches
   .ocsb-wall-viewport's affordance: on both surfaces, dragging empty space moves
   the board. The mechanism differs (the wall transforms its canvas, this scrolls
   natively) but the gesture is the same, so the pointer says the same thing.

   touch-action is deliberately LEFT ALONE. The wall sets touch-action:none because
   its engine owns every gesture; here the browser's own scrolling is what a finger
   gets, and the engine only claims the mouse — see the PAN note in scrum-board.js. */
.ocsb-cols {
    display: flex;
    gap: .75rem;
    align-items: flex-start;
    overflow-x: auto;
    flex: 1 1 auto;
    min-height: 0;
    padding-bottom: .5rem;
    cursor: grab;
}

/* Panning: the surface holds the gesture, so nothing under the pointer may start a
   text selection — the class is applied on pointerdown, before the browser would. */
.ocsb-cols.is-panning {
    cursor: grabbing;
    user-select: none;
}

/* A card drag holds the surface too, and the cursor has to say so everywhere the
   pointer can travel: the ghost is pointer-events:none, so what the pointer is
   actually over during a drag is the board underneath. */
.ocsb-cols.is-dragging,
.ocsb-cols.is-dragging .ocsb-col-card { cursor: grabbing; }

/* A read-only board never mounts the engine, so nothing here drags OR pans. Say so
   with the cursor rather than offering a grab that does nothing. */
.ocsb-cols.is-readonly { cursor: default; }

/* Columns take the same per-section accent as the wall zones (inline
   --ocsb-accent from the section's ColorToken, or the default cycle). */
.ocsb-col {
    flex: 0 0 18rem;
    display: flex;
    flex-direction: column;
    max-height: 100%;
    --ocsb-accent: var(--oc-gray-500);
    border: 1px solid var(--oc-border);
    border-radius: var(--oc-radius-lg);
    background: color-mix(in srgb, var(--ocsb-accent) 6%, var(--oc-gray-50));
    transition: background var(--oc-dur-fast) var(--oc-ease);
}

.ocsb-col.is-drop-target { background: color-mix(in srgb, var(--ocsb-accent) 16%, var(--oc-gray-50)); }

/* My Board (2026-08-17): while a card is mid-drag, columns its home board has no
   section for dim and never highlight — the drag engine also refuses the drop, so
   this is the SIGNAL half of "it should not allow dropping an item on a column not
   supported". Opacity only: no colour of its own, so it works on every accent. */
.ocsb-col.is-drop-disallowed { opacity: .4; }
.ocsb-col.is-drop-disallowed .ocsb-col-card { cursor: default; }

/* The header is a LABEL, not empty space: pressing it must not pan the board (the
   engine excludes .ocsb-col-head), so it must not wear the grab cursor either. */
.ocsb-col-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: .5rem;
    padding: .625rem .75rem;
    border-bottom: 2px solid color-mix(in srgb, var(--ocsb-accent) 55%, var(--oc-border));
    cursor: default;
}

.ocsb-col-name {
    font-weight: 700;
    font-size: var(--oc-fs-sm);
    color: color-mix(in srgb, var(--ocsb-accent) 45%, var(--oc-text));
}
.ocsb-col-count { font-size: var(--oc-fs-xs); color: var(--oc-text-muted); font-variant-numeric: tabular-nums; }

.ocsb-col-body {
    display: flex;
    flex-direction: column;
    gap: .5rem;
    padding: .5rem;
    overflow-y: auto;
    min-height: 4rem;
}

.ocsb-col-card {
    position: relative;        /* the bulk-selection check badge anchors to it */
    padding: .5rem .625rem;
    border: 1px solid var(--oc-border);
    border-radius: var(--oc-radius-sm);
    background: var(--oc-surface-raised, var(--oc-surface));
    color: var(--oc-text);
    box-shadow: var(--oc-shadow-sm);
    cursor: grab;
    user-select: none;
    touch-action: none;
}

/* ---------------------------------------------------- the columns drag layer

   WHY A LAYER AND NOT A z-index (2026-08-12).

   A column card used to be dragged IN PLACE — .is-dragging plus a transform on the
   card itself — and it drew BEHIND its neighbours and behind the next column for
   the whole drag. That is not a stacking bug. .ocsb-cols has overflow-x:auto and
   .ocsb-col-body has overflow-y:auto, and an `overflow` ancestor CLIPS a
   transformed descendant: the card was being painted inside its own column's
   scroll box. No z-index escapes a clip, and dropping the overflow would take the
   columns' scrolling with it.

   So the thing that follows the pointer is a CLONE on this layer — fixed, outside
   every scroll box on the page, and outside Blazor's <app> root so no re-render
   can touch it. The real card stays where it is as the placeholder
   (.is-drag-source below), which is also why the column never collapses mid-drag.

   z-index 10000 sits above the shell overlays that use 9999 (the ghost is fixed
   and can travel over the sidebar) and below the modal layer at 10500/10501 — a
   modal cannot be open during a drag, and if one somehow is, it wins. */
.ocsb-drag-layer {
    position: fixed;
    inset: 0;
    z-index: 10000;
    pointer-events: none;   /* the ghost must never eat the drop hit test */
    /* The ghost has left .ocsb-page, so it inherits the board's font from nowhere.
       Everything else on a column card is stated in --oc-* tokens and travels. */
    font-family: var(--oc-font-sans);
}

.ocsb-drag-layer .ocsb-drag-ghost {
    position: fixed;
    left: 0;
    top: 0;                 /* the engine's transform carries the whole position */
    margin: 0;
    box-sizing: border-box;
    opacity: .95;
    box-shadow: var(--oc-shadow-lg);
    cursor: grabbing;
    pointer-events: none;
    will-change: transform;
}

/* The card left behind IS the placeholder: same element, same height, so nothing
   below it moves while the ghost is out. Faded and dashed so it reads as "this is
   where it came from" rather than as a second copy of the card. */
.ocsb-col-card.is-drag-source {
    opacity: .35;
    border-style: dashed;
    box-shadow: none;
}

.ocsb-col-card.is-readonly { cursor: default; }

.ocsb-col-card-title { font-weight: 600; font-size: var(--oc-fs-sm); }

/* A column card is portal chrome, not paper, so its meta line takes the house
   secondary-text token. --oc-text-muted states the softer tone directly, which is
   both computable and darker than a blanket fade would be. */
/* Same bottom-left gutter as the note's foot: the priority number is pinned to
   this row's bottom-left corner rather than merely leading it, so it holds that
   spot however many lines the row wraps to. */
.ocsb-col-card-meta {
    margin-top: .25rem;
    color: var(--oc-text-muted);
    position: relative;
    padding-left: 14px;
}

/* Owner bubble + name on a column card. The avatar is an OcAvatar, so the size
   comes from the component; this only lines the two up. */
.ocsb-card-person {
    display: inline-flex;
    align-items: center;
    gap: .25rem;
}

/* Co-owners on a column card: a short overlapping bubble stack after the primary,
   "+N" when there are more than fit. */
.ocsb-card-others {
    display: inline-flex;
    align-items: center;
}

.ocsb-card-others > * + * { margin-left: -.35rem; }

.ocsb-card-others-more {
    margin-left: .25rem !important;
    font-size: var(--oc-fs-xs);
    color: var(--oc-text-muted);
}

.ocsb-card-aging { color: var(--oc-orange-600); }

/* An empty column is a normal resting state AND a drop target, so it gets a
   one-line hint rather than the full OcEmptyState. */
.ocsb-col-empty { margin: .25rem; }

/* ------------------------------------------------------------- kiosk mode */

/* Kiosk strips the portal furniture so the wall fills a wall-mounted screen.
   Scoped under the body class the drag engine toggles, so it can never affect a
   normal page. Escape still exits: scrum-board.js removes the class on
   fullscreenchange.

   It used to name .oc-shell-nav and .oc-shell-header. NEITHER CLASS EXISTS —
   OcAppShell renders .oc-topbar, .oc-sidebar, .oc-right-rail, .oc-bottombar and
   .oc-shell-backdrop. So kiosk hid none of the chrome and the only rule that bit
   was the third one, which hid .oc-page-header — i.e. the zoom controls, Fit, and
   the Kiosk button itself. Those are exactly what someone standing at the screen
   needs, so the header now STAYS and only the chrome goes. */
body.ocsb-kiosk .oc-topbar,
body.ocsb-kiosk .oc-sidebar,
body.ocsb-kiosk .oc-right-rail,
body.ocsb-kiosk .oc-bottombar,
body.ocsb-kiosk .oc-shell-backdrop { display: none !important; }

/* Lift the whole page out of the document flow, the way the legacy Feature Board
   did it (body.ocfb-kiosk .ocfb-page { position:fixed; inset:0 }). The previous
   attempt gave the VIEWPORT height:100vh while it still started ~151px down the
   page inside the shell's content column, so a full viewport-height board hung
   below the fold on a TV. Nothing above the page can push it now, and the
   viewport's own flex:1 takes the remaining height instead of a fixed 100vh. */
body.ocsb-kiosk .ocsb-page {
    position: fixed;
    inset: 0;
    z-index: 5000;
    box-sizing: border-box;
    margin: 0;
    padding: var(--oc-space-4);
    background: var(--oc-surface);
    overflow: hidden;
}

/* Wall-display copy: the instructional subtitle, the breadcrumb and the filter bar
   are for someone driving the board with a mouse, not for a room reading it. The
   breadcrumb in particular is navigation — there is nowhere to click on a wall. */
body.ocsb-kiosk .oc-page-subtitle,
body.ocsb-kiosk .oc-breadcrumbs,
body.ocsb-kiosk .ocsb-filterbar { display: none !important; }

body.ocsb-kiosk .ocsb-wall-viewport {
    border: 0;
    border-radius: 0;
    min-height: 0;             /* let flex:1 own the height; 24rem could overflow a short screen */
}

/* ----------------------------------------------------------------- editors */

.ocsb-editor-row {
    display: flex;
    align-items: center;
    gap: .5rem;
    padding: .5rem;
    border: 1px solid var(--oc-border);
    border-radius: var(--oc-radius-md);
}

.ocsb-editor-row + .ocsb-editor-row { margin-top: .375rem; }

/* A row whose contents stack instead of sitting on one line (a trigger with its
   sentence underneath, the delete-section confirmation). */
.ocsb-editor-row-stacked {
    flex-direction: column;
    align-items: stretch;
    gap: .375rem;
}

/* The "add one more" row that follows a list. */
.ocsb-editor-row-spaced { margin-top: .75rem; }

/* One horizontal line of controls inside a stacked row. */
.ocsb-editor-line {
    display: flex;
    align-items: center;
    gap: .5rem;
}

.ocsb-editor-heading { margin-top: 1rem; }
.ocsb-editor-grow { flex: 1; min-width: 16rem; }
.ocsb-editor-name { max-width: 14rem; }
.ocsb-editor-num { width: 4.5rem; }
.ocsb-editor-num-wide { width: 7rem; }

.ocsb-grip { cursor: grab; color: var(--oc-text-muted); }

/* Section colour swatches (sections editor). The swatch shows the ACCENT token
   itself; the "auto" swatch is the neutral split chip meaning "cycle a default". */
.ocsb-swatches {
    display: inline-flex;
    align-items: center;
    gap: .25rem;
    flex-wrap: wrap;
}

.ocsb-swatch {
    width: 1.125rem;
    height: 1.125rem;
    padding: 0;
    border: 1px solid var(--oc-border);
    border-radius: 50%;
    background: var(--oc-gray-50);
    cursor: pointer;
}

.ocsb-swatch:focus-visible { outline: none; box-shadow: var(--oc-shadow-focus); }

.ocsb-swatch.is-selected {
    box-shadow: 0 0 0 2px var(--oc-surface), 0 0 0 4px var(--oc-orange);
}

.ocsb-swatch-auto {
    background: linear-gradient(135deg,
        var(--oc-gray-300) 0 44%, var(--oc-surface) 44% 56%, var(--oc-gray-300) 56% 100%);
}

/* The trigger editor reads as a sentence, not a rules-engine form: the whole point
   of keeping triggers to (scope, event, action, target) is that they stay
   explainable to the person configuring one. */
.ocsb-sentence {
    display: flex;
    align-items: center;
    gap: .375rem;
    flex-wrap: wrap;
    font-size: var(--oc-fs-sm);
}

.ocsb-sentence select,
.ocsb-sentence input {
    padding: .25rem .375rem;
    border: 1px solid var(--oc-border);
    border-radius: var(--oc-radius-xs);
    font-size: var(--oc-fs-sm);
}

.ocsb-sentence-name { max-width: 12rem; }
.ocsb-sentence-url { max-width: 20rem; }

/* OcUserPicker inside the sentence: hold it to a sentence-sized width and let it
   position its own dropdown. */
.ocsb-sentence-picker {
    display: inline-block;
    position: relative;
    min-width: 14rem;
}

.ocsb-secret {
    font-family: ui-monospace, monospace;
    font-size: var(--oc-fs-xs);
    padding: .375rem .5rem;
    border: 1px dashed var(--oc-orange-600);
    border-radius: var(--oc-radius-sm);
    word-break: break-all;
}

/* ------------------------------------------------------------ card dialog */

/* The scrim behind the card modal. A token, not a raw rgba: a literal colour
   cannot flip for dark mode, and --oc-overlay already carries both values. */
.ocsb-modal-scrim { background: var(--oc-overlay); }

/* The "which column?" prompt (CEO 2026-08-19). The choices are the board's own
   column NAMES, stacked and full width: the reader is telling two columns of the
   SAME status apart, so the name is the entire decision and must not be
   truncated onto one crowded row. */
.ocsb-column-choices {
    display: flex;
    flex-direction: column;
    gap: .5rem;
    margin-top: .75rem;
}

.ocsb-column-choices .oc-btn { justify-content: flex-start; width: 100%; }

/* ── The Rock popup, hosted over the card dialog (CEO 2026-08-16) ─────────────
   RockEditDrawer is reused AS-IS — same editor as the Rocks page, so there is no
   second Rock editor to disagree with the first — and it brings its own scrim and
   its own z-indexes (55 / 60). Those are right on the Rocks page and far too low
   here: a Bootstrap .modal sits at 1055, so hosted inside the card dialog the
   drawer opens BEHIND it and reads as "the link does nothing".

   Fixed WITHOUT touching the drawer's stylesheet: this wrapper is positioned with
   a z-index above the modal, which makes it a stacking context of its own. The
   drawer's 55 and 60 then only order the drawer's scrim against the drawer's
   panel, and the whole thing paints above the card dialog. Overriding the
   drawer's own values instead would have been a specificity race against scoped
   CSS (its selectors carry a [b-*] attribute) that the boards sheet can lose
   depending on bundle order. */
.ocsb-rock-popup { position: fixed; inset: 0; z-index: 1090; }

/* The "from EOS Rocks" row on the card dialog. */
.ocsb-rock-link-row { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }

.ocsb-rock-badge {
    font-size: var(--oc-fs-xs);
    font-weight: 700;
    letter-spacing: .03em;
    color: var(--oc-text-muted);
}

.ocsb-watch-row {
    display: flex;
    align-items: center;
    gap: .5rem;
    flex-wrap: wrap;
}

/* The live "N watching" figure. Re-read after every toggle. */
.ocsb-watch-count { margin: 0; }

/* The dialog's field grid: Team | Owner(s) on one line, Due date on the next, every
   cell top-aligned and starting at the left edge. Its own class because the global
   `div.row` rule in app.css centres rows and their contents. One column on a phone. */
.ocsb-fields {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: .5rem 1rem;
    align-items: start;
    margin-bottom: .5rem;
}

.ocsb-field { min-width: 0; }

@media (max-width: 47.98rem) {
    .ocsb-fields { grid-template-columns: minmax(0, 1fr); }
}

/* Owners: one row per owner (avatar + inline dropdown + remove), then the
   "add another" dropdown. Change a person in place; nothing to click first. */
.ocsb-owners {
    display: flex;
    flex-direction: column;
    gap: .375rem;
}

.ocsb-owner-row {
    display: flex;
    align-items: center;
    gap: .5rem;
}

.ocsb-owner-row .ocsb-owner-select { flex: 1 1 auto; min-width: 0; }

.ocsb-owner-remove { flex: 0 0 auto; }

/* The add dropdown reads as an invitation, not a value: dashed, muted, quiet. */
.ocsb-owner-add {
    border-style: dashed;
    color: var(--oc-text-muted);
    background-color: transparent;
}

.ocsb-owner-add:focus { color: var(--oc-color-text); }

/* History: last thing in the dialog, closed until asked for. */
.ocsb-history {
    margin-top: 1rem;
    border-top: 1px solid var(--oc-border-soft);
    padding-top: .5rem;
}

.ocsb-history-summary {
    display: flex;
    align-items: center;
    gap: .375rem;
    cursor: pointer;
    list-style: none;
    font-weight: 600;
    font-size: var(--oc-fs-sm);
    color: var(--oc-color-text);
    user-select: none;
}

.ocsb-history-summary::-webkit-details-marker { display: none; }

.ocsb-history-summary::before {
    content: "";
    display: inline-block;
    width: .4rem;
    height: .4rem;
    border-right: 2px solid var(--oc-text-muted);
    border-bottom: 2px solid var(--oc-text-muted);
    transform: rotate(-45deg);
    transition: transform .15s ease;
    margin-right: .125rem;
}

.ocsb-history[open] > .ocsb-history-summary::before { transform: rotate(45deg); }

.ocsb-history-body { padding: .5rem 0 0 .25rem; }

.ocsb-history-h {
    font-size: var(--oc-fs-xs);
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--oc-text-muted);
    margin: .5rem 0 .25rem;
}

.ocsb-history-times li,
.ocsb-history-list li { font-size: var(--oc-fs-sm); }

/* ------------------------------------------------------------- small screens */

@media (max-width: 48rem) {
    .ocsb-col { flex-basis: 15rem; }
    /* The wall is a large-screen/touchscreen surface; on a phone the toolbar wraps
       and the viewport gets the remaining height rather than a fixed minimum. */
    .ocsb-wall-viewport { min-height: 18rem; }
}

/* ---------------------------------------------------------- reduced motion */

@media (prefers-reduced-motion: reduce) {
    .ocsb-card { transition: none; }
    .ocsb-card.is-aging { animation: none; }
}

/* The reason a card dialog failed, shown IN the failure rather than only in a log an OC
   admin has to go and read (CEO 2026-08-22). Monospace and wrapped: it carries exception
   text, which has no spaces to break on and would otherwise run off the dialog. */
.ocsb-card-error-detail {
    margin: .25rem 0 0;
    padding: .5rem .625rem;
    border-radius: var(--oc-radius-sm);
    background: var(--oc-gray-50);
    color: var(--oc-text);
    font-size: var(--oc-fs-sm);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    max-height: 12rem;
    overflow-y: auto;
}
