/* ============================================================
   octa-sales — app-level styles. COMPLEMENTS the Octacer design
   system (octacer-theme.css owns .oc-card / .btn / .field-* / .badge /
   .oc-table / .oc-empty / .oc-stat / .oc-hero / .oc-tabs). This file adds
   only the app SHELL + layout helpers and never redefines a theme class.
   Uses the theme's CSS variables (--brand-*, --ink-*, --line, --surface).
   ============================================================ */

/* Set by portal.js the moment a portal page enters the browser's back/forward cache, so the snapshot it
   keeps has nothing readable in it. Without this, pressing Back after signing out paints the old page —
   dashboard, flash messages and all — for the frame or two before the forced reload lands. */

.oc-brand-badge { width: 2.1rem; height: 2.1rem; border-radius: .65rem; display: grid; place-items: center; color: var(--primary-foreground); background: linear-gradient(180deg, var(--brand-500), var(--brand-700)); box-shadow: 0 2px 6px -1px rgba(79,70,229,.5); }
.oc-brand-badge i { width: 18px; height: 18px; }

.oc-nav-label { font-size: .64rem; text-transform: uppercase; letter-spacing: .07em; color: var(--ink-400); font-weight: 700; padding: .8rem .6rem .3rem; }

.oc-side-user { flex: 1; min-width: 0; }
.oc-side-user .n { font-size: .82rem; font-weight: 600; color: var(--ink-900); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.oc-side-user .r { font-size: .7rem; color: var(--ink-400); }



/* Guest top bar (unauthenticated) */

/* ---------- Spacing utilities (no inline styles allowed in .cshtml) ---------- */
.oc-mt { margin-top: 1.25rem; }
.oc-mb { margin-bottom: 1.25rem; }
.oc-mb-lg { margin-bottom: 1.5rem; }
.oc-mb-sm { margin-bottom: .6rem; }

/* Fuses two adjacent .oc-card elements into what reads as one continuous card — used where a fixed-price
   proposal's single pricing item and its totals card sit flush against each other instead of as two
   visibly separate boxes. */

/* One priced line's fields (_PricingItemFields.cshtml), spaced by the wrapper itself rather than by
   `.field`'s own margin — which only exists inside an `.oc-form` ancestor (see `.oc-form .field` above).
   This partial renders both inside one (the Builder's inline section, whose form carries `oc-form`) and
   outside one (the dedicated Pricing page's save form, which does not) — without its own spacing, a field
   sitting directly outside a grid had NO gap at all from the field above it on that second page, and two
   adjacent 1px borders with nothing between them read as one missing border, not two present ones. The
   nested `.oc-form-grid` rows keep their own margin too; cancelled here so gaps stay one size, not two. */
.oc-pricing-item-fields { display: flex; flex-direction: column; gap: 1rem; }
.oc-pricing-item-fields .oc-form-grid { margin-bottom: 0; }
/* Same reason: the gap above already spaces it from the field before it, so its own margin-top would
   stack on top of that gap (flex gap does not collapse with margins) and open a visibly wider gap here
   than everywhere else on the same card. The divider line (padding/border) still stands. */
.oc-pricing-item-fields > .oc-price-item-total { margin-top: 0; }

/* ---------- Page header ---------- */
.oc-page-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; margin-bottom: 1.5rem; }
.oc-page-title { font-size: 1.4rem; font-weight: 800; letter-spacing: -.02em; color: var(--ink-900); }
.oc-page-sub { color: var(--ink-500); font-size: .9rem; margin-top: .25rem; max-width: 60ch; }
.oc-head-actions { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; }
/* Dummy Data for Testing + its Delete sit LAST in the markup (so a crowded row wraps them, not Send —
   see the comment beside them in Builder.cshtml) but read FIRST, to the left of Preview/Send: visual
   order via flex `order`, independent of the DOM order the wrap behaviour depends on. */
.oc-head-actions [data-oc-confirm="#dummy_data_modal"] { order: -2; }
.oc-head-actions [data-oc-confirm="#delete_dummy_data_modal"] { order: -1; }
/* The proposal / agreement Builder's header: Send / Send update is always the LAST button, on the same line
   right after Duplicate (by request). The button row takes all the width the title leaves (flex: 1) rather
   than shrinking to its own narrowest size — which is what wrapped Send onto a line of its own while the
   header still had room — and stays right-aligned, so on a genuinely narrow screen Send still wraps to the
   right edge. */
.oc-page-head .oc-head-actions:has([data-testid="proposal__send"]) { justify-content: flex-end; flex: 1 1 auto; min-width: 0; }
.oc-head-actions [data-testid="proposal__send"] { order: 99; }
/* Agreement: Save Agreement right before Send — and, once sent (no Send any more), the last button:
   View · Download · Save Agreement (by request). */
.oc-head-actions [data-testid="agreement__save"] { order: 98; }
/* All of the Builder's header buttons on ONE line (by request). Beside the title they need more width than
   a laptop window leaves (~830px with full labels), so: the header may wrap, and the button row asks for
   ~42rem — when the title leaves less than that, the whole row moves under the title and spans the full
   width, right-aligned, instead of wrapping Send onto a line of its own. Below 100rem the testing chip
   "Dummy Data for Testing" shows its flask icon only (its name stays in aria-label / the tooltip), as it
   already does on a phone — that is what lets the row fit in one line. Desktop only: ≤60rem keeps its own
   two-line phone layout below. */
@media (min-width: 60.0625rem) {
    .oc-page-head:has([data-testid="proposal__send"]) { flex-wrap: wrap; }
    .oc-page-head .oc-head-actions:has([data-testid="proposal__send"]) { flex: 1 1 42rem; }
}
@media (min-width: 60.0625rem) and (max-width: 100rem) {
    .oc-page-head .oc-head-actions [data-oc-confirm="#dummy_data_modal"] .oc-btn-label { display: none; }
}

/* ---------- Card body (theme .oc-card intentionally has no padding) ---------- */
.oc-card-body { padding: 1.25rem; }

/* The Deliverables billing-type strip and the submit form directly under it (_TabDeliverables.cshtml)
   are two separate .oc-card-body blocks in one card, so stacking their padding as-is would double the
   gap between the tab strip and the File/Link toggle beneath it. The strip's own .oc-mb on its tabs
   already sets that one gap deliberately, so each side gives up the padding facing the other. */
.oc-tab-strip-billing { padding-bottom: 0; }
.oc-tab-strip-billing + .oc-card-body { padding-top: 0; }
/* Auto-pad bare content dropped straight into a card so nothing sits flush to the edge. */
.oc-card > .oc-dl,
.oc-card > .oc-list,
.oc-card > .oc-funnel,
.oc-card > .oc-assist-links,
.oc-card > .oc-checklist,
.oc-card > .oc-niche-stats,
.oc-card > .oc-empty,
.oc-card > textarea { padding: 1.15rem 1.25rem 1.25rem; margin: 0; }
.oc-card > p { padding: .3rem 1.25rem; margin: 0; color: var(--ink-600); font-size: .9rem; line-height: 1.55; }
.oc-card > p:first-of-type { padding-top: 1.15rem; }
.oc-card > p:last-of-type { padding-bottom: 1.25rem; }
.oc-card > .badge { margin: 0 1.25rem 1.25rem; }
.oc-card > .oc-alert { margin: 0 1.25rem 1.25rem; }
/* action bar placed directly in a card (e.g. the cockpit plan approve row) */
.oc-card > .oc-form-actions { padding-left: 1.25rem; padding-right: 1.25rem; padding-bottom: 1.25rem; }

/* ---------- Engagement card: the pre-project narrative (Parts 14-20) ----------
   The client's own words for where things stand, one clear action or one reassurance, and a checklist —
   deliberately not a percentage or a database status name. Sits above everything else on the dashboard
   because it answers the question the client actually opened the page to ask. */
.oc-engagement { display: flex; flex-direction: column; gap: 1.5rem; }
.oc-engagement-title { font-size: 1.35rem; font-weight: 800; letter-spacing: -.01em; color: var(--ink-900); margin-top: .25rem; }
.oc-engagement-detail { color: var(--ink-600); font-size: .95rem; max-width: 62ch; margin-top: .4rem; line-height: 1.55; }
.oc-engagement-note {
    display: inline-flex; align-items: center; gap: .5rem;
    color: var(--ink-500); font-size: .875rem; font-weight: 600;
}
.oc-engagement-note.is-brand { color: var(--brand-700); }

.oc-engagement-steps {
    display: flex; flex-wrap: wrap; gap: 1.5rem 2rem; list-style: none; margin: 0; padding: 0;
    padding-top: 1.25rem; border-top: 1px solid var(--line-2);
}
.oc-engagement-steps li { display: flex; align-items: center; gap: .6rem; font-size: .875rem; color: var(--ink-400); }
.oc-engagement-steps li.is-done { color: var(--ink-700); }
.oc-engagement-steps li.is-current { color: var(--brand-700); font-weight: 700; }
.oc-engagement-dot {
    width: 1.35rem; height: 1.35rem; border-radius: 999px; flex-shrink: 0;
    border: 2px solid var(--line); display: grid; place-items: center; color: #fff;
}
.oc-engagement-steps li.is-done .oc-engagement-dot { background: var(--brand-600); border-color: var(--brand-600); }
.oc-engagement-steps li.is-current .oc-engagement-dot { border-color: var(--brand-600); border-width: 2px; }

@media (max-width: 640px) {
    .oc-engagement-steps { gap: .85rem 1.5rem; }
}

/* ---------- Prospect card: the pre-DECISION narrative (§7.2, PP-001 to PP-009) ----------
   Reuses .oc-engagement's card shell; adds a plain numbered "what happens next" explainer (not a
   progress tracker — nothing here is done/current, it's instructions) and an inline contact block so
   "who do I ask" (§40) doesn't send a first-time prospect hunting through a separate page. */
.oc-next-steps, .oc-next-help {
    padding-top: 1.25rem; border-top: 1px solid var(--line-2);
}
.oc-next-steps ol {
    list-style: none; counter-reset: step; margin: .6rem 0 0; padding: 0;
    display: flex; flex-direction: column; gap: .5rem;
}
.oc-next-steps ol li {
    counter-increment: step;
    display: flex; align-items: center; gap: .65rem;
    font-size: .9rem; color: var(--ink-700);
}
.oc-next-steps ol li::before {
    content: counter(step);
    width: 1.35rem; height: 1.35rem; flex-shrink: 0; border-radius: 999px;
    background: var(--brand-50); color: var(--brand-700); font-weight: 700; font-size: .78rem;
    display: grid; place-items: center;
}

/* ---------- Hero banner (theme .oc-hero provides the gradient; we add padding/content) ---------- */
.oc-hero-body { padding: 1.6rem 1.85rem; display: flex; align-items: center; justify-content: space-between; gap: 1.5rem; flex-wrap: wrap; }
.oc-hero-title { font-size: 1.5rem; font-weight: 800; letter-spacing: -.02em; }
.oc-hero-sub { color: rgba(255, 255, 255, .8); font-size: .9rem; margin-top: .3rem; max-width: 52ch; }
.oc-hero-metrics { display: flex; gap: 2rem; }
.oc-hero-metric .v { font-size: 1.7rem; font-weight: 800; line-height: 1; letter-spacing: -.02em; }
.oc-hero-metric .l { font-size: .74rem; color: rgba(255, 255, 255, .75); margin-top: .3rem; text-transform: uppercase; letter-spacing: .04em; }

/* ---------- Stat / KPI cards ---------- */
.oc-kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 1rem; margin-bottom: 1.5rem; }
.oc-kpi { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); padding: 1.15rem 1.25rem; transition: box-shadow .2s, transform .2s, border-color .2s; }
.oc-kpi:hover { box-shadow: var(--shadow-md); transform: translateY(-2px); border-color: var(--line-strong); }
.oc-kpi-label { font-size: .74rem; text-transform: uppercase; letter-spacing: .04em; color: var(--ink-500); font-weight: 600; }
.oc-kpi-value { font-size: 1.9rem; font-weight: 800; color: var(--ink-900); margin-top: .3rem; letter-spacing: -.02em; line-height: 1; }
.oc-kpi-foot { font-size: .78rem; color: var(--ink-400); margin-top: .3rem; }

/* ── Client proposal cards (PORTAL-UI-011/012) ───────────────────────────────────────────────────────
   A client has one or two proposals and one question about each; the admin table this replaced was built
   to scan dozens. Cards, not rows, so the status and the action are the two things the eye lands on. */
.oc-proposal-cards { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: minmax(0, 1fr); gap: 1rem; }
/* minmax(0,1fr), not the implicit auto column: an auto grid track will not shrink below its content, so one
   nowrap row of buttons pushed the whole card past the right edge of the screen and off-centre. */
.oc-proposal-cards > li { min-width: 0; }
.oc-proposal-card-head {
    display: flex; align-items: flex-start; justify-content: space-between;
    gap: 1rem; flex-wrap: wrap; margin-bottom: .9rem;
}
.oc-proposal-card-title {
    font-size: 1.05rem; font-weight: 700; color: var(--ink-900); margin: 0; line-height: 1.3;
}
/* A declined proposal stays listed (the client's own decision, and why a later copy of the same title
   exists) but must not read as a second open proposal beside the live one: quieter by opacity only, so
   the badge, date and buttons keep their contrast for anyone who does need to read it. */
.oc-proposal-card.is-closed { opacity: .72; }
.oc-proposal-card.is-closed:hover, .oc-proposal-card.is-closed:focus-within { opacity: 1; }
.oc-proposal-card-foot {
    display: flex; flex-wrap: wrap; align-items: center; gap: .55rem; margin-top: 1rem;
    padding-top: .9rem; border-top: 1px solid var(--line);
}
/* BROWSER AUTOFILL / SUGGESTION HOVER (by request, everywhere). Hovering a remembered suggestion makes the
   browser paint the INPUT only — inside a .field-group (search icon + input, dialling code + number) that
   left the group's padding and the code segment unpainted, so the highlight covered part of the field.
   The browser's own paint is covered with one tint (inset shadow — the only thing that beats its
   !important background) and the SAME tint fills the whole group, so the highlight is the full field. A
   plain .field-input gets the same tint, so every field reads alike. */
:root { --oc-autofill: color-mix(in srgb, var(--brand-500, #a3dc2f) 9%, var(--surface, #fff)); }
.field-input:-webkit-autofill, .field-input:-webkit-autofill:hover, .field-input:-webkit-autofill:focus,
.field-group > input:-webkit-autofill, .field-group > input:-webkit-autofill:hover, .field-group > input:-webkit-autofill:focus {
    -webkit-box-shadow: 0 0 0 1000px var(--oc-autofill) inset;
    box-shadow: 0 0 0 1000px var(--oc-autofill) inset;
    -webkit-text-fill-color: var(--ink-900);
    caret-color: var(--ink-900);
}
/* The theme's focus ring is a box-shadow too — keep it alongside the tint. */
.field-input:-webkit-autofill:focus {
    -webkit-box-shadow: 0 0 0 1000px var(--oc-autofill) inset, var(--ring);
    box-shadow: 0 0 0 1000px var(--oc-autofill) inset, var(--ring);
}
.field-group:has(> input:-webkit-autofill) { background: var(--oc-autofill); }
.field-group:has(> input:-webkit-autofill) > .oc-searchable .oc-searchable-trigger { background: transparent; }

/* Invoice view, Billing card (by request): its items table scrolls sideways, and the scrollbar ran past
   the card's rounded corners at either end. Clipping the card to its own shape keeps it inside. */
/* Specificity matters: ".oc-card:has(.oc-table-scroll) { overflow: visible }" (narrow-viewport layer, for card menus) is
   0,2,0 and sits later in this file, so a bare class here lost to it and the scrollbar ran past the corners. */
.oc-card.oc-inv-view-billing:has(.oc-table-scroll) { overflow: hidden; }
.oc-inv-view-billing > .oc-table-scroll { border-radius: 0 0 var(--radius-lg) var(--radius-lg); }

/* A "no data" dash alone in a table cell sits in the middle of its box, both ways (by request) —
   oc-empty-cell.js tags those cells. !important: column rules (text-right on amounts, etc.) would
   otherwise push the dash to one side. */
td.is-empty-cell { text-align: center !important; vertical-align: middle !important; }

/* THE CLIENT PORTAL WORDMARK — wwwroot/FINAL_LOGO.png (by request): white "CTACER" and the lime ring on a
   SOLID BLACK ground. That black is made to disappear rather than boxed: on a dark surface `lighten`
   keeps only what is brighter than the surface (the ring and the letters); on a light surface the image is
   inverted (dark letters; hue-rotate keeps the ring green) and `multiply` drops its now-white ground. */
.oc-brand-logo, .oc-auth-mini-wordmark { display: block; width: auto; max-width: 100%;
    filter: invert(1) hue-rotate(180deg); mix-blend-mode: multiply; }
.dark .oc-brand-logo, .dark .oc-auth-mini-wordmark { filter: none; mix-blend-mode: lighten; }
.oc-brand-logo { height: 2.1rem; margin-left: -.45rem; }   /* a touch larger now that "Client Portal" no longer sits under it (by request) */
.oc-auth-mini-wordmark { height: 3.4rem; margin-left: -.5rem; }   /* larger so it reads over the background video (by request) */
/* The sign-in brand panel is always dark. */
.oc-auth-wordmark { display: block; height: 3.2rem; width: auto; margin-left: -.65rem; mix-blend-mode: lighten; position: relative; top: -1rem; }   /* larger, "Client Portal" removed (by request) */
.oc-brand-name-logo { display: flex; flex-direction: column; justify-content: center; gap: .15rem; min-width: 0; }
.oc-brand-name-logo small { padding-left: .05rem; }
/* The Octacer mark (_OctacerRing, an <svg class="oc-ring">) shares its class name with the progress ring
   (div.oc-ring), whose conic-gradient background and 50% radius painted a FILLED circle behind the logo —
   the "filled green circle" beside "Octacer / Client support" on Home (by request). The SVG mark draws
   itself; it never takes the progress ring's disc. */
svg.oc-ring { background: none; border-radius: 0; }

/* The ring badge only stands in when the sidebar is collapsed and the wordmark has no room. */
.oc-brand-badge-collapsed { display: none !important; }
.oc-sidebar.is-collapsed .oc-brand-badge-collapsed { display: grid !important; }

/* Portal Payment receipts: the "Send proof of a payment you've made" line under Upload receipt is hidden
   on a phone (by request). */
@media (max-width: 40rem) { .oc-upload-summary .oc-upload-summary-hint { display: none !important; } }

/* Portal Help & Contact: email, phone and website one per line, aligned on the same left edge; a long
   address wraps inside its own button on a phone instead of running off it. */
.oc-help-contacts { display: flex; flex-direction: column; align-items: flex-start; gap: .5rem; }
.oc-help-contacts .btn { max-width: 100%; justify-content: flex-start; text-align: left; white-space: normal; overflow-wrap: anywhere; }
/* Desktop only (by request): email, phone and website sit in ONE line instead of stacking. The stacked layout
   above stays for tablets and phones, where three buttons side by side would not fit. */
@media (min-width: 64rem) {
    .oc-help-contacts { flex-direction: row; flex-wrap: nowrap; align-items: center; gap: .75rem; }
    .oc-help-contacts .btn { flex: 0 0 auto; white-space: nowrap; }
}

/* Portal pages with nothing on them yet, on a phone (by request): the empty states kept their desktop
   padding and icon size and crowded a 375px screen. Tighter, and long words wrap. */
@media (max-width: 40rem) {
    .oc-brand-octacer .oc-blank { padding: 2rem 1.1rem; }
    .oc-brand-octacer .oc-blank-icon { width: 3.75rem; height: 3.75rem; margin-bottom: 1rem; font-size: 1.4rem; }
    .oc-brand-octacer .oc-blank-text { font-size: .875rem; }
    .oc-brand-octacer [role="status"].py-14 { padding-block: 2.25rem; padding-inline: 1rem; }
    .oc-brand-octacer [role="status"] :is(h3, p) { overflow-wrap: anywhere; }
}

/* Client page tab row: no "→" on the Proposals tab (by request). The arrow is printed by the octa-core
   _TabbedView primitive, which is not edited here — hidden for this one tab row only. */
#client_tabs .oc-tab-link > span[aria-hidden="true"] { display: none; }

/* Agreements list (portal): Add Signature · View Agreement · Download (icon) on ONE line (by request). */
.oc-agreement-actions { flex-wrap: nowrap; }
.oc-agreement-actions .btn { white-space: nowrap; }
.oc-agreement-actions .oc-icon-btn { flex: none; }
@media (max-width: 40rem) {
    .oc-agreement-actions { gap: .4rem; }
    .oc-agreement-actions .btn:not(.oc-icon-btn) { flex: 1 1 auto; min-width: 0; padding-inline: .6rem; font-size: .8rem; justify-content: center; }
    /* "Standalone agreement — not tied to a proposal" on one line beside its icon (by request). */
    /* Icon and text on ONE line: the row is flex-wrap, so the text (one item) dropped under the icon when it did not fit.
       No wrap and a smaller size, so the whole line fits beside its icon. */
    .oc-agreement-standalone { flex-wrap: nowrap; gap: .25rem; font-size: .66rem; white-space: nowrap; min-width: 0; }
    /* The document icon is dropped on a phone (by request): the text is clear without it and gets the full width. */
    .oc-agreement-standalone svg, .oc-agreement-standalone i { display: none; }
}

/* Accept / Request changes / Decline on an open proposal card, each confirmed in its own small panel. */
.oc-proposal-decide { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: 1rem; }
.oc-proposal-decide .oc-btn-decline { color: var(--danger-600, #dc2626); border-color: currentColor; }
.oc-proposal-decide-panel {
    margin-top: .75rem; padding: .85rem 1rem; display: grid; gap: .6rem;
    border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--surface-muted);
}
.oc-proposal-decide-panel .field { margin: 0; }
.oc-proposal-decide-actions { display: flex; justify-content: flex-end; gap: .5rem; flex-wrap: wrap; }
@media (max-width: 40rem) {
    /* One line on a phone: three equal buttons rather than a wrapped stack. */
    .oc-proposal-decide { flex-wrap: nowrap; }
    .oc-proposal-decide .btn { flex: 1 1 0; min-width: 0; justify-content: center; padding-inline: .4rem; font-size: .8rem; white-space: normal; text-align: center; line-height: 1.2; }
    .oc-proposal-decide .btn i { display: none; }
}

/* Quoted Octacer reply on the prospect card — the answer itself, not a link to go and find it. */
.oc-quote {
    margin: 0;
    padding: .5rem .75rem;
    border-left: 3px solid var(--brand-500, var(--ink-400));
    background: var(--surface-muted);
    border-radius: var(--radius-sm, .375rem);
    font-size: .88rem;
    color: var(--ink-700);
    font-style: italic;
}

/* ---------- Funnel row ---------- */
.oc-funnel { display: flex; flex-wrap: wrap; gap: .6rem; }
.oc-funnel-step { flex: 1; min-width: 108px; background: var(--line-2); border: 1px solid var(--line); border-radius: .75rem; padding: .85rem .6rem; text-align: center; }
.oc-funnel-step .n { font-size: 1.35rem; font-weight: 800; color: var(--brand-700); line-height: 1; }
.oc-funnel-step .s { font-size: .72rem; color: var(--ink-500); margin-top: .3rem; }

/* ---------- Grids ---------- */
.oc-card-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 1.1rem; }
.oc-detail-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.25rem; }
.oc-cockpit { display: grid; grid-template-columns: repeat(auto-fill, minmax(330px, 1fr)); gap: 1.15rem; }
.oc-card-wide { grid-column: 1 / -1; }

/* ---------- Definition list / plain list inside cards ---------- */
.oc-dl { display: grid; grid-template-columns: minmax(90px, auto) 1fr; gap: .5rem 1rem; margin: 0; }
.oc-dl dt { color: var(--ink-500); font-size: .82rem; }
.oc-dl dd { margin: 0; color: var(--ink-900); font-size: .875rem; font-weight: 500; }
.oc-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: .55rem; }
.oc-list li { font-size: .875rem; color: var(--ink-700); }
.oc-checklist { list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: 1fr 1fr; gap: .4rem; font-size: .875rem; color: var(--ink-700); }
.oc-niche-stats { display: flex; gap: 1.25rem; font-size: .82rem; color: var(--ink-600); }
.oc-add-row { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
.oc-assist-links { display: flex; flex-wrap: wrap; gap: .45rem; }

.oc-muted { color: var(--ink-400); font-size: .85rem; }
.oc-link { color: var(--brand-700); font-weight: 600; text-decoration: none; }
.oc-link:hover { text-decoration: underline; }
/* .oc-link on a <button> — opening a modal (not navigating) is sometimes the correct control for
   something that otherwise reads exactly like every other "→" link on this page, and .oc-link itself only
   ever styled an <a>, so the button's own native chrome (border, background, padding) still showed
   through underneath. This strips that back to plain inline text, same as any other link. */
.oc-link[type="button"] { appearance: none; background: none; border: 0; padding: 0; cursor: pointer; font: inherit; }
.oc-mono { font-family: var(--font-sans); font-size: .85rem; }
.oc-truncate { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
/* List pagination: count on the left, numbered pages + arrows on the right. */
.oc-pagination { display: flex; align-items: center; justify-content: space-between; gap: .75rem;
  flex-wrap: wrap; padding: .55rem 1rem; border-top: 1px solid var(--line-2); }
.oc-pagination-count { font-size: .78rem; color: var(--ink-400); }
.oc-pagination-pages { display: flex; align-items: center; gap: .15rem; }

/* Rows per page, between the count and the page numbers. margin-left:auto moved here from
   .oc-pagination-pages so this pair is what gets pushed right — with it on the page links, the size
   control was stranded beside the count on the far left. */
.oc-pagination-size {
    display: flex; align-items: center; gap: .45rem;
    margin-left: auto;
}
.oc-pagination-size-label { font-size: .78rem; color: var(--ink-500); white-space: nowrap; }
.oc-pagination-size .field-select {
    width: auto; min-width: 4.5rem;
    font-size: .8125rem; padding-block: .3rem; padding-right: 2rem;
    background-position: right .5rem center;
}
/* Narrow: the count keeps the first line and the two controls share the second, rather than the page
   numbers wrapping one at a time down the side. */
@media (max-width: 48rem) {
    .oc-pagination { flex-wrap: wrap; }
    .oc-pagination-count { flex: 1 0 100%; }
    .oc-pagination-size { margin-left: 0; }
    .oc-pagination-pages { margin-left: auto; }
}
.oc-page-num, .oc-page-arrow {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 1.5rem; height: 1.5rem; padding: 0 .3rem; border-radius: .375rem;
  font-size: .75rem; font-weight: 600; color: var(--ink-600);
  text-decoration: none; border: 0; background: none; cursor: pointer;
  transition: background-color .15s ease, color .15s ease;
}
.oc-page-num:hover, .oc-page-arrow:hover { background: var(--brand-50); color: var(--brand-700); }
.oc-page-num.is-current { background: var(--brand-600); color: var(--primary-foreground); }
.oc-page-num.is-current:hover { background: var(--brand-600); color: var(--primary-foreground); }
.oc-page-arrow.is-disabled { opacity: .35; pointer-events: none; }
.oc-page-gap { padding: 0 .1rem; color: var(--ink-400); font-size: .75rem; }

/* ---------- Standalone tables get a card frame (theme table is bare, meant for inside a card) ---------- */
/* No overflow:hidden — it rounded the corners by amputating every column past the viewport, the exact
   failure portal-ux §96 names. Scrolling is the theme's .oc-table-scroll wrapper's job; a <table>
   cannot be its own scroll container. */
.oc-table { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); }
.oc-card .oc-table { background: transparent; border: none; border-radius: 0; box-shadow: none; }

/* ---------- Toolbar / forms ---------- */
.oc-toolbar { display: flex; gap: .6rem; align-items: center; margin-bottom: 1.25rem; flex-wrap: wrap; }
.oc-toolbar .field-group { flex: 1 1 16rem; max-width: 20rem; }

/* ── FILTER BAR ───────────────────────────────────────────────────────────────────────────────────────
   .oc-toolbar is a flex row, which is right for a list with a search box and one dropdown and wrong the
   moment there are six: controls of different natural widths wrap wherever they happen to run out of
   room, so nothing lines up column to column, a date range stretches across a whole row on its own, and
   there is no way to tell which control is which without reading every placeholder.

   .oc-filterbar is the answer for the grids that genuinely filter on many columns. A GRID, not a flex
   row: every control gets the same track width, so they align down the page however many there are and
   however the bar wraps. Each carries a real <label> above it, which is what makes the bar scannable —
   "All statuses" tells you what the control does only if you already knew.

   The search box keeps a row of its own at the top: it is the one control people reach for first, and it
   is the only one whose useful width is "as much as you have". */
.oc-filterbar {
    display: flex; flex-direction: column; gap: .75rem;
    padding: .9rem 1rem; margin-bottom: 1.25rem;
    background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
}
/* Same tracks as .oc-filterbar-grid below, spanning two like the date range — by request, the search is
   exactly as wide as the date field under it, instead of the whole row. */
.oc-filterbar-top {
    display: grid; gap: .6rem .7rem; align-items: center;
    /* auto-fill, not auto-fit: with one control in the row, auto-fit collapses the empty tracks and the
       search stretches back to full width. The grid below is full of controls, so its tracks match. */
    grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
}
.oc-filterbar-top .field-group { grid-column: span 2; min-width: 0; }
@media (max-width: 40rem) { .oc-filterbar-top .field-group { grid-column: span 1; } }

/* ONE BARE ROW, for a bar with only two or three controls.
   The grid above is right when there are six or seven — it wraps them into equal tracks that line up,
   and the labels above each are what make that many controls readable. With three, all of that became
   the problem: a panel, a row of uppercase captions and two dropdowns stretched to half the page, for a
   list people mostly filter by clicking the status cards above it.
   So this variant drops the card entirely and sits the controls straight on the page, each sized to its
   own content. The captions stay in the markup for screen readers and are hidden visually — a <select>
   whose first option reads "All statuses" already says what it does to anyone who can see it. */
.oc-filterbar-inline {
    display: flex; flex-direction: row; align-items: center; flex-wrap: wrap; gap: .5rem;
    background: none; border: 0; padding: 0;
}
/* No intermediate row wrapper any more: the FORM is the row. That extra layer was its own flex
   container sized to fit beside the footer, so it ran out of width early and pushed the last dropdown
   onto a second line while half the bar sat empty. One flex context, one line. */
.oc-filterbar-row { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }

/* Visually hidden, still announced. Not display:none, which would take the label out of the
   accessibility tree along with the select's name. */
.oc-filterbar-inline .oc-filterfield-label {
    position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* Sized to what it holds, not to the space available: a client name needs about this much, and a search
   box that eats the whole row reads as the page's main control. */
.oc-filterbar-inline .field-group { flex: 0 1 16rem; min-width: 10rem; }
.oc-filterbar-inline .field-group > input { padding-block: .45rem; font-size: .8125rem; }
.oc-filterbar-inline .oc-filterfield { flex: 0 0 auto; }
.oc-filterbar-inline .oc-filterfield .field-select {
    width: auto; min-width: 9rem; max-width: 13rem;
    font-size: .8125rem; padding-block: .45rem; padding-right: 2rem;
    background-position: right .55rem center;
}

/* Clear sits IN the row here rather than on a line of its own — with three controls there is room, and a
   footer under a bar with no panel around it would be a stray line of text. The match count goes with it:
   the pager under the table already says "Showing 1-15 of 40", and saying it twice on one screen invites
   the reader to check whether the two agree. */
.oc-filterbar-inline .oc-filterbar-foot { flex: 0 0 auto; padding-top: 0; }
.oc-filterbar-inline .oc-filterbar-count { display: none; }

/* auto-fit + minmax: as many controls per row as fit, each the same width, and the last row's controls
   keep that width rather than stretching to fill — which is what made the old bar look improvised. */
.oc-filterbar-grid {
    display: grid; gap: .6rem .7rem;
    grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
    align-items: end;
}
.oc-filterfield { display: flex; flex-direction: column; gap: .25rem; min-width: 0; }
.oc-filterfield > .oc-filterfield-label {
    font-size: .7rem; font-weight: 600; letter-spacing: .02em; text-transform: uppercase;
    color: var(--ink-500);
}
/* Full width of its own grid track, so every control lines up on both edges. */
.oc-filterbar .oc-filterfield .field-select,
.oc-filterbar .oc-filterfield .field-input { width: 100%; min-width: 0; font-size: .8125rem; padding-block: .4rem; }
.oc-filterbar .oc-filterfield .field-select { padding-right: 2rem; background-position: right .55rem center; }

/* A date RANGE is one control made of two fields. It spans two tracks so each date keeps a usable width,
   and falls back to one track — stacking its two inputs — when even that does not fit. */
.oc-filterfield-range { grid-column: span 2; }
.oc-filterfield-range .oc-filterfield-pair { display: flex; align-items: center; gap: .4rem; min-width: 0; }
.oc-filterfield-range .oc-filterfield-pair > input { flex: 1 1 0; min-width: 0; }
.oc-filterfield-range .oc-filterfield-sep { color: var(--ink-400); font-size: .75rem; flex: 0 0 auto; }
@media (max-width: 40rem) {
    .oc-filterfield-range { grid-column: span 1; }
    .oc-filterfield-range .oc-filterfield-pair { flex-wrap: wrap; }
}

/* Proposals list (by request): the search box and every filter on ONE line on desktop — narrower controls,
   the captions above each hidden visually (still announced; each select's first option says what it
   filters). The two wrapper rows dissolve (display: contents) so every control is one item of one flex
   row. Phones keep the stacked grid above. */
@media (min-width: 60.0625rem) {
    .oc-filterbar-oneline { flex-direction: row; flex-wrap: nowrap; align-items: center; gap: .5rem; padding: .6rem .75rem; }
    .oc-filterbar-oneline .oc-filterbar-top,
    .oc-filterbar-oneline .oc-filterbar-grid { display: contents; }
    .oc-filterbar-oneline .oc-filterbar-top .field-group { flex: 1.5 1 0; min-width: 10rem; }
    .oc-filterbar-oneline .field-group > input { padding-block: .4rem; font-size: .8125rem; }
    .oc-filterbar-oneline .oc-filterfield { flex: 1 1 0; min-width: 0; }
    .oc-filterbar-oneline .oc-filterfield-label {
        position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
        overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
    }
    .oc-filterbar-oneline .oc-filterbar-foot { flex: 0 0 auto; padding-top: 0; }
    .oc-filterbar-oneline .oc-filterbar-count { display: none; }
}

/* Only present while something is actually filtering, so the bar has no permanently dead row. */
.oc-filterbar-foot {
    display: flex; align-items: center; justify-content: space-between; gap: .6rem;
    padding-top: .1rem;
}
.oc-filterbar-count { font-size: .78rem; color: var(--ink-500); }

/* The theme gives every field width:100%, which is right in a form column and wrong in a toolbar: the
   select claimed the whole line and pushed the next control onto its own row, so a search plus one
   dropdown read as three stacked bars. Sized to its content here, with a floor so the label is never
   clipped. Direct children only — a field wrapped in its own container (.oc-search on Receipts) is
   sized by that container and must keep filling it.
   A filter is chrome above the data, not a form to fill in, so the controls also sit a size below the
   form fields they borrow their class from. */
.oc-toolbar > .field-select,
.oc-toolbar > .field-input { width: auto; flex: 0 0 auto; min-width: 9rem; }
/* The search box's height comes from the input INSIDE .field-group (the group itself has no vertical
   padding), so shrinking the group would leave it taller than the select beside it. */
.oc-toolbar > .field-select,
.oc-toolbar > .field-input,
.oc-toolbar .field-group > input { font-size: .8125rem; padding-block: .4rem; }
.oc-toolbar > .field-select { padding-right: 2rem; background-position: right .55rem center; }

/* Admin Receipts (by request): the search box and the invoice-status filter the same width AND height —
   they were sized by two unrelated rules, and the search input carried taller padding. On a phone both sit
   on one line, sharing it equally, with the spacing tightened. */
.oc-receipts-toolbar > .oc-search,
.oc-receipts-toolbar > .field-select { flex: 0 0 16rem; width: 16rem; max-width: 16rem; min-width: 0; }
.oc-receipts-toolbar .oc-search-input,
.oc-receipts-toolbar > .field-select { height: 2.375rem; padding-block: 0; font-size: .8125rem; line-height: 1.2; }
/* The select text sat at the top of the box; centre it vertically (base-select picker renders it as flex). */
.oc-receipts-toolbar > .field-select { display: flex; align-items: center; }
@media (max-width: 40rem) {
    /* .oc-toolbar.… — the generic phone rule further down stacks every toolbar into a column. */
    .oc-toolbar.oc-receipts-toolbar { flex-direction: row; align-items: center; flex-wrap: nowrap; gap: .5rem; margin-bottom: .75rem; }
    .oc-receipts-toolbar > .oc-search,
    .oc-receipts-toolbar > .field-select { flex: 1 1 0; width: auto; max-width: none; }
}

/* ---------- Receipt upload form ----------
   The class was on the markup with nothing behind it, so the fields had no rhythm at all. One column, in
   reading order: a two-column version was tried and put a half-width file input beside a reference box,
   which is worse on every width. */
.oc-form-inline-upload { display: flex; flex-direction: column; gap: 1rem; }
.oc-form-inline-upload > .field { margin: 0; }
.oc-form-inline-upload .oc-form-actions { display: flex; justify-content: flex-end; }

@media (max-width: 640px) {
    /* A full-width primary action is easier to hit than one tucked into a corner (portal-ux §167). */
    .oc-form-inline-upload .oc-form-actions .btn { width: 100%; justify-content: center; }
}

/* ---------- Filter toolbar laid out as a grid ----------
   The flex version gave each select its own full-width row: .field-select is width:100%, so it claims the
   line it lands on and the next control wraps under it. A grid sizes the columns instead of letting the
   controls do it: every column is auto so each control (search, the one status filter, the button) takes
   only the room its own content needs and they sit right beside each other, not spread across the row.
   One column on a phone, where side-by-side controls would be unreadable. */
.oc-filter-grid { display: grid; grid-template-columns: 1fr; gap: .6rem; align-items: center;
                  align-content: start; }
.oc-filter-grid > * { min-width: 0; }
.oc-filter-grid .field-select { width: 100%; }
/* Match the inputs it sits beside. A field is .625rem padding + .875rem text + 1px border each side;
   the button copies that so the row reads as one row rather than a control that shrank. */
.oc-filter-grid .btn {
    justify-self: stretch; width: 100%;
    font-size: .875rem; padding: .625rem 1rem; border-radius: .625rem;
}

@media (min-width: 640px) {
    .oc-filter-grid { grid-template-columns: auto auto; justify-content: start; }
}
/* Search sized itself from .oc-search's own flex-basis/max-width, the select from its min-width — two
   unrelated rules, so the two controls in the same row never landed on the same width. Pinned to a shared
   width here, in the one place every portal list's filter row is built (§139: one rule, everywhere). */
.oc-toolbar.oc-filter-grid > .oc-search,
.oc-toolbar.oc-filter-grid > .field-select { flex: 0 0 16rem; width: 16rem; max-width: 16rem; min-width: 0; }
@media (max-width: 639px) {
    .oc-toolbar.oc-filter-grid > .oc-search,
    .oc-toolbar.oc-filter-grid > .field-select { width: 100%; max-width: none; }
}

/* A page action that belongs beside its list's own filters (e.g. Invoices' Upload receipt) rather than up
   in the page head: the filter form on the left, the action pinned to the row's right edge — with or
   without the form actually rendering (an empty list still needs the action). */
.oc-toolbar-row { display: flex; flex-wrap: wrap; align-items: center; gap: .75rem; margin-bottom: 1.25rem; }
.oc-toolbar-row > .oc-filter-grid { margin: 0; }
.oc-toolbar-row > .oc-toolbar-cta { margin-left: auto; }
@media (max-width: 639px) {
    .oc-toolbar-row > .oc-toolbar-cta { margin-left: 0; width: 100%; justify-content: center; }
}
.oc-form { padding: 1.5rem; }
/* A data-entry form (client create/edit) reads better capped and centred than stretched edge-to-edge
   across a wide screen — unlike the Proposal builder's .oc-form, which genuinely needs the full width. */
/* Capped width, LEFT-aligned — not centred. .oc-page-head above every one of these forms is full width and
   starts at the content area's left edge; margin-inline:auto centred the card instead, so on any screen
   wider than 56rem the card's left edge sat to the right of the heading above it. Every caller is a
   single-column page with nothing beside the form to balance against, so there is no layout this centring
   was serving — removing it is what makes the card start exactly under "New contract". */
.oc-form-narrow { max-width: 56rem; }

/* Contracts' create/edit pages, specifically: wraps BOTH the page heading and the card in one centred
   56rem column, so they move together as a unit — the card no longer sits narrower than a full-width
   heading above it, hugging the left edge with a wide dead strip of blank .oc-main to the right. The
   card inside carries no width rule of its own; the wrapper is what caps and centres it. Scoped to these
   two pages only — the other .oc-form-narrow callers (Clients, Projects, Documents) were not reported and
   keep their existing left-aligned-under-the-heading layout, per the note above. */
.oc-form-narrow-wrap { max-width: 56rem; margin-inline: auto; }
/* Every other create/edit page pairs .oc-form with .oc-card, so the 1.5rem padding above is a bordered
   card's own body inset. New Proposal's <form> carries .oc-form alone — for its .field/.oc-form-actions
   spacing rules, not to BE a card — so that padding had nothing bordering it and just pushed the title
   card, template grid and buttons in from the page edge on all four sides, out of line with the heading
   above and with every other page's flush content. Cancels it back to flush. */
.oc-form-flush { padding: 0; }
.oc-required { color: var(--danger); margin-left: .15rem; }
/* A date only needs "mm/dd/yyyy" and the icon; sized to the calendar popup it opens, not the whole column. */
.field-input.oc-date-compact,
.field-input.oc-date-compact ~ .field-hint { max-width: 14.5rem; }
/* The whole date field opens the picker (app.js showPicker), so the whole field reads as clickable. */
input[type="date"]:not(:disabled):not([readonly]), input[type="datetime-local"]:not(:disabled):not([readonly]),
input[type="month"]:not(:disabled):not([readonly]), input[type="time"]:not(:disabled):not([readonly]),
input[type="date"]::-webkit-calendar-picker-indicator, input[type="datetime-local"]::-webkit-calendar-picker-indicator,
input[type="month"]::-webkit-calendar-picker-indicator, input[type="time"]::-webkit-calendar-picker-indicator { cursor: pointer; }

/* ---------- A field whose value is decided elsewhere ----------
   Shaped like an input so the form's rhythm holds, but plainly not one: a dashed edge, a muted ground,
   and a note naming what set it. A disabled <input> would look almost identical and still say the wrong
   thing — "you could edit this if something changed" — where this value is a consequence of an
   agreement the client has already accepted. */
.oc-locked-field {
    display: flex; align-items: center; justify-content: space-between; gap: .75rem; flex-wrap: wrap;
    padding: .625rem .75rem; border-radius: .625rem;
    background: var(--surface-muted); border: 1px dashed var(--line-strong);
}
.oc-locked-value { font-size: .875rem; font-weight: 700; color: var(--ink-900); }
.oc-locked-note {
    display: inline-flex; align-items: center; gap: .3rem;
    font-size: .72rem; font-weight: 600; color: var(--ink-500);
}
.oc-form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem 1.25rem; margin-bottom: 1rem; }
.oc-form-grid-full { grid-column: 1 / -1; }
/* Proposal Builder's Recipient & terms, by request: a contact's "name (email) ★" wrapped onto two lines in
   a third of the row while Currency and Valid until had room to spare. Desktop: Contact gets half, the
   other two a quarter each. Narrower screens keep the ordinary auto-fit grid. */
@media (min-width: 60.0625rem) {
    .oc-form-grid.oc-terms-grid { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr) minmax(0, 1fr); }
}
/* A fieldset used only to disable a group of inputs (an accepted proposal's locked terms) must not
   also draw a box — the card around it is already the visual grouping. */
.oc-fieldset-bare { border: 0; margin: 0; padding: 0; min-width: 0; }

/* Repeating lists in the section editor, and the control that extends them. Quiet by design: adding a row
   is an ordinary act done many times, not a decision, so it reads as a link under the rows rather than a
   button competing with Save section. */
.oc-repeat { display: flex; flex-direction: column; gap: .5rem; }
.oc-repeat-add { margin: .5rem 0 .25rem; align-self: flex-start; padding-left: .35rem; }

/* A confirmation that is not available yet. Dimmed, and the cursor says so on the way in — but the text
   stays readable, because the label is what explains WHY it is locked and a greyed-out sentence nobody
   can read is just a mystery. Only the control itself is disabled. */
.oc-check-inline.is-locked { cursor: not-allowed; }
.oc-check-inline.is-locked > input { cursor: not-allowed; opacity: .55; }
.oc-check-inline.is-locked > span > strong:first-child { color: var(--ink-500); }

/* The "what this section is for" note at the top of a section editor. Quiet and set apart: it is read
   once, before the fields, and must not compete with them afterwards. Left rule rather than a filled
   callout — an alert-coloured box here would read as a warning on every section, every time. */
.oc-section-guide {
    margin: 0 0 1.25rem; padding: .1rem 0 .1rem .85rem;
    border-left: 2px solid var(--line-2);
    font-size: .82rem; line-height: 1.55; color: var(--ink-600); max-width: 68ch;
}

/* .oc-confirm-review styled the per-section "I confirm that I have reviewed..." checkbox and the copy of
   it inside the leave-section modal. Both are gone — see SectionProgress for why — and so is this. */
.oc-fieldset-bare[disabled] { opacity: .72; }

/* ── Phone control ───────────────────────────────────────────────────────────────────────────────────
   A dialling-code picker and the national number inside ONE .field-group, so they read as a single field
   with one border and one focus ring. Two separately bordered boxes would look like two questions, and a
   phone number is one answer. The select sheds its own chrome exactly as .field-group > input already
   does — .field-select is deliberately NOT used here, since its border and chevron would draw a second
   control inside the first. */
/* 25% of the field: enough for "+971" and no more. The list needs the country spelled out to be
   searchable, but the CLOSED control only has to say which code is in force — phone-code.js trims the
   trigger to the code alone, since "+20 Egypt" in this width read as "+2…". Centered (50/50) gave the
   code as much room as the number, which is the longer of the two answers. */
.oc-phone-code {
    flex: 0 0 25%; max-width: 25%; min-width: 4.75rem;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    border: 0; background: transparent; outline: none;
    padding: .625rem .25rem .625rem 0;
    font-size: .875rem; color: var(--ink-700); cursor: pointer;
}
/* A hairline between the two halves, so it is obvious which part the caret is in. */
.oc-phone-code + input { border-left: 1px solid var(--line); padding-left: .65rem; }
/* The whole group turns when the number inside it is invalid — .field-group already has .is-error for
   this, and oc-live-validate.js sets it, since the input has no border of its own to colour. */
.field-group.is-error { border-color: var(--danger); }

/* ── The dial-code picker, once octacer-searchable.js has enhanced it ────────────────────────────────
   That script (core-owned, not edited here) wraps the <select> in .oc-searchable and builds a full-width
   trigger styled as a .field-input — correct for a select standing on its own, wrong for one that is half
   of a combined field. These rules re-seat it as a segment INSIDE .field-group: fixed width, no border of
   its own, and the group's single border doing the work for both halves. */
/* min-width (by request): in a narrow box — the proposal page's Create Client dialog — the code segment
   used to shrink until "Code" and the chevron were cut off. Same on every phone field. */
.field-group > .oc-searchable { flex: 0 0 25%; width: 25%; min-width: 5.5rem; }
.field-group > .oc-searchable + input { min-width: 6rem; }
/* Placeholders that can actually be read — "Code" and "03001234567" were near-invisible grey. */
.field-group > .oc-searchable .oc-searchable-trigger .text-slate-400 { color: var(--ink-500); }
.field-group > .oc-searchable + input::placeholder { color: var(--ink-400); opacity: 1; }
.field-group > .oc-searchable .oc-searchable-trigger {
    border: 0; background: transparent; box-shadow: none; outline: none;
    min-height: 0; padding: .625rem .4rem .625rem 0; font-size: .875rem;
}
.field-group > .oc-searchable + input { border-left: 1px solid var(--line); padding-left: .65rem; }
/* The menu used to be positioned against the 7.5rem trigger and allowed to grow rightwards over the
   number beside it — width followed nothing in particular, so it never lined up with either edge of the
   field it belongs to. .oc-searchable's own "relative" (octacer-searchable.js) is what the menu's
   left/right anchor against; switching it to static hands that job to .field-group instead, so left:0/
   right:0 span the WHOLE combined phone field — code segment and number segment together, same width the
   field itself has, not an arbitrary one. */
.field-group:has(> .oc-searchable) { position: relative; }
.field-group > .oc-searchable { position: static; }
.field-group > .oc-searchable .oc-searchable-menu { left: 0; right: 0; min-width: 0; }

/* A searchable dropdown on a phone (a bottom sheet, OctacerSheetDock) could not be scrolled by touch (by request:
   "the phone code dropdown is not working on mobile"): core switches the list's own scroll off inside the sheet
   so the sheet itself scrolls, but the menu's Tailwind `overflow-hidden` beats the theme's `.is-sheet` overflow —
   only the first dozen codes could ever be reached. The sheet scrolls again; the search box stays pinned on top. */
.oc-searchable-menu.is-sheet { overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; }

/* Every searchable dropdown's search box (.oc-searchable-search, octacer-searchable.js — core-owned, not
   edited there): it used to sit flush against the top edge of .oc-searchable-menu (rounded-xl +
   overflow-hidden), sharing a corner with it. Matching the two corners' radii (even accounting for the
   menu's 1px border) still left a hairline mismatch visible at typical OS display scaling — nested
   rounded corners are fragile to get pixel-exact across zoom levels. Simpler and robust: stop sharing an
   edge at all. A small top margin holds the search box clear of the menu's own corner, and full rounding
   on all four sides (not just the top two) makes it read as its own self-contained control floating
   inside the panel, so its focus ring never touches the parent's rounded boundary to begin with. */
.oc-searchable-search { margin: .4rem .4rem 0; width: calc(100% - .8rem); border-radius: .5rem; border: 1px solid var(--input); }

/* A <details> summary styled as a card head: the disclosure IS the control, so it needs a pointer and a
   visible focus ring like any other one. The default marker is removed because the card head already
   reads as something you open. */
.oc-pricing-models { flex-wrap: wrap; gap: .5rem; }

/* Whether a proposal section has been filled in. A tick and a cross rather than colour alone: colour is
   not a state to anyone who cannot see it, and this is the one signal that decides whether Send works. */
/* Completion indicator: green tick = this section is filled in, red cross = something required is
   missing. One consistent circle either way, so a reviewer reads the COLOUR down the column rather than
   comparing two differently-shaped marks.

   Sized in a fixed square with the glyph centred by grid, and a faint ring instead of a border — a
   1px border on a 1.35rem circle renders unevenly at fractional device pixels, whereas an inset shadow
   stays smooth. No margin-left: auto any more; the grid column places it. */
.oc-section-state {
    display: inline-grid; place-items: center;
    width: 1.35rem; height: 1.35rem; border-radius: 50%;
    font-size: .68rem; line-height: 1;
    /* Every property a state swap can touch, animated together — the marker morphs from one status to
       the next instead of snapping, which is the "clear visual feedback" a static colour swap does not
       give you. transform lives here too, for the pop keyframes below. */
    transition: background-color .22s ease, color .22s ease, box-shadow .22s ease, transform .18s ease;
}
/* @Html.Icon's own w-N/h-N utility classes give the <i> an explicit CSS width/height (here 12px, from
   w-3) that is very often NOT the glyph's own advance width (fa-xmark's is 8.25px, checked in DevTools) —
   the parent's place-items:center correctly centers the <i>'s BOX, but that leftover space inside the
   box itself was never split evenly (line-height/text-align only get most of the way there). Making the
   <i> itself a flex container — align-items/justify-content:center — centers its glyph content (the
   ::before Font Awesome draws it as) directly within whatever box the w-N/h-N classes gave it, on both
   axes, at once. No transform/translate nudge, by request: a per-icon hardcoded offset is exactly what
   went stale and broke before (.oc-section-state used to carry a hand-measured .19rem/.15rem correction
   for fa-xmark alone that drifted wrong over time) — plain flex alignment here instead. */
.oc-section-state i { font-size: .7rem; line-height: 1; display: flex; align-items: center; justify-content: center; transition: transform .18s ease; }
.oc-section-state svg { width: .7rem; height: .7rem; transition: transform .18s ease; }
/* fa-check is a thin diagonal stroke that fills noticeably less of its own em-box than the bolder,
   symmetric fa-xmark, so at a shared font-size it reads visibly smaller inside an otherwise identical
   circle. Sized up to match xmark's weight — the shared line-height/text-align above still center it
   correctly at this larger size, no extra rule needed. */
.oc-section-state .fa-check { font-size: .82rem; }

/* One shared "a status just landed" pop — same motion on every stage, so the marker's own colour is
   still what carries the meaning; the animation only says "look, this one just changed." Re-plays
   whenever the class that carries it is (re)applied, which is exactly on a genuine state change, since
   the four stage classes below are mutually exclusive and JS only ever toggles between them. */
@keyframes oc-status-pop {
    0%   { transform: scale(.55); }
    55%  { transform: scale(1.18); }
    100% { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
    .oc-section-state { transition: background-color .22s ease, color .22s ease, box-shadow .22s ease; }
    .oc-section-state.is-done, .oc-section-state.is-missing { animation: none; }
}

/* SOLID fill + white glyph, by request — a light tint at this icon size read as washed out no matter how
   dark the glyph on top of it was mixed. Same four hues everywhere else in the app (--success/--danger/
   --warning/--info) at full strength, with their own -foreground tokens (already used by badges elsewhere)
   for the glyph, instead of another color-mix darkening pass on top of a pastel background. */
.oc-section-state.is-done {
    background: var(--success);
    color: var(--success-foreground, #fff);
    animation: oc-status-pop .32s ease;
}
/* EMPTY IS THE ABSENCE OF THE TICK, NOT A CROSS.
   This was a solid red disc with a white x in it, and the x was read as a delete button — reasonably,
   since it is the universal one, and now literally so: every row in the rail carries a real remove
   control a few pixels to its right. A hollow ring says "nothing here yet" without offering to do
   anything, and the colour still picks the row out of a list of sixteen. The tick element is present in
   the markup either way (Builder.cshtml) so proposal-autosave.js can colour a section that fills up live
   — from the server's own answer, not its own opinion — without swapping glyphs; it is simply not drawn
   until there is something to tick. */
.oc-section-state.is-missing {
    background: transparent;
    color: transparent;
    box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--danger) 55%, transparent);
    animation: oc-status-pop .32s ease;
}
.oc-section-state.is-missing i,
.oc-section-state.is-missing svg { display: none; }
/* The blue "filled but not confirmed" and amber "confirmed but not saved" stages lived here. Both
   described a per-section confirmation checkbox that no longer exists, so a section is now either
   finished or it is not — see SectionProgress. Two states, two rules. */
/* Incomplete is the one worth noticing, so it gets slightly more presence than the tick — a list where
   every row shouts equally tells you nothing about which row needs you. */
.oc-section-state.is-missing { font-weight: 700; }

/* Interactive feedback on every state, not only the ones needing attention — a row that only reacts to
   hover when it is wrong reads as "hover to find your mistakes", not as a live list. The fill deepens a
   touch and the marker lifts; on an already-solid background a same-hue ring barely reads, so hover
   darkens toward black instead of adding one. */
.oc-section-item:hover .oc-section-state { transform: scale(1.08); }
/* The ring deepens; it never fills, or hover would put the solid red disc back. */
.oc-section-item:hover .oc-section-state.is-missing {
    background: transparent;
    box-shadow: inset 0 0 0 2px var(--danger);
}
.oc-section-item:hover .oc-section-state.is-done {
    background: color-mix(in srgb, var(--success) 88%, black);
}

.oc-summary { cursor: pointer; list-style: none; }
.oc-summary::-webkit-details-marker { display: none; }
.oc-summary:focus-visible { outline: 2px solid var(--brand-500); outline-offset: 2px; }
/* Portal Receipts "Upload receipt" (local to that one disclosure — .oc-card-head/.oc-summary above stay
   shared with Admin's other folds, e.g. Projects "Reopen this project"). A plain card head gave no sign
   it opened a form; the title reads as a tinted chip like the app's other action buttons, and a chevron
   flips as it opens — same drawn-arrow technique as .oc-more-summary below. */
/* Solid, same as .btn-primary (--primary/--primary-foreground — lime with black text in the portal's dark
   theme, by request) rather than the tinted-outline look every other chip on this page uses — this is the
   one action on the page, so it reads as the button it is. */
.oc-upload-summary-chip {
    background: var(--primary); color: var(--primary-foreground);
    padding: .4rem .75rem; border-radius: .5rem; font-weight: 600;
    transition: background .15s ease;
}
.oc-upload-summary:hover .oc-upload-summary-chip { background: var(--primary-active); }
.oc-upload-summary .oc-card-sub { margin-left: auto; }
.oc-upload-summary::after {
    content: ""; width: .5rem; height: .5rem; flex: 0 0 auto; margin-left: .5rem;
    border-right: 2px solid var(--ink-400); border-bottom: 2px solid var(--ink-400);
    transform: rotate(-45deg); transition: transform .15s ease;
}
.oc-upload-card[open] > .oc-upload-summary::after { transform: rotate(45deg); }
.oc-radio-row { display: flex; gap: 1.25rem; flex-wrap: wrap; }
.oc-radio-option { display: inline-flex; align-items: center; gap: .45rem; font-size: .88rem; color: var(--ink-700); cursor: pointer; }
.oc-radio-option input[type="radio"] { width: 1rem; height: 1rem; accent-color: var(--brand-600); }
.oc-form .field { margin-bottom: 1rem; }
.oc-form-actions { display: flex; gap: .6rem; justify-content: flex-end; margin-top: .5rem; padding-top: 1.1rem; border-top: 1px solid var(--line-2); }
/* Same row, no border/margin/padding of its own — used inside the signature modal's draw panel, where a
   lone Clear button belongs with the canvas above it, not visually separated the way a form's real
   actions row is. */
.oc-form-actions-start { display: flex; gap: .6rem; justify-content: flex-start; }

/* Contract signature field/modal (_ContractForm.cshtml, contract-signature.js) */
.oc-signature-preview { display: flex; align-items: center; gap: .6rem; margin-top: .6rem; }
.oc-signature-preview img { max-width: 12rem; max-height: 4rem; border: 1px solid var(--line); border-radius: .375rem; background: #fff; }
.oc-signature-pad {
    width: 100%; max-width: 26.25rem; height: 10rem; border: 1px dashed var(--line-strong, var(--line));
    border-radius: .5rem; background: #fff; cursor: crosshair; touch-action: none; display: block;
}

/* Delete-scope choice in a confirmation dialog. Boxed and full-width so it reads as part of the decision
   rather than a stray control next to the buttons; the label wraps because it explains a consequence.
   Neither .oc-check-inline nor this class ever declared `display: flex` — the checkbox and its two-line
   label were laying out in plain inline flow, which put the checkbox at the text's top-left corner rather
   than beside it. `.oc-check-inline.oc-delete-scope` (two classes) beats `.oc-check-inline` alone (one) on
   specificity, so align-items holds regardless of which rule comes later in the file. */
.oc-delete-scope {
  display: flex; align-items: flex-start; gap: .65rem; white-space: normal;
  background: var(--line-2); border: 1px solid var(--line);
  border-radius: var(--radius); padding: .75rem .9rem; font-size: .8rem;
}
/* .oc-check-inline sets `white-space: nowrap` for its usual single-line case, and — same trap as
   align-items above — wins on source order alone since both rules share one-class specificity. That left
   this label's text forced onto one line no matter how narrow the box, spilling past the edge as ink
   overflow instead of wrapping (invisible to a bounding-box overflow check, since a block element's own
   rect doesn't grow to reflect nowrap text spilling out of it — only actually visible on screen). */
.oc-check-inline.oc-delete-scope { align-items: flex-start; white-space: normal; }

/* The dynamic per-check-state explanation (proposal-checkbox-tooltip.js) now lives on its OWN small "i"
   icon beside the checkbox's label text, not on the checkbox or the label row — hovering the checkbox or
   its text shows nothing any more; only this icon does. A <span>, not a button: it does nothing on click,
   only on hover/focus, so it carries tabindex itself instead of the surrounding label's real control.
   Positioning/wrapping of its tooltip bubble is oc-admin-tooltip.js's job now, not this class's. */
.oc-check-info {
    display: inline-flex; align-items: center; justify-content: center;
    width: 1rem; height: 1rem; color: var(--ink-400); cursor: pointer;
}
.oc-check-info svg { width: 100%; height: 100%; }
.oc-check-info:hover, .oc-check-info:focus-visible { color: var(--ink-600); }

/* ---------- Admin tooltips (oc-admin-tooltip.js) ----------
   octacer-theme.css's own [data-oc-tooltip]::after is a plain CSS tooltip: centred on the trigger,
   absolutely positioned inside whatever ancestor happens to be its containing block. That is exactly
   why it kept getting clipped or run off-screen — an ancestor with overflow-x:auto (any scrollable
   table) clips it the moment it needs to open past that ancestor's own edge, and near a viewport edge
   "centred on the trigger" is sometimes half off-screen with nothing to pull it back in. Both are
   layout facts a static CSS rule cannot see; only something that reads the trigger's actual position
   and the viewport's actual size can. Three page-specific ::after overrides had accumulated here for
   exactly that reason (a checkbox row's icon, a table's row-actions column, a "this control sits at
   the page's right edge" opt-in class) — patches for individual symptoms of the same one cause.
   oc-admin-tooltip.js replaces all of them: ONE bubble, appended once to <body> so no ancestor's
   overflow can ever clip it, positioned in JS from the trigger's real getBoundingClientRect() and
   clamped to the viewport on every axis, flipping below the trigger when there is no room above.
   [data-oc-tooltip] itself is untouched — same attribute, same content, same triggers — only how the
   bubble it produces is rendered and placed. Scoped to the Admin shell only (data-theme-scope="admin",
   set by _LayoutAdmin.cshtml): Portal carries no [data-oc-tooltip] usage today, and this was asked for
   on the Admin side specifically. */
html[data-theme-scope="admin"] [data-oc-tooltip]::after { display: none; }
/* The Portal uses the same bubble now (by request: icon-only buttons on phones must explain themselves). On a touch
   screen the tip opens on press-and-hold, so the system's own long-press callout must not cover it. */
html[data-theme-scope="portal"] [data-oc-tooltip]::after { display: none; }
:is(button, a):is([data-oc-tooltip], [title], [aria-label]) { -webkit-touch-callout: none; }
/* The bubble, restyled to the theme by request: the ink-900 surface the theme uses for dark UI, Montserrat
   at a readable 12px, the theme's spacing and radius, a soft shadow, and a small pointer at the trigger
   (--oc-tip-arrow / data-side, set by the script). It fades in with a 2px drift toward its trigger. */
.oc-admin-tooltip {
    position: fixed; z-index: 9999; top: 0; left: 0;
    max-width: min(18rem, calc(100vw - 1rem)); width: max-content;
    padding: .45rem .7rem; border-radius: .5rem;
    /* Literal colours, not var(--ink-900): that token flips to near-white under .dark, which left white
       text on a white bubble — the tooltip looked empty in the dark theme. */
    background: #0f172a; color: #f8fafc;
    font: 500 .75rem/1.45 var(--font-sans); letter-spacing: .005em; text-align: left;
    white-space: normal; word-wrap: break-word;
    box-shadow: 0 8px 24px rgba(15, 23, 42, .22), 0 0 0 1px rgba(255, 255, 255, .06) inset;
    opacity: 0; visibility: hidden; pointer-events: none; transform: translateY(2px);
    transition: opacity .14s var(--ease-out, ease), transform .14s var(--ease-out, ease), visibility 0s linear .14s;
}
.oc-admin-tooltip[data-side="bottom"] { transform: translateY(-2px); }
.oc-admin-tooltip.is-visible {
    opacity: 1; visibility: visible; transform: none;
    transition: opacity .14s var(--ease-out, ease), transform .14s var(--ease-out, ease), visibility 0s;
}
.oc-admin-tooltip::after {
    content: ""; position: absolute; left: var(--oc-tip-arrow, 50%); width: .55rem; height: .55rem;
    background: inherit; border-radius: 1px; transform: translateX(-50%) rotate(45deg);
    bottom: -.25rem;
}
.oc-admin-tooltip[data-side="bottom"]::after { bottom: auto; top: -.25rem; }
/* Dark theme: a navy bubble on a navy page needs to stand off it — one step lighter, with a visible edge. */
.dark .oc-admin-tooltip {
    background: #1e293b; color: #f8fafc;
    box-shadow: 0 8px 24px rgba(0, 0, 0, .45), 0 0 0 1px rgba(148, 163, 184, .28);
}
@media (prefers-reduced-motion: reduce) { .oc-admin-tooltip, .oc-admin-tooltip.is-visible { transition: none; transform: none; } }
.oc-delete-scope input[type="checkbox"] { margin-top: .15rem; flex: none; }
/* The text wrapper is a flex item too: without flex-basis + min-width:0 a flex item defaults to
   min-width:auto, meaning it never shrinks below its own intrinsic content width — the description line
   just kept growing wider instead of wrapping, running the text off the edge of the box instead of onto a
   second line inside it. */
.oc-delete-scope > span { flex: 1 1 auto; min-width: 0; }
.oc-delete-scope strong { display: block; color: var(--ink-900); font-size: .85rem; }
.oc-delete-scope .oc-muted { display: block; margin-top: .2rem; line-height: 1.5; }
/* a card head placed at the top of a padded form should still bleed to the card edges */
.oc-form > .oc-card-head:first-child { margin: -1.5rem -1.5rem 1.25rem; }
.oc-inline-form { display: inline-flex; gap: .4rem; align-items: center; margin: 0; }
.field-input-sm { padding: .4rem .55rem; font-size: .82rem; }

/* ---------- Chips (filter / sub-nav) ---------- */
.oc-chips { display: flex; flex-wrap: wrap; gap: .4rem; margin-bottom: 1.35rem; }
.oc-chip { padding: .35rem .75rem; border-radius: 999px; font-size: .82rem; font-weight: 500; background: var(--surface); border: 1px solid var(--line); color: var(--ink-600); text-decoration: none; transition: background .15s, color .15s, border-color .15s; }
.oc-chip:hover { background: var(--line-2); color: var(--ink-900); }
.oc-chip-active { background: var(--brand-600); border-color: var(--brand-600); color: var(--primary-foreground); }

/* ---------- Landing (guest) ---------- */
.oc-land { max-width: 720px; margin: 4.5rem auto; text-align: center; }
.oc-land-badge { display: inline-flex; align-items: center; gap: .4rem; background: var(--brand-50); color: var(--brand-700); font-weight: 600; font-size: .8rem; padding: .35rem .8rem; border-radius: 999px; }
.oc-land-badge i { width: 15px; height: 15px; }
.oc-land-title { font-size: 2.7rem; font-weight: 800; letter-spacing: -.03em; color: var(--ink-900); margin: 1.2rem 0 .7rem; line-height: 1.08; }
.oc-land-sub { color: var(--ink-600); font-size: 1.05rem; line-height: 1.65; margin: 0 auto 1.9rem; max-width: 34rem; }
.oc-land-foot { color: var(--ink-400); font-size: .85rem; margin-top: 1.1rem; }

/* ---------- Centered card (access-denied / error) ---------- */
/* The access-denied page's own way out for the signed-in-but-refused account it names — this bare shell
   (_Layout.cshtml) carries no topbar to hang a sign-out control off, and one belongs here only, not on
   every page that shell serves (e.g. the generic error page). Fixed, not static: the card below it is
   vertically centered with margin auto, so a static placement would push the whole page down instead of
   sitting in the corner. */
.oc-corner-signout { position: fixed; top: 1.5rem; right: 1.5rem; margin: 0; }
.oc-center-card { max-width: 460px; margin: 5rem auto; text-align: center; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-lg); box-shadow: var(--shadow-md); padding: 2.5rem 2rem; }
.oc-center-card .oc-icon-box { margin: 0 auto 1rem; }
.oc-center-card .oc-page-title { font-size: 1.25rem; }
.oc-center-card .oc-page-sub { margin: .5rem auto 0; }
.oc-center-actions { display: flex; gap: .6rem; justify-content: center; margin-top: 1.5rem; }
/* A table row's action buttons. Centering (.oc-center-actions, for a standalone card) shifts the whole
   group sideways once one row has fewer buttons than another — every row must start at the same left
   edge regardless of how many actions it has, in the same left-to-right order every time. */
.oc-row-actions { display: flex; align-items: center; gap: .5rem; flex-wrap: nowrap; justify-content: flex-start; }

/* …but inside an actions CELL the group must hug the right edge, because that is where the column's own
   `ACTION` header sits. A flex container ignores the `text-right` on its parent <td>, so without this the
   buttons stack at the left of the column while the header label stays at the far right — a gap that grows
   with the column's width (.oc-table-fixed gives it 34%, so it grows a lot). Every markup use of
   .oc-row-actions inside a <td> is an .oc-col-actions cell; the one standalone use (a contract's detail
   toolbar) is not, and correctly keeps starting at the left. */
.oc-col-actions .oc-row-actions { justify-content: flex-end; }

/* …EXCEPT on the dense admin grids, where the header and the buttons line up on the LEFT instead.
   The column is shrink-to-fit (width: 1%), so its width is set by whichever is wider — and "ACTIONS" is
   wider than three icon buttons. Right-aligning the buttons under a header that starts at the left of
   that same cell left the two visibly out of line, the icons sitting in from the word above them.
   Agreeing on one edge is what fixes it, and left is the edge the rest of the row is read from.
   Vertical alignment comes from the dense table's own `vertical-align: middle`. */
.oc-table-dense th.oc-col-actions,
.oc-table-dense td.oc-col-actions { text-align: left; }
.oc-table-dense .oc-col-actions .oc-row-actions { justify-content: flex-start; }

/* Vertically the theme's own direction is kept — the bubble opens UPWARD, and the table header leaves
   room above the first row. (There is no fixing the vertical axis with overflow anyway: once overflow-x
   is auto, a `visible` overflow-y computes to auto, so the two axes clip together whatever you write.) */
/* Classic "shrink to fit" table trick: width:1% forces auto-layout to size the column to its content's
   natural (single-line) width instead of splitting leftover space with it — without this, an Actions
   column with only a couple of short buttons still gets handed a wide, arbitrary share of the row,
   which is exactly what starves it back down until the buttons wrap onto their own line. */
/* ---------- Table footers ----------
   The theme styles thead th and tbody td and stops there, so a tfoot cell inherits NO padding: the total
   sat flush against the card and the rounded corner clipped the last digits off it. Mirrors the body's
   metrics, with a rule above it because a total is a different kind of row from the ones it sums. */
.oc-table tfoot td, .oc-table tfoot th {
    padding: .75rem .875rem;
    border-top: 1px solid var(--line);
    color: var(--ink-900);
    font-weight: 600;
    background: var(--surface);
}
.oc-table tfoot td:first-child, .oc-table tfoot th:first-child { padding-left: 1.25rem; }
.oc-table tfoot td:last-child, .oc-table tfoot th:last-child { padding-right: 1.25rem; }

/* The last row of a card needs the corner's radius as clearance, or its text tucks under the curve. */
.oc-card > .oc-table-scroll:last-child .oc-table tfoot tr:last-child td,
.oc-card > .oc-table-scroll:last-child .oc-table tfoot tr:last-child th { padding-bottom: 1rem; }

.oc-col-actions { width: 1%; white-space: nowrap; }

/* Compound with the cell, not bare .is-danger/.is-warning — those already mean something specific on
   .oc-tile-facts dd and .oc-tile-amount-sub, and a bare rule would leak into every future table cell that
   reuses the name for something else. Same tone/weight as those, for a table cell reading the same fact
   (money overdue, an invoice not yet published to the portal). */
.oc-table td.is-danger  { color: var(--danger);  font-weight: 600; }
.oc-table td.is-warning { color: var(--warning); font-weight: 600; }

/* A column that earns its place on a wide screen and not on a phone. Hidden rather than shrunk: eight
   columns squeezed into 375px is a table nobody can read, and side-scrolling to reach Status is worse
   than not showing Paid. The figures dropped here are all on the invoice's own page. */
@media (max-width: 767px) {
    .oc-table th.oc-col-optional, .oc-table td.oc-col-optional { display: none; }
}

/* ---------- Record list: the table's mobile form ----------
   portal-ux's table/list contract: a table is for comparing columns, a list is for scanning records, and a
   list reflows before forcing horizontal overflow. Both are rendered and CSS picks one, so there is no
   toggle to remember and no script to run — a phone gets cards, a laptop keeps the sortable table. */
.oc-record-list { display: none; list-style: none; margin: 0; padding: 0; }

/* Portal Projects shows cards at every width by request, not just below the mobile breakpoint — scoped
   to its own class so the shared table/list toggle above stays untouched everywhere else that uses it
   (Invoices, Receipts, ...). Same specificity as the rule it overrides (one class each); this one wins by
   coming later in the file. */
.oc-record-list-always { display: block; }

.oc-record {
    padding: 1rem 1.1rem;
    border-bottom: 1px solid var(--line-2);
    display: flex; flex-direction: column; gap: .75rem;
}
.oc-record:last-child { border-bottom: 0; }

.oc-record-head { display: flex; align-items: center; justify-content: space-between; gap: .75rem; }
.oc-record-title { font-weight: 700; color: var(--ink-900); }
/* Combined selector, not source order: a mobile card whose title is a real link (Contracts, Projects)
   still needs to look clickable — .oc-record-title alone would otherwise repaint .oc-link's brand colour
   back to plain ink, the same same-specificity/source-order trap as .oc-check-inline vs .oc-delete-scope. */
.oc-record-title.oc-link { color: var(--brand-700); }

/* Two columns of label-over-value: four facts fit a 320px screen without a figure wrapping mid-number. */
.oc-record-facts { display: grid; grid-template-columns: 1fr 1fr; gap: .7rem 1rem; margin: 0; }
.oc-record-facts dt { font-size: .7rem; text-transform: uppercase; letter-spacing: .04em; color: var(--ink-500); }
.oc-record-facts dd { margin: 0; font-size: .9rem; color: var(--ink-900); }
.oc-record-facts dd.is-due { font-weight: 700; }
/* A fact whose value is a phrase rather than a figure — it needs the row to itself. */
.oc-record-facts .oc-record-wide { grid-column: 1 / -1; }
/* The one-line relationship under a card title ("For proposal …" / "Agreement …"): the proposal an
   agreement belongs to, or the agreement that followed a proposal. Quiet, one row, inline badge — it
   answers "is this the same deal?" without becoming a second title. */
.oc-record-link { display: flex; flex-wrap: wrap; align-items: center; gap: .3rem; margin: .25rem 0 0; font-size: .8rem; color: var(--ink-500); }
.oc-record-link i, .oc-record-link svg { color: var(--ink-400); }
.oc-record-facts dd .badge { vertical-align: middle; }
.oc-ml-xs { margin-left: .4rem; }

.oc-record-foot { display: flex; justify-content: flex-end; }
/* §167: ~44px, measured on the container. */
.oc-record-foot .btn { min-height: 2.75rem; }

/* A block that earns its place on a wide screen and not on a phone. Used for the KPI strips: two tall
   tiles above a list push the records themselves off the first screen, and the same figures are in the
   rows underneath. */
.oc-hide-sm { display: revert; }

/* Opt-in by class. A blanket rule would hide every portal table below 768px, including the pages with no
   card list to fall back to — they would show nothing at all. */
@media (max-width: 767px) {
    .oc-table-swap, .oc-hide-sm { display: none; }
    .oc-record-list { display: block; }
}
/* color-mix off the token rather than a fixed tint: #fef2f2 is a near-white wash that stays near-white on
   a dark card, so the icon lost its contrast the moment the theme flipped (accessibility.md, theme axis). */
.oc-icon-danger  { background: color-mix(in srgb, var(--danger) 12%, transparent);  color: var(--danger); }
.oc-icon-warning { background: color-mix(in srgb, var(--warning) 16%, transparent); color: var(--warning); }
.oc-icon-success { background: color-mix(in srgb, var(--success) 14%, transparent); color: var(--success); }
.oc-icon-muted   { background: var(--line-2); color: var(--ink-600); }
.oc-icon-brand { background: var(--brand-50); color: var(--brand-700); }
.dark .oc-icon-brand { background: color-mix(in srgb, var(--brand-500) 20%, transparent); color: var(--brand-100); }

/* ---------- Kanban ---------- */
.oc-kanban { display: flex; gap: .9rem; overflow-x: auto; padding-bottom: 1rem; align-items: flex-start; }
.oc-kcol { flex: 0 0 268px; background: var(--line-2); border: 1px solid var(--line); border-radius: var(--radius-lg); padding: .7rem; }
.oc-kcol-head { font-size: .8rem; font-weight: 700; color: var(--ink-700); display: flex; align-items: center; justify-content: space-between; padding: .25rem .35rem .6rem; }
.oc-kcol-body { display: flex; flex-direction: column; gap: .55rem; min-height: 60px; }
.oc-kcard { background: var(--surface); border: 1px solid var(--line); border-radius: .7rem; padding: .7rem .8rem; cursor: grab; box-shadow: var(--shadow-sm); transition: box-shadow .15s, transform .1s; }
.oc-kcard:hover { box-shadow: var(--shadow-md); }
.oc-kcard:active { cursor: grabbing; }
.oc-kcard-sub { font-size: .75rem; color: var(--ink-400); margin-top: .4rem; display: flex; gap: .3rem; flex-wrap: wrap; }
.oc-dragging { opacity: .5; }
.oc-col-over { outline: 2px dashed var(--brand-400, #818cf8); outline-offset: 2px; border-radius: var(--radius-lg); }

/* ---------- Get-started tiles (empty dashboard) ---------- */
.oc-start-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: .8rem; }
.oc-start { display: block; padding: 1.1rem; border: 1px solid var(--line); border-radius: var(--radius-lg); background: var(--surface); text-decoration: none; transition: box-shadow .2s, transform .1s, border-color .2s; }
.oc-start:hover { box-shadow: var(--shadow-md); transform: translateY(-2px); border-color: var(--brand-200, #c7d2fe); }
.oc-start-ic { width: 2.4rem; height: 2.4rem; border-radius: .7rem; display: grid; place-items: center; background: var(--brand-50); color: var(--brand-700); margin-bottom: .6rem; }
.oc-start-ic i { width: 20px; height: 20px; }
.oc-start-t { display: block; font-weight: 700; color: var(--ink-900); font-size: .92rem; }
.oc-start-d { display: block; color: var(--ink-500); font-size: .82rem; margin-top: .25rem; line-height: 1.45; }

/* ---------- Guide / help page ---------- */
.oc-obj-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: .9rem; margin-bottom: 1.75rem; }
.oc-obj { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); padding: 1.1rem 1.2rem; }
.oc-obj-name { display: flex; align-items: center; gap: .5rem; font-weight: 700; color: var(--ink-900); font-size: .95rem; }
.oc-obj-name i { width: 17px; height: 17px; color: var(--brand-600); }
.oc-obj-desc { color: var(--ink-500); font-size: .84rem; margin-top: .4rem; line-height: 1.5; }
.oc-obj-arrow { display: flex; align-items: center; color: var(--ink-300, #cbd5e1); }

.oc-steps { display: flex; flex-direction: column; gap: 0; margin: 0 0 1.75rem; }
.oc-step { display: grid; grid-template-columns: 2.4rem 1fr; gap: 1rem; padding-bottom: 1.4rem; position: relative; }
.oc-step:not(:last-child)::before { content: ""; position: absolute; left: 1.2rem; top: 2.4rem; bottom: 0; width: 2px; background: var(--line); }
.oc-step-num { width: 2.4rem; height: 2.4rem; border-radius: 50%; background: linear-gradient(180deg, var(--brand-500), var(--brand-700)); color: var(--primary-foreground); display: grid; place-items: center; font-weight: 800; font-size: .9rem; z-index: 1; box-shadow: 0 2px 6px -1px rgba(79,70,229,.5); }
.oc-step-body { padding-top: .25rem; }
.oc-step-title { font-weight: 700; color: var(--ink-900); font-size: .98rem; }
.oc-step-text { color: var(--ink-600); font-size: .88rem; line-height: 1.55; margin-top: .3rem; }
.oc-step-text code { background: var(--line-2); padding: .05rem .35rem; border-radius: .3rem; font-size: .82em; color: var(--ink-700); }
.oc-step-links { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: .6rem; }

.oc-guide-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.25rem; }
.oc-checkline { display: flex; gap: .55rem; align-items: flex-start; font-size: .875rem; color: var(--ink-700); padding: .3rem 0; }
.oc-checkline i { width: 16px; height: 16px; flex: 0 0 auto; margin-top: .15rem; }
.oc-checkline.ok i { color: var(--success); }
.oc-checkline.wire i { color: var(--warning); }

/* ============================================================
   PREMIUM LAYER — data-viz + richer components (theme untouched)
   ============================================================ */

/* ...but never on a block that hosts a modal. This animation keeps a transform in effect (fill: both),
   and a transformed element becomes the containing block AND stacking context for its position:fixed
   descendants. A modal inside one is then sized to that block instead of the viewport, and its z-index
   is trapped in the block's context, so it paints behind whatever the page puts above it. :has() exempts
   such blocks automatically, so a modal dropped into any card keeps working without anyone remembering
   this rule. */

/* Section header (icon + title + sub + optional action) */
.oc-section { display: flex; align-items: center; gap: .75rem; margin: 1.75rem 0 1rem; }
.oc-section-ic { width: 2rem; height: 2rem; border-radius: .6rem; display: grid; place-items: center; background: var(--brand-50); color: var(--brand-700); flex: 0 0 auto; }
/* The glyph is sized to its 17px box and centred in it — it used to inherit an 18px+ font size and spill
   down-right, so the icon sat off-centre in its tile. */
.oc-section-ic i { width: 17px; height: 17px; font-size: 15px; line-height: 1;
    display: inline-flex; align-items: center; justify-content: center; }
.oc-section-t { font-weight: 800; letter-spacing: -.01em; color: var(--ink-900); font-size: 1.02rem; }
.oc-section-s { color: var(--ink-400); font-size: .82rem; }

/* New agreement's "Fields shared across these agreements" card (Create.cshtml) — Option C from the
   empty-state UI options: a thin muted strip until #shared_agreement_fields actually has fields, then
   the full card. Driven by :has() reading is-empty/is-filled, which _SharedAgreementFields.cshtml
   restamps on its own root every time the htmx swap runs, so this reacts live to every tick/untick, not
   just on page load. */
.oc-shared-s-filled { display: none; }
.oc-shared-body { display: none; }
.oc-shared-header {
    background: var(--surface-muted); border: 1px solid var(--line-2); border-radius: .75rem;
    padding: .7rem 1rem; margin: 1.75rem 0 1rem;
}
.oc-shared-header .oc-section-ic { background: var(--line-2); color: var(--ink-400); }
.oc-shared-info-card:has(#shared_agreement_fields.is-filled) .oc-shared-header {
    background: transparent; border: none; border-radius: 0; padding: 0;
}
.oc-shared-info-card:has(#shared_agreement_fields.is-filled) .oc-shared-header .oc-section-ic {
    background: var(--brand-50); color: var(--brand-700);
}
.oc-shared-info-card:has(#shared_agreement_fields.is-filled) .oc-shared-s-empty { display: none; }
.oc-shared-info-card:has(#shared_agreement_fields.is-filled) .oc-shared-s-filled { display: block; }
.oc-shared-info-card:has(#shared_agreement_fields.is-filled) .oc-shared-body { display: block; }

/* New agreement, desktop: ONE main card under the client — templates stacked and compact on the left,
   the shared-sections box on the right with the rest of the width (Create.cshtml, .oc-agreement-main).
   Layout only; the panels are unchanged. Below 60rem the wrapper is a plain block and each panel is its
   own screen as before (the tab rule for it is in the 60rem block further down). */
@media (min-width: 60.0625rem) {
    .oc-agreement-main {
        display: grid; grid-template-columns: minmax(16rem, 20rem) minmax(0, 1fr); gap: 1.5rem;
        align-items: start; margin-bottom: 1rem; padding: 1.25rem 1.5rem;
        border: 1px solid var(--line-2); border-radius: 1rem; background: var(--surface);
    }
    .oc-agreement-main > [data-oc-tab-panel="picker"] { grid-column: 1; grid-row: 1; min-width: 0; }
    .oc-agreement-main > [data-oc-tab-panel="shared"] { grid-column: 2; grid-row: 1; min-width: 0; }
    .oc-agreement-main .oc-section { margin-top: 0; flex-wrap: wrap; }
    .oc-agreement-main .oc-shared-header { margin-top: 0; }
    .oc-agreement-main .oc-template-grid { grid-template-columns: 1fr; gap: .75rem; }
    .oc-agreement-main .oc-template-card { padding: .85rem 1rem; }
    /* By request: no thumbnail and no "N sections · N required" line on the agreement cards — name,
       "When to use", the checkbox and "All N sections" are what is left (as on a phone, minus the list). */
    .oc-agreement-main .oc-thumb,
    .oc-agreement-main .oc-template-meta { display: none; }
    /* Nothing shared yet (none or one type ticked): no empty right-hand column, by request. The shared box
       is hidden and the templates use the whole card, three across; ticking a second type fills
       #shared_agreement_fields (is-filled) and the two-column layout returns. */
    .oc-agreement-main:has(#shared_agreement_fields.is-empty) { grid-template-columns: minmax(0, 1fr); }
    .oc-agreement-main:has(#shared_agreement_fields.is-empty) > [data-oc-tab-panel="shared"] { display: none; }
    /* auto-FIT, not auto-fill, by request: the three cards stretch to fill the whole box instead of
       leaving an empty fourth track on the right. */
    .oc-agreement-main:has(#shared_agreement_fields.is-empty) .oc-template-grid {
        grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
    }
    /* Each card's title on ONE line, and the "When to use" box as wide as the card (the body kept a
       1.75rem right gutter from the proposal card's corner badge, which agreement cards do not have). */
    .oc-agreement-main .oc-template-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .oc-agreement-main .oc-template-body { padding-right: 0; }
    /* "Start drafting" lives at the bottom beside Cancel on desktop (Create.cshtml, .oc-batch-start-foot),
       so the copy beside the Template heading is hidden here. */
    .oc-agreement-main [data-oc-tab-panel="picker"] .oc-section > .oc-head-actions:has([data-oc-batch-start]) { display: none; }
    /* ...and that bottom row is a STICKY bar, by request: at the very end of a long page Cancel and Start
       drafting were easy to never scroll to. It rides the bottom edge of the window the whole time, the
       same place the phone keeps its own button, and settles into its normal spot at the end. */
    .oc-form-actions:has(> .oc-batch-start-foot) {
        position: sticky; bottom: 0; z-index: 20;
        margin-top: 1rem; padding: .75rem 1.25rem;
        background: color-mix(in srgb, var(--surface) 92%, transparent);
        backdrop-filter: blur(6px);
        border: 1px solid var(--line-2); border-radius: .9rem .9rem 0 0;
        box-shadow: 0 -6px 18px rgba(15, 23, 42, .06);
    }
}

/* Numbered, collapsible groups inside that card (_SharedAgreementFields.cshtml). Native <details> like
   .oc-more — opens with no script; shared-fields-card.js only toggles is-complete and the counts. */
.oc-sharedgroup { border: 1px solid var(--line-2); border-radius: .75rem; margin-bottom: .75rem; background: var(--surface); }
.oc-sharedgroup-summary {
    display: flex; align-items: center; gap: .85rem; padding: .85rem 1rem; cursor: pointer; list-style: none;
}
.oc-sharedgroup-summary::-webkit-details-marker { display: none; }
.oc-sharedgroup-summary:focus-visible { outline: 2px solid var(--brand-500); outline-offset: -2px; border-radius: .75rem; }
.oc-sharedgroup[open] > .oc-sharedgroup-summary { border-bottom: 1px solid var(--line-2); }
.oc-sharedgroup-num {
    width: 2.1rem; height: 2.1rem; border-radius: 50%; display: grid; place-items: center; flex: 0 0 auto;
    background: var(--brand-50); color: var(--brand-700); font-weight: 800; font-size: .9rem;
}
.oc-sharedgroup-titles { display: flex; flex-direction: column; min-width: 0; flex: 1 1 auto; }
.oc-sharedgroup-t { font-weight: 700; color: var(--ink-900); font-size: .95rem; }
.oc-sharedgroup-s { color: var(--ink-400); font-size: .8rem; }
.oc-sharedgroup-count {
    display: inline-flex; align-items: center; gap: .4rem; font-size: .8rem; color: var(--ink-500);
    white-space: nowrap; font-variant-numeric: tabular-nums;
}
.oc-sharedgroup-count .oc-sharedgroup-todo { color: var(--ink-300, #cbd5e1); }
.oc-sharedgroup-count .oc-sharedgroup-done { display: none; color: var(--success); }
.oc-sharedgroup.is-complete .oc-sharedgroup-done { display: inline-flex; }
.oc-sharedgroup.is-complete .oc-sharedgroup-todo { display: none; }
.oc-sharedgroup-chev { color: var(--ink-400); transition: transform .15s ease; display: inline-flex; }
.oc-sharedgroup[open] .oc-sharedgroup-chev { transform: rotate(180deg); }
.oc-sharedgroup-body { padding: 1rem; margin-bottom: 0; }
.oc-sharedgroup-foot {
    display: flex; align-items: center; gap: .6rem; padding-top: 1rem; margin-top: .25rem;
    border-top: 1px solid var(--line-2);
}
.oc-sharedgroup-note {
    display: inline-flex; align-items: center; gap: .45rem; margin-right: auto;
    font-size: .8rem; color: var(--ink-500);
}
.oc-sharedgroup-note i { color: var(--brand-600); }
@media (max-width: 40rem) {
    .oc-sharedgroup-s { display: none; }
    .oc-sharedgroup-foot { flex-wrap: wrap; }
    .oc-sharedgroup-note { flex-basis: 100%; }
}
@media (prefers-reduced-motion: reduce) { .oc-sharedgroup-chev { transition: none; } }
.oc-section .oc-head-actions { margin-left: auto; }

/* Company / contact square avatar (initials) */
.oc-sq { width: 2.1rem; height: 2.1rem; border-radius: .55rem; display: grid; place-items: center; font-weight: 800; font-size: .78rem; color: var(--primary-foreground); flex: 0 0 auto; background: linear-gradient(135deg, var(--brand-500), var(--brand-700)); box-shadow: 0 2px 5px -1px rgba(79,70,229,.4); }
.oc-sq-lg { width: 3rem; height: 3rem; border-radius: .7rem; font-size: 1rem; }
.oc-idcell { display: flex; align-items: center; gap: .65rem; }
.oc-idcell .sub { color: var(--ink-400); font-size: .78rem; }

/* Score ring (conic-gradient) — set --v: 0..100 and --c: color */
.oc-ring { --v: 0; --c: var(--brand-600); width: 84px; height: 84px; border-radius: 50%; display: grid; place-items: center; flex: 0 0 auto;
    background: conic-gradient(var(--c) calc(var(--v) * 1%), var(--line-2) 0); position: relative; }
.oc-ring::before { content: ""; position: absolute; inset: 8px; border-radius: 50%; background: var(--surface); box-shadow: inset 0 1px 2px rgba(15,23,42,.05); }
.oc-ring-in { position: relative; text-align: center; line-height: 1; }
.oc-ring-v { font-size: 1.25rem; font-weight: 800; color: var(--ink-900); }
.oc-ring-l { font-size: .6rem; text-transform: uppercase; letter-spacing: .05em; color: var(--ink-400); font-weight: 700; margin-top: .15rem; }
.oc-ring-sm { width: 60px; height: 60px; }
.oc-ring-sm .oc-ring-v { font-size: 1rem; }

/* Horizontal meter */
/* Capped: the meter stretched to whatever the Progress column happened to be, so space-between threw
   "0/0" and "0%" to opposite ends of it and they stopped reading as one figure. */
.oc-meter { display: flex; flex-direction: column; gap: .3rem; max-width: 11rem; }
.oc-meter-top { display: flex; justify-content: space-between; font-size: .78rem; color: var(--ink-500); }
.oc-meter-top b { color: var(--ink-900); font-weight: 700; }
.oc-meter-bar { height: .5rem; border-radius: 999px; background: var(--line-2); overflow: hidden; }
.oc-meter-fill { height: 100%; border-radius: 999px; background: linear-gradient(90deg, var(--brand-500), var(--brand-700)); }
.oc-meter-fill.warn { background: linear-gradient(90deg, #f59e0b, var(--warning)); }
.oc-meter-fill.ok { background: linear-gradient(90deg, #10b981, var(--success)); }

/* Progress-meter widths.
   The width of a progress bar is DATA, and .cshtml may not carry inline styles (CLAUDE.md §4) — so the
   percentage is rounded to the nearest 5 and rendered as a class. A Tailwind arbitrary value (w-[47%])
   cannot work here either: the theme ships COMPILED Tailwind, so a class nobody wrote at build time does
   not exist in the stylesheet. Twenty-one real classes do. */
.oc-meter-fill.p-0 { width: 0%; }
.oc-meter-fill.p-5 { width: 5%; }
.oc-meter-fill.p-10 { width: 10%; }
.oc-meter-fill.p-15 { width: 15%; }
.oc-meter-fill.p-20 { width: 20%; }
.oc-meter-fill.p-25 { width: 25%; }
.oc-meter-fill.p-30 { width: 30%; }
.oc-meter-fill.p-35 { width: 35%; }
.oc-meter-fill.p-40 { width: 40%; }
.oc-meter-fill.p-45 { width: 45%; }
.oc-meter-fill.p-50 { width: 50%; }
.oc-meter-fill.p-55 { width: 55%; }
.oc-meter-fill.p-60 { width: 60%; }
.oc-meter-fill.p-65 { width: 65%; }
.oc-meter-fill.p-70 { width: 70%; }
.oc-meter-fill.p-75 { width: 75%; }
.oc-meter-fill.p-80 { width: 80%; }
.oc-meter-fill.p-85 { width: 85%; }
.oc-meter-fill.p-90 { width: 90%; }
.oc-meter-fill.p-95 { width: 95%; }
.oc-meter-fill.p-100 { width: 100%; }


/* Legacy KPI stat tile (icon + big value + label). Named .oc-ktile because the record
   card further down owns .oc-tile — two different components cannot share one class. */
.oc-ktiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 1.5rem; }
.oc-ktile { position: relative; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); padding: 1.2rem 1.25rem; overflow: hidden; transition: box-shadow .2s, transform .15s, border-color .2s; }
.oc-ktile:hover { box-shadow: var(--shadow-md); transform: translateY(-3px); border-color: var(--line-strong); }
.oc-ktile::after { content: ""; position: absolute; right: -20px; top: -20px; width: 90px; height: 90px; border-radius: 50%; background: radial-gradient(circle, rgba(79,70,229,.09), transparent 70%); }
.oc-ktile-ic { width: 2.5rem; height: 2.5rem; border-radius: .7rem; display: grid; place-items: center; background: var(--brand-50); color: var(--brand-700); margin-bottom: .85rem; }
.oc-ktile-ic i { width: 19px; height: 19px; }
.oc-ktile-v { font-size: 1.95rem; font-weight: 800; color: var(--ink-900); letter-spacing: -.02em; line-height: 1; }
/* A tile whose value is WORDS rather than a figure — a client name, a status, a date. 1.95rem is sized
   for "USD 4,567,890.00"; a company name at that size wraps to three lines and stops being scannable,
   which is the opposite of what a tile is for. */
.oc-ktile-v-text {
    font-size: 1.05rem; line-height: 1.3; font-weight: 700; letter-spacing: -.01em;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* A tile whose "value" is genuinely absent — no price set, no date agreed. Nothing is wrong; there is
   simply nothing yet. The dash/"Not set" fallback used to render through .oc-ktile-v at full 800-weight
   size, which put the LEAST meaningful content in the tile at the LOUDEST possible size — a reader's eye
   lands on the empty tiles first, not the ones that actually say something. This keeps the layout (same
   slot, same tile) but drops the weight/size/color to read as absence rather than as a headline. */
.oc-ktile-v-muted {
    font-size: 1.05rem; font-weight: 600; color: var(--ink-400); letter-spacing: -.01em; line-height: 1.3;
}

.oc-ktile-l { font-size: .8rem; color: var(--ink-500); margin-top: .35rem; font-weight: 500; }
/* A second, quieter figure under the label — the count is the headline, the currency total is context
   (e.g. "3 · USD 6,000.00 paid"), so it stays visually secondary rather than competing with oc-ktile-v. */
.oc-ktile-sub { font-size: .78rem; color: var(--ink-500); margin-top: .2rem; font-weight: 600; }

/* Compact variant — a couple of small counts (e.g. paid/unpaid) don't need the full-size tile: capped
   column width keeps two equal cards from stretching edge-to-edge, and the smaller icon/type scale reads
   as one deliberate size rather than the same big KPI tile with a short number in it. */
.oc-ktiles-compact { grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); }
.oc-ktiles-compact .oc-ktile { padding: .85rem 1rem; }
.oc-ktiles-compact .oc-ktile-ic { width: 2rem; height: 2rem; margin-bottom: .55rem; }
.oc-ktiles-compact .oc-ktile-ic i { width: 15px; height: 15px; }
.oc-ktiles-compact .oc-ktile-v { font-size: 1.4rem; }
.oc-ktiles-compact .oc-ktile-l { font-size: .75rem; margin-top: .2rem; }
.oc-ktiles-compact .oc-ktile-sub { font-size: .72rem; margin-top: .15rem; }

/* Accent card — colored top bar (tier / service) */
.oc-acard { position: relative; }
.oc-acard::before { content: ""; position: absolute; left: 0; right: 0; top: 0; height: 3px; border-radius: var(--radius-lg) var(--radius-lg) 0 0; background: var(--acc, var(--brand-600)); }

/* Tier + service colors */
.tier-A { --acc: var(--success); } .tier-B { --acc: var(--info); } .tier-C { --acc: var(--warning); } .tier-D { --acc: var(--ink-400); }
.badge-tier-A { color:#047857; background:#ecfdf5; } .badge-tier-B { color:#1d4ed8; background:#eff6ff; }
.badge-tier-C { color:#b45309; background:#fffbeb; } .badge-tier-D { color:var(--ink-600); background:var(--line-2); }
.oc-svc-dot { width:.5rem;height:.5rem;border-radius:50%;display:inline-block;margin-right:.35rem;background:var(--brand-500); }

/* Funnel as proportional bars */
.oc-fbars { display: flex; flex-direction: column; gap: .55rem; }
.oc-fbar { display: grid; grid-template-columns: 130px 1fr 2.5rem; align-items: center; gap: .75rem; }
.oc-fbar-l { font-size: .82rem; color: var(--ink-600); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.oc-fbar-track { height: 1.4rem; background: var(--line-2); border-radius: .5rem; overflow: hidden; }
.oc-fbar-fill { height: 100%; width: 0; border-radius: .5rem; background: linear-gradient(90deg, var(--brand-400,#818cf8), var(--brand-600)); min-width: 3px; transition: width .6s cubic-bezier(.22,1,.36,1); }
.oc-fbar-fill.acc { background: var(--acc); }
.oc-fbar.tier-A .oc-fbar-fill.acc { background: linear-gradient(90deg,#34d399,var(--success)); }
.oc-fbar.tier-B .oc-fbar-fill.acc { background: linear-gradient(90deg,#60a5fa,var(--info)); }
.oc-fbar.tier-C .oc-fbar-fill.acc { background: linear-gradient(90deg,#fbbf24,var(--warning)); }
.oc-fbar.tier-D .oc-fbar-fill.acc { background: linear-gradient(90deg,#cbd5e1,var(--ink-400)); }
.oc-fbar-n { font-size: .85rem; font-weight: 700; color: var(--ink-900); text-align: right; }

/* Two-column deal header */
.oc-deal-head { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; }

.oc-rings-row { display: flex; gap: 1.1rem; justify-content: space-around; flex-wrap: wrap; }

/* Cockpit block state chip */
.oc-block-done { color: var(--success); } .oc-block-todo { color: var(--ink-300, #cbd5e1); }

/* ---------- Responsive: collapse the sidebar to a top bar ---------- */

/* ---------- octa-clients: size inline SVG icons the same as the lucide <i> the shell was built for ---------- */
.oc-brand-badge svg { width: 18px; height: 18px; }
.oc-ktile-ic svg    { width: 19px; height: 19px; }
.oc-start-ic svg    { width: 20px; height: 20px; }
.oc-section-ic svg  { width: 17px; height: 17px; }
.oc-card-title svg  { width: 16px; height: 16px; }

/* ============================================================
   Font Awesome <i> glyph sizing for shell chrome.
   The theme pivoted icons from Lucide inline-SVG to Font Awesome font
   glyphs (rendered as <i>), which size by FONT-SIZE, not width/height.
   Size the glyphs where the shell expects a specific icon size.
   ============================================================ */
.oc-brand-badge i { font-size: 1.05rem; }
.oc-nav-link i, .oc-side-foot i, .oc-icon-btn i, .oc-card-title i,
.oc-checkline i, .oc-obj-name i { font-size: 1rem; }
.oc-ktile-ic i, .oc-start-ic i, .oc-section-ic i { font-size: 1.15rem; }
.oc-empty-icon i, .oc-icon-box i { font-size: 1.25rem; }
/* Centre the glyph in its box. Html.Icon's default w-4 h-4 pins the <i> to a 1rem square while the glyph
   renders at 1.25rem, so it spilled out to the right — a wide icon (the scales on "No pricing yet") sat
   visibly off-centre. Sized to its own glyph instead, the box's flex centring does the rest. */
.oc-empty-icon i, .oc-icon-box i { width: auto; height: auto; line-height: 1; }
/* The same fix for three more icon boxes that were never given it (by request): the admin dashboard's
   Welcome icon, the portal Home's "Nothing is waiting" icon, and the empty-state primitive's round
   badge (e.g. Receipts' "Nothing sent yet") — glyph sized to itself, so the box's own centring works. */
.oc-welcome-icon i,
.oc-brand-octacer .oc-blank-icon i,
[role="status"] > .rounded-full > i { width: auto; height: auto; line-height: 1; }
[role="status"] > .rounded-full.size-16 > i { font-size: 1.6rem; }
[role="status"] > .rounded-full.size-12 > i { font-size: 1.15rem; }
.oc-land-badge i { font-size: .9rem; }

/* ============================================================
   Tailwind utility supplement. Written when the theme shipped a COMPILED
   Tailwind build that only carried the utilities Metronic itself used;
   octa-core a13f714 moved the theme to the Tailwind browser CDN, which
   generates every utility on demand, so these are now duplicates rather
   than the only definition. Kept because they also serve the two pages
   that must render with no external request (proposal + PDF), and because
   dropping them would change nothing except the risk of a silent regress.
   Do not add to this list — author Tailwind utilities directly.
   ============================================================ */
.m-0 { margin: 0; }
.mt-8 { margin-top: 2rem; }
.p-6 { padding: 1.5rem; }
.px-6 { padding-left: 1.5rem; padding-right: 1.5rem; }
.py-16 { padding-top: 4rem; padding-bottom: 4rem; }
.pl-3 { padding-left: .75rem; }
.max-w-3xl { max-width: 48rem; }
.min-h-screen { min-height: 100vh; }
.space-y-1 > * + * { margin-top: .25rem; }
.font-mono { font-family: var(--font-sans); }
.text-gray-400 { color: var(--ink-400); }
.text-gray-900 { color: var(--ink-900); }
.text-brand-600 { color: var(--brand-600); }
.bg-gray-100 { background-color: var(--line-2); }
.empty\:hidden:empty { display: none; }
.hover\:underline:hover { text-decoration: underline; }
@media (min-width: 1024px) {
    .lg\:grid-cols-\[1fr_320px\] { grid-template-columns: 1fr 320px; }
}

/* ============================================================
   Auth split-screen (anonymous portal auth pages).
   Replaces the Tailwind gradient/arbitrary-value markup + inline <style>
   the page once relied on, so _LayoutPortalAuth's look lives in one place
   and no inline styles are needed. Keyframes oc-float / oc-mesh live in
   octacer-theme.css.
   ============================================================ */
.oc-auth { display: flex; min-height: 100vh; }
.oc-auth-brand {
    position: relative; overflow: hidden; display: none; color: #fff;
    background: linear-gradient(135deg, var(--brand-800), var(--brand-700) 55%, var(--brand-600));
    background-size: 200% 200%;
}
.oc-auth-brand-inner { position: relative; /* no z-index: it would make a stacking context and the logo's lighten blend would see no gradient behind it (a black box) */ display: flex; flex-direction: column; justify-content: space-between; width: 100%; padding: 3rem; }
.oc-auth-orbs { position: absolute; inset: 0; opacity: .18; }
.oc-auth-orb { position: absolute; border-radius: 50%; filter: blur(48px); }
.oc-auth-orb-1 { left: -2.5rem; top: -2.5rem; width: 20rem; height: 20rem; background: #fff; }
.oc-auth-orb-2 { right: -4rem; top: 33%; width: 24rem; height: 24rem; background: #a5b4fc; }
.oc-auth-brand-head { display: flex; align-items: center; gap: .75rem; margin-bottom: 3.5rem; }
.oc-auth-logo { display: inline-flex; align-items: center; justify-content: center; padding: .625rem; border-radius: 1rem; background: rgba(255,255,255,.15); box-shadow: var(--shadow-lg); border: 1px solid rgba(255,255,255,.25); backdrop-filter: blur(12px); }
.oc-auth-logo img { width: 2.25rem; height: 2.25rem; border-radius: .5rem; object-fit: cover; display: block; }
.oc-auth-brand-name { font-family: 'Montserrat','Inter',sans-serif; font-size: 1.125rem; font-weight: 800; letter-spacing: -.02em; line-height: 1.1; }
.oc-auth-brand-kicker { display: block; font-size: 11px; font-weight: 500; text-transform: uppercase; letter-spacing: .18em; color: rgba(255,255,255,.6); }
.oc-auth-brand-title { font-family: 'Montserrat','Inter',sans-serif; max-width: 28rem; font-size: 1.875rem; font-weight: 800; line-height: 1.15; }
.oc-auth-brand-sub { max-width: 28rem; margin-top: 1rem; color: rgba(255,255,255,.7); }
.oc-auth-brand-foot { font-size: .75rem; color: rgba(255,255,255,.5); }
.oc-auth-form { display: flex; flex-direction: column; justify-content: center; width: 100%; padding: 2.5rem 1.5rem; }
.oc-auth-form-inner { width: 100%; max-width: 24rem; margin: 0 auto; }
.oc-auth-mini { display: flex; align-items: center; gap: .625rem; margin-bottom: 1.5rem; }
.oc-auth-mini-logo { display: inline-flex; align-items: center; justify-content: center; width: 2.25rem; height: 2.25rem; overflow: hidden; border-radius: .75rem; background: linear-gradient(135deg, var(--brand-600), var(--brand-800)); box-shadow: var(--shadow-sm); }
.oc-auth-mini-logo img { width: 2.25rem; height: 2.25rem; object-fit: cover; }
/* Vector mark, so the gradient tile the JPEG needed goes with it. */
.oc-brand-octacer .oc-auth-mini-logo { background: none; box-shadow: none; border-radius: 0; }
.oc-brand-octacer .oc-auth-mini-logo .oc-ring { width: 2.1rem; height: 2.1rem; display: block; }
.oc-auth-mini-name { font-size: 15px; font-weight: 800; letter-spacing: -.02em; }
@media (min-width: 1024px) {
    .oc-auth-brand { display: flex; width: 50%; }
    .oc-auth-mini { display: none; }
    .oc-auth-form { width: 50%; padding-top: 2.5rem; padding-bottom: 2.5rem; }
}
@media (min-width: 1280px) {
    .oc-auth-brand { width: 44%; }
    .oc-auth-brand-inner { padding: 3.5rem; }
    .oc-auth-brand-title { font-size: 2.25rem; }
    .oc-auth-form { width: 56%; }
}

/* ---- Proposal template picker (new proposal screen) -------------------------------------------- */
.oc-template-grid { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fill, minmax(19rem, 1fr)); }
.oc-template-card { position: relative; display: flex; flex-direction: column; border: 1px solid var(--oc-line, var(--line));
  border-radius: 14px; background: var(--surface); padding: 1.15rem 1.25rem; transition: border-color .15s, box-shadow .15s;
  cursor: pointer; }
.oc-template-card:hover { border-color: #c7d2fe; }
/* Interactive controls inside the card (Preview, the batch checkbox, the disclosure summary, the
   button itself) keep their own real cursor — only the otherwise-inert card surface reads as "pick
   this" (template-card-click.js). */
.oc-template-card a, .oc-template-card button, .oc-template-card label,
.oc-template-card summary { cursor: pointer; }
/* The label carries the click target so the preview disclosure below it stays independent — opening
   a preview must never change which template is selected. */
/* No radio and no selected state any more: the card is something to read, and its own "Use this
   template" button is the only thing on it that acts. What is left is a plain hover lift, so the card
   reads as one unit without pretending to be a control. */
.oc-template-pick { display: block; }
.oc-template-card:hover { border-color: var(--brand-600); box-shadow: 0 0 0 3px rgba(79,70,229,.10); }
.oc-template-card:focus-within { border-color: var(--brand-600); box-shadow: 0 0 0 3px rgba(79,70,229,.12); }
.oc-template-body { display: flex; flex-direction: column; gap: .4rem; padding-right: 1.75rem; }
.oc-template-name { font-weight: 600; color: var(--ink-900); }
.oc-template-desc { font-size: .8125rem; color: var(--ink-600); line-height: 1.45; }
/* Agreement cards: the description is the template's "when to use" guidance — what decides which
   agreements to tick — so it reads as a callout rather than faint body text. */
.oc-template-desc.is-when {
    display: block; margin-top: .35rem; padding: .6rem .75rem; border-radius: .6rem;
    background: color-mix(in srgb, var(--brand-500, #6366f1) 9%, transparent);
    border-left: 3px solid var(--brand-500, #6366f1);
    color: var(--ink-900); font-size: .875rem; font-weight: 500;
}
.oc-template-when-label {
    display: block; margin-bottom: .2rem; font-size: .6875rem; font-weight: 700;
    letter-spacing: .06em; text-transform: uppercase; color: var(--brand-700, #4338ca);
}
.oc-template-meta { font-size: .75rem; font-weight: 600; color: var(--brand-600); }
.oc-template-sections { font-size: .75rem; color: var(--ink-400); line-height: 1.5; }

/* Card footer: preview action on the left, selection state on the right. */
.oc-template-foot { display: flex; align-items: center; justify-content: space-between; gap: .5rem;
  margin-top: .9rem; }
.oc-template-foot-note { font-size: .75rem; color: var(--ink-400); }


/* Mini document. Shapes are drawn from the template's own block types (see _TemplateThumb.cshtml), so
   the picture cannot drift from what the template builds. */
.oc-thumb { display: flex; flex-direction: column; gap: .3rem; height: 8.5rem; overflow: hidden;
  padding: .6rem .65rem; margin-bottom: .9rem; border: 1px solid var(--oc-line, var(--line));
  border-radius: 10px; background: linear-gradient(180deg, #fbfbff 0%, #f5f4ff 100%); }
.oc-template-card:hover .oc-thumb { border-color: #c7d2fe; background:
  linear-gradient(180deg, #f8f7ff 0%, #eeecff 100%); }
.oc-thumb.is-blank { align-items: center; justify-content: center; }
/* Drawn with two CSS bars rather than the icon font: a webfont glyph's own metrics (advance width,
   internal padding) are never quite square, so a "+" from the font reads as slightly off-center inside a
   perfectly round badge no matter how its box is centered. Two bars crossed at the exact midpoint are
   centered by construction — nothing left to misalign. */
.oc-thumb-plus { display: block; position: relative; width: 2rem; height: 2rem; border-radius: 999px;
  background: #ede9fe; color: #6d28d9; }
.oc-thumb-plus::before, .oc-thumb-plus::after {
  content: ""; position: absolute; top: 50%; left: 50%; background: currentColor; border-radius: 1px;
}
.oc-thumb-plus::before { width: .875rem; height: 2px; transform: translate(-50%, -50%); }
.oc-thumb-plus::after { width: 2px; height: .875rem; transform: translate(-50%, -50%); }
.oc-thumb-cover { display: flex; flex-direction: column; gap: .22rem; padding: .45rem .5rem;
  border-radius: 6px; background: var(--brand-600); }
.oc-thumb-row { display: flex; flex-direction: column; gap: .18rem; }
.oc-thumb-bar { height: .3rem; border-radius: 2px; background: #a5b4fc; }
.oc-thumb-bar.is-wide { width: 80%; height: .38rem; background: #ffffff; }
.oc-thumb-bar.is-mid { width: 50%; background: rgba(255,255,255,.6); }
.oc-thumb-bar.is-head { width: 42%; background: #6366f1; }
.oc-thumb-line { height: .22rem; border-radius: 2px; background: #dcdcf0; }
.oc-thumb-line.is-short { width: 62%; }
.oc-thumb-tick { position: relative; height: .22rem; border-radius: 2px; background: #dcdcf0;
  margin-left: .6rem; }
.oc-thumb-tick.is-short { width: 55%; }
.oc-thumb-tick::before { content: ""; position: absolute; left: -.6rem; top: -.07rem; width: .36rem;
  height: .36rem; border-radius: 999px; background: #a5b4fc; }
.oc-thumb-steps { display: flex; gap: .22rem; }
.oc-thumb-step { flex: 1; height: .5rem; border-radius: 2px; background: #dcdcf0; }
.oc-thumb-step:first-child { background: #a5b4fc; }
.oc-thumb-tr { display: flex; justify-content: space-between; gap: .5rem; }
.oc-thumb-tr > span { height: .22rem; border-radius: 2px; background: #dcdcf0; flex: 1; }
.oc-thumb-tr > span.is-num { flex: 0 0 1.4rem; background: #c7d2fe; }
.oc-thumb-tr.is-total > span { background: #a5b4fc; }

.oc-template-preview { margin-top: .85rem; border-top: 1px solid var(--oc-line, var(--line)); padding-top: .6rem; }
.oc-template-preview > summary { cursor: pointer; font-size: .75rem; font-weight: 600; color: var(--brand-600);
  list-style: none; user-select: none; }
.oc-template-preview > summary::-webkit-details-marker { display: none; }
.oc-template-preview > summary::before { content: "▸ "; }
.oc-template-preview[open] > summary::before { content: "▾ "; }
.oc-template-preview-body { padding-top: .5rem; }
.oc-template-preview-h { font-size: .6875rem; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  color: var(--ink-400); margin: .5rem 0 .25rem; }
.oc-template-secs { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: .15rem; }
.oc-template-secs > li { font-size: .8125rem; color: var(--ink-600); line-height: 1.4; }
.oc-template-secs > li.is-req { color: var(--ink-900); }

/* ---- Proposal editor: section list + editor pane ------------------------------------------------ */
/* 19rem, up from the original 16rem. The list is number · name · state, and the width is set by the
   longest real section name ("What We Need From You") sitting on one line with room to spare. It briefly
   needed 22rem to also fit a type label per row; that label was removed as a duplicate of the name, so
   the extra width went back to the editor pane where it is worth more. */
/* ---- Builder tabs (proposal-builder-tabs.js) -------------------------------------------------------
   The strip exists only where the scrolling does. Above 60rem the two panels stack exactly as the page
   always has, and this is not on screen at all — which is also why the panel-hiding rule lives in the
   media query and nowhere else. */
.oc-tabstrip { display: none; }
/* ---- Mobile section navigation (proposal-section-nav.js) --------------------------------------------
   The stepper, the "Section X of Y" line and the sticky Previous/Next are the phone's replacement for
   the rail, and exist only where the rail is hidden. Off by default so a desktop never pays for them. */
.oc-sectionsteps-bar, .oc-sectionstep-title, .oc-sectionstep-nav,
.oc-shared-panel-actions { display: none; }
.oc-editor { display: grid; gap: 1.25rem; grid-template-columns: 19rem 1fr; align-items: start; }
@media (max-width: 60rem) { .oc-editor { grid-template-columns: 1fr; } }
/* Full-width "Previous agreement" / "Next agreement" strip above the Sections editor grid — spans the
   whole card, not just the sidebar column (AgreementIntake batch flow). Both buttons cluster together
   on the right, Previous before Next in reading order. */
.oc-editor-batch-bar { display: flex; align-items: center; justify-content: flex-end; gap: .75rem; }
/* "Common Sections" sits on the far LEFT of the bar, by request, while Previous/Next keep clustering on the
   right — an auto margin, so the bar still right-aligns them when this button is absent. Reset where the
   bar stacks into a full-width column (phone), or the auto margin would stop it stretching. */
.oc-editor-batch-bar .oc-batch-common { margin-right: auto; }
@media (max-width: 60rem) {
    .oc-editor-batch-bar .oc-batch-common { margin-right: 0; }
}
/* Phone: the two buttons' full "Previous agreement: <template name>" / "Next agreement: <template name>"
   labels ran past the viewport width side by side at .btn-sm's desktop size, cut off rather than wrapped.
   Stacked full-width, smaller and wrapping, keeps both readable without spilling off-screen. */
@media (max-width: 640px) {
    .oc-editor-batch-bar { flex-direction: column; align-items: stretch; gap: .5rem; }
    .oc-editor-batch-bar .btn { width: 100%; justify-content: center; white-space: normal;
        text-align: center; font-size: .8rem; padding: .5rem .75rem; }
}
/* Agreement type, in the "Agreement details" card head (Builder.cshtml) — bigger and bolder than the
   ordinary .badge it builds on, since this is the one fact telling apart which of several open
   agreements this document is, and it needs to read at a glance, not on close inspection. */
.oc-agreement-type-badge { font-size: .8rem; padding: .3rem .75rem; font-weight: 700; }
.oc-editor-nav { background: var(--surface); border: 1px solid var(--oc-line, var(--line)); border-radius: 14px; overflow: hidden;
  position: sticky; top: 1rem; }
.oc-editor-nav-head { padding: .8rem 1rem; font-size: .72rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: .08em; color: var(--ink-400); border-bottom: 1px solid var(--line-2); }
.oc-section-list { list-style: none; margin: 0; padding: .35rem; counter-reset: s; max-height: 40rem; overflow-y: auto; }
/* The handle sits OUTSIDE .oc-section-item (a sibling in the <li>), not inside its grid — the row stays a
   plain link with its own three-column layout, and grabbing the handle can never be mistaken for a click
   on the link. */
.oc-section-list > li { display: flex; align-items: center; gap: .05rem; }
.oc-drag-handle {
    display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto;
    width: 1.35rem; height: 1.9rem; border: 0; border-radius: 6px; background: transparent;
    color: var(--ink-400); cursor: grab; touch-action: none;
}
.oc-drag-handle:hover, .oc-drag-handle:focus-visible { color: var(--ink-700); background: #eef2f7; }
.oc-drag-handle:active { cursor: grabbing; }
/* Never locked, by request — reordering only changes what order sections are READ in, not their content,
   so it stays available on an otherwise-locked (Accepted) proposal. Nothing here gates on status. */
.oc-section-list > li.is-dragging { opacity: .45; }
.oc-section-list > li.is-drop-target > .oc-section-item {
    box-shadow: inset 0 2px 0 0 var(--brand-500, #6366f1);
}
/* A GRID with fixed column widths, not a flex row. Each list item is its own formatting context, so
   `auto` widths size to that row's own content and nothing lines up with the row above it — which is
   why the type labels and ticks marched in and out as section names wrapped to two lines. Fixed columns
   are the only way to get a straight edge down a list of independent rows.
   Columns: number (::before) · name · type · state. */
.oc-section-item { display: grid; grid-template-columns: 1.35rem minmax(0, 1fr) 1.35rem;
  align-items: center; gap: .6rem; padding: .5rem .65rem; border-radius: 8px;
  text-decoration: none; color: var(--ink-700); counter-increment: s;
  flex: 1 1 auto; min-width: 0; }
.oc-section-item:hover { background: #f8fafc; }
.oc-section-item.is-active { background: #eef2ff; color: #3730a3; font-weight: 600; }
/* The client is waiting on a reply here specifically — the same amber the "Awaiting reply" badge already
   uses elsewhere (badge-warning, _CommentThread.cshtml), so the row's own background says the same thing
   the badge inside it says, not a second, uncoordinated color. Active still wins when both apply — you
   are already looking at the row the tint would be pointing you toward. */
.oc-section-item.has-open-feedback { background: var(--warning-subtle); }
.oc-section-item.has-open-feedback:hover { background: color-mix(in srgb, var(--warning-subtle) 80%, black 4%); }
.oc-section-item.has-open-feedback.is-active { background: #eef2ff; color: #3730a3; }
/* Same mark on the editor pane's own card — the section this belongs to, opened. A thin amber border
   rather than a filled background: the card is the full-height working surface, not a compact list row,
   and filling it amber would fight the form fields sitting inside it all day. */
.oc-card.has-open-feedback { border-color: var(--warning); }
/* Right-aligned and tabular so 9 and 10 sit under each other instead of stepping sideways. */
.oc-section-item::before { content: counter(s); font-size: .7rem; color: var(--ink-400);
  font-variant-numeric: tabular-nums; text-align: right; }
/* One line, ellipsis if it genuinely will not fit — never a broken word. overflow-wrap: anywhere was
   here briefly and was plainly wrong for a title: it gave the browser permission to break "Executive"
   into "Execut/ive" the moment the column was tight. That property is for pasted comment text, where an
   unbroken 400-character string has no other escape; a section name would rather be truncated with its
   full text on hover. */
.oc-section-name {
    min-width: 0; font-size: .875rem; line-height: 1.35;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* The comment badge sits inside this cell, so it must not be squeezed by the ellipsis rules above. */
.oc-section-name .badge { margin-left: .35rem; vertical-align: baseline; }
/* Right-aligned in its fixed column so the type labels end on one edge and the ticks beside them form a
   straight line. Truncates rather than widening — the column width is what holds the alignment, and a
   longer type name must not be allowed to buy itself more room. */
/* Retained for any other caller; the section list no longer renders it — see Builder.cshtml. */
.oc-section-kind { font-size: .6rem; color: var(--ink-400); text-transform: uppercase; letter-spacing: .03em;
  text-align: right; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.oc-section-req { color: #6366f1; font-weight: 700; }
/* ---- The rail's add/remove controls ---------------------------------------------------------------
   The rail lists EVERY section type: the ones on this document, then the ones that could be. The
   dropdown-plus-"Add section"-button that used to sit at the foot of it is gone — it asked you to name a
   section from a closed list before you could see whether it was already there.

   One control shape for both halves, so the pair reads as a single toggle: + adds, x removes, same size,
   same place on the row. Both carry a title= (Builder.cshtml), which is the desktop tooltip — a pointer
   has to rest on something for a browser to draw one, so it is desktop-only by nature rather than by a
   media query. */
.oc-section-toggle {
    display: inline-grid; place-items: center; flex: 0 0 auto;
    width: 1.7rem; height: 1.7rem; border: 0; border-radius: 7px;
    background: transparent; color: var(--ink-400); cursor: pointer;
    transition: background-color .15s ease, color .15s ease;
}
.oc-section-toggle:focus-visible { outline: 2px solid var(--brand-500); outline-offset: 1px; }
/* Removing is the destructive half and says so on approach, rather than only in the dialog it opens. */
.oc-section-toggle.is-on:hover,
.oc-section-toggle.is-on:focus-visible { background: var(--danger-subtle); color: var(--danger); }

.oc-section-add { padding: .35rem; border-top: 1px solid var(--line-2); }
.oc-section-add-head { padding: .5rem .65rem .35rem; font-size: .68rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .08em; color: var(--ink-400); }
.oc-section-add-list { list-style: none; margin: 0; padding: 0; }
/* The WHOLE row submits, not just the +. The icon is the affordance; the row is the target — which is
   what makes this usable on a phone, where a 1.7rem icon is not something anyone can reliably hit. */
.oc-section-add-row {
    display: grid; grid-template-columns: minmax(0, 1fr) 1.7rem; align-items: center; gap: .6rem;
    width: 100%; padding: .5rem .65rem; border: 0; border-radius: 8px; background: transparent;
    color: var(--ink-500); font: inherit; text-align: left; cursor: pointer;
}
.oc-section-add-row:hover { background: #f8fafc; color: var(--ink-700); }
.oc-section-add-row:hover .oc-section-toggle,
.oc-section-add-row:focus-visible .oc-section-toggle { background: #e0e7ff; color: var(--brand-600); }
.oc-section-add-row:focus-visible { outline: 2px solid var(--brand-500); outline-offset: -2px; }
.oc-editor-pane { min-width: 0; }
/* The card head now carries only two things: the section's description (left) and Delete section
   (right) — the "Section name" field that used to live here, sized taller than either, moved into the
   body below, so a plain vertical CENTER is what lines a short paragraph up with a button correctly. */
.oc-editor-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
.oc-editor-head .oc-section-guide { margin: 0; }
/* margin-left:auto rather than relying on the head's space-between: nothing else sits in this row now,
   but the rule still holds if that ever changes — the button stays pinned to the edge regardless. */
.oc-editor-delete { margin-left: auto; flex: 0 0 auto; }

/* ---- The rail on a phone ---------------------------------------------------------------------------
   THE BUG THIS FIXES. Below 60rem .oc-editor drops to one column, so the rail stops being a sidebar and
   stacks ON TOP of the editor — and .oc-section-list was allowed to be 40rem tall. On a 667px screen the
   sections list was therefore the entire viewport and the form began below the fold: the editor was not
   cramped on a phone, it was unreachable.

   The list is the same list, re-laid as a wrap of chips inside one short scroll box. Every section is
   still there and still one tap away, in about a fifth of the height, and the form starts on the first
   screen where it belongs. */
@media (max-width: 60rem) {
    /* Sticky would pin a quarter of the viewport permanently once it is this wide. */
    .oc-editor-nav { position: static; max-height: 13rem; overflow-y: auto; }
    /* One scroll box for the whole rail (above), not two nested ones. */
    .oc-section-list { max-height: none; display: flex; flex-wrap: wrap; gap: .4rem; }
    /* Drag-to-reorder is a gesture over a vertical list; across a wrapped grid of chips it has nothing
       to mean, and the handle would only be taking width from the name. Reordering stays a desktop act —
       the arrow-key route on the handle was never a touch affordance either. */
    .oc-drag-handle { display: none; }
    /* ---- The rail's chips are gone; the stepper replaced them --------------------------------
       The chips answered "which sections exist" and left "how far through am I" unanswerable, which on a
       phone is the question. The stepper above answers it, and the "All" sheet carries the names, the
       ticks and Remove. What is left of the rail here is the one job neither of those does: ADDING a
       section. Hiding the whole aside would have made a section addable on a phone and never removable,
       and addable nowhere at all. */
    .oc-editor-nav { position: static; max-height: none; overflow: visible; border: 0; }
    .oc-editor-nav-head,
    .oc-section-list { display: none; }
    /* padding:0 here put the heading and the chips flush against .oc-editor-nav's own rounded surface —
       which it keeps (only its border was dropped) — so "NOT ADDED YET" was clipped by the corner it sat
       in. The panel needs its own inset on all four sides; the heading then needs none of its own. */
    .oc-section-add { border-top: 0; padding: .85rem; }
    .oc-section-add-head { padding: 0 0 .5rem; }
    .oc-section-add-list { display: flex; flex-wrap: wrap; gap: .4rem; }
    .oc-section-add-row {
        width: auto; border: 1px dashed var(--line-2); border-radius: 999px;
        grid-template-columns: minmax(0, 1fr) 2rem; gap: .35rem;
        padding: .1rem .15rem .1rem .7rem;
    }
    .oc-section-toggle { width: 2rem; height: 2rem; border-radius: 999px; }

    /* ---- One surface, not four ----------------------------------------------------------------
       THE BUG THIS FIXES. The builder nests a card inside a card: .oc-builder-card wraps
       .oc-editor (itself an .oc-card-body) wraps .oc-editor-pane wraps the section form (another
       .oc-card) wraps its own .oc-card-body. Each of those adds 1.25rem a side, on top of .oc-main's
       own 1rem gutter — about 115px of walls on a 390px screen, leaving the fields around 240px wide
       and drawing three concentric rounded borders down the left edge to prove it.

       On a phone none of that separation is doing any work: there is only one thing on screen. The
       walls come down and everything lines up on .oc-main's single gutter, which is what a gutter is
       for. Desktop is untouched — there the nesting is what separates the rail from the editor. */
    .oc-builder-card {
        border: 0; border-radius: 0; background: transparent; box-shadow: none;
    }
    .oc-builder-card > .oc-card-head,
    .oc-builder-card > .oc-card-body,
    .oc-builder-card > .oc-editor { padding-inline: 0; }

    .oc-editor-pane > .oc-card > div > .oc-card-head,
    .oc-editor-pane > .oc-card > div > .oc-card-body { padding-inline: 0; }
    /* And .oc-form's own 1.5rem. The section editor is BOTH (.oc-card .oc-form), so zeroing the card
       body alone left this one still standing — which is why the fields sat a step inside the Sections
       rail above them instead of lining up with it. */
    .oc-editor-pane > .oc-form { padding-inline: 0; }
    /* ---- The top band above the section description ---------------------------------------------
       Three containers each contributed a top padding, one inside the next: .oc-editor (a card body,
       1.25rem), the section form (.oc-form, 1.5rem) and the form's own head. .oc-editor's own is still
       zeroed — that gap really was empty. The form itself is a plain card on a phone (Option A, the
       "Mobile flow" block at the end of this file) and keeps its own padding; its card-head keeps a top
       padding of zero so it doesn't add a SECOND gap on top of that. */
    .oc-editor.oc-card-body { padding-block-start: 0; }
    .oc-editor-pane > .oc-card > div > .oc-card-head { padding-block-start: 0; }
    /* .oc-form > .oc-card-head:first-child pulls a head out to the form's padding edge with a negative
       margin. With no inline padding left there is nothing to pull it out to, and the margin would drag
       it off the screen. (It does not currently apply here — the builder's head is inside a wrapper div,
       not a first child — but it is one refactor away from applying, and silently.) */
    .oc-editor-pane > .oc-form > .oc-card-head:first-child { margin-inline: 0; }
    /* The head's rule is what separated it from the body; with no card around it, it is the only thing
       still saying where the guidance ends and the fields begin. */
    .oc-editor-pane > .oc-card > div > .oc-card-head { padding-top: 0; }

    /* New agreement's Shared information panel (Create.cshtml, Task 10): Back + a second "Start drafting"
       only make sense as a mobile-only pair, once that panel is its own separate screen reached via
       Continue. Desktop shows this panel inline beside the picker, always visible together, where the
       picker's own "Start drafting" button is already on screen and there is nothing to navigate "back"
       from — showing this row there duplicated it. */
    .oc-shared-panel-actions { display: flex; }

    /* The rail stacks ABOVE the editor at one column, which was right when it was the navigation and is
       wrong now that it is only "what else could I add". That is the thought that comes after finishing a
       section, not before starting one, so it goes last. */
    .oc-editor { gap: 1rem; }
    .oc-editor-pane { order: 1; }
    .oc-editor-nav { order: 2; }

    /* ---- One panel at a time --------------------------------------------------------------------
       .oc-tabbed is added by the script, so with JavaScript off or broken both panels stay visible and
       this page is the stacked page it was before — a tab strip that cannot switch would otherwise hide
       half the builder permanently. */
    /* The phone rules just below size tabs for the BUILDER's strip only (.oc-tabstrip). They were bare .oc-tab
       and squeezed every other tab row to zero width once octa-core's CSS layering let this unlayered rule beat
       the tabs' own shrink-0 (client page, Documents, Deliverables). */
    .oc-tabstrip {
        display: flex; gap: .25rem;
        margin-bottom: .9rem; border-bottom: 1px solid var(--line-2);
    }
    .oc-tabstrip .oc-tab {
        flex: 1 1 0; min-width: 0;
        padding: .65rem .5rem; border: 0; border-bottom: 2px solid transparent;
        background: transparent; color: var(--ink-500);
        font: inherit; font-size: .85rem; font-weight: 600;
        cursor: pointer;
    }
    .oc-tabstrip .oc-tab[aria-selected="true"] { color: var(--brand-600); border-bottom-color: var(--brand-500); }
    .oc-tabstrip .oc-tab:focus-visible { outline: 2px solid var(--brand-500); outline-offset: -2px; }

    .oc-tabbed > [data-oc-tab-panel]:not(.is-active),
    .oc-tabbed > .oc-agreement-main > [data-oc-tab-panel]:not(.is-active) { display: none; }

    /* The panels are new wrappers between the card and what it used to hold directly, so the rules above
       that zero this card's own padding have to reach through one more level. */
    .oc-builder-card > [data-oc-tab-panel] > .oc-card-body,
    .oc-builder-card > [data-oc-tab-panel] > .oc-editor,
    .oc-builder-card > [data-oc-tab-panel] > #builder_editor > .oc-editor { padding-inline: 0; }

    /* ---- The stepper -----------------------------------------------------------------------------
       Option A: a sideways-scrolling row of numbered circles. THE FADE IS THE FIX: a strip of nineteen
       circles cut off at the screen edge read as "six sections", so the right edge (and the left, once
       scrolled) fades out via a mask — the half-visible circle says "there is more this way". Pure CSS:
       scroll-driven animation flips the mask's edges as the strip moves, and a browser without it keeps
       the right-hand fade, which is the one that matters on first sight. */
    .oc-sectionsteps-bar {
        display: flex; align-items: center; gap: .5rem;
        margin-bottom: .6rem;
    }
    .oc-sectionsteps-strip {
        position: relative; /* offsetParent for proposal-section-nav.js's scroll-into-view */
        flex: 1 1 0; min-width: 0;
        display: flex; gap: .45rem; padding: .25rem .15rem;
        overflow-x: auto; scrollbar-width: none; scroll-snap-type: x proximity;
        --fade-l: 0rem; --fade-r: 2rem;
        mask-image: linear-gradient(to right, transparent, #000 var(--fade-l), #000 calc(100% - var(--fade-r)), transparent);
    }
    .oc-sectionsteps-strip::-webkit-scrollbar { display: none; }
    @supports (animation-timeline: scroll()) {
        /* @property/@keyframes oc-strip-fade are top level, just below this media block. */
        .oc-sectionsteps-strip { animation: oc-strip-fade linear both; animation-timeline: scroll(self inline); }
    }
    .oc-sectionstep {
        flex: 0 0 auto; scroll-snap-align: center;
        display: flex; align-items: center; justify-content: center;
        width: 2.1rem; height: 2.1rem; border-radius: 999px;
        border: 1.5px solid var(--line-2); background: var(--surface);
        font-size: .8rem; font-weight: 700; font-variant-numeric: tabular-nums;
        color: var(--ink-500); text-decoration: none;
    }
    .oc-sectionstep.is-done { border-color: var(--success); color: var(--success); }
    .oc-sectionstep.is-current {
        border-color: var(--brand-500); color: var(--brand-600);
        box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand-500) 18%, transparent);
    }
    .oc-sectionstep:focus-visible { outline: 2px solid var(--brand-500); outline-offset: 2px; }
    .oc-sectionsteps-all { flex: 0 0 auto; }

    /* Which section this is, spelled out — the stepper gives the position, this gives the name. */
    .oc-sectionstep-title { display: block; margin-bottom: .75rem; }
    .oc-sectionstep-title .oc-sectionstep-count {
        display: block; font-size: .7rem; font-weight: 700; letter-spacing: .06em;
        text-transform: uppercase; color: var(--ink-400);
    }
    .oc-sectionstep-title strong { font-size: 1.02rem; line-height: 1.3; color: var(--ink-900); }

    /* ---- Previous / Next ------------------------------------------------------------------------
       Pinned, so moving on never means scrolling back past the form you just filled in. position:sticky
       rather than fixed: it stays inside the editor's own column and cannot end up floating over the
       details panel or the page below. */
    .oc-sectionstep-nav {
        display: flex; justify-content: space-between; align-items: center; gap: .75rem;
        position: sticky; bottom: 0; z-index: 20;
        margin-top: 1rem; padding: .7rem 0 max(.7rem, env(safe-area-inset-bottom));
        background: linear-gradient(to top, var(--surface) 72%, transparent);
        border-top: 1px solid var(--line-2);
    }
    .oc-sectionstep-nav .btn { min-width: 7.5rem; justify-content: center; }
}
/* The mobile stepper's edge fade (above). @property must sit at top level, not inside @media/@supports. */
@property --fade-l { syntax: "<length>"; inherits: false; initial-value: 0rem; }
@property --fade-r { syntax: "<length>"; inherits: false; initial-value: 2rem; }
@keyframes oc-strip-fade {
    0% { --fade-l: 0rem; --fade-r: 2rem; }
    8%, 92% { --fade-l: 2rem; --fade-r: 2rem; }
    100% { --fade-l: 2rem; --fade-r: 0rem; }
}

/* ---- The "All sections" sheet ----------------------------------------------------------------------
   Names and state together, one tap away, without standing between the person and the form for the rest
   of the time. Rendered on every width (the sheet itself only opens from a mobile control), so these are
   not inside the media query. */
.oc-sheet-sections { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .15rem; }
/* Each row is a link AND a remove button, siblings rather than one inside the other — a <button> inside an
   <a> is invalid HTML as well as ambiguous to press, the same reasoning as the rail's own rows. The <li>
   therefore has to be the row: left alone it is a plain block, so the link (a full-width grid) and the
   button stacked, which put every x on a line of its own under the section it belonged to. */
.oc-sheet-sections > li { display: flex; align-items: center; gap: .25rem; }
.oc-sheet-section {
    flex: 1 1 auto; min-width: 0;
    display: grid; grid-template-columns: 1.6rem minmax(0, 1fr) 1.35rem;
    align-items: center; gap: .65rem;
    padding: .7rem .5rem; border-radius: 10px;
    color: var(--ink-700); text-decoration: none;
}
.oc-sheet-section:hover { background: #f8fafc; }
.oc-sheet-section.is-current { background: #eef2ff; color: #3730a3; font-weight: 600; }
.oc-sheet-section-n {
    font-size: .72rem; font-weight: 700; color: var(--ink-400);
    font-variant-numeric: tabular-nums; text-align: right;
}
.oc-sheet-section-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* "All sections" is a phone control. Widening past 60rem with it open used to play the sheet's 0.3s
   slide-down at full desktop width, and the page's scroll lock only lifted when that ended — so the
   scrollbar popped in late and the whole builder shifted. At desktop width it is simply gone at once, and
   so is its lock; proposal-section-nav.js closes it properly underneath (state, focus) with nothing
   left on screen to animate. */
@media (min-width: 60.0625rem) {
    #sections_sheet { display: none !important; }
    html.oc-scroll-lock:has(#sections_sheet.is-open), html.oc-scroll-lock:has(#sections_sheet.is-closing) {
        overflow: visible; overflow-x: clip;
    }
}
/* Agreement Builder, phone: the sheet's spacing was lopsided — a tall gap under the title (pb-4) against
   a .5rem strip at the foot, so the last row sat on the screen edge while the top looked padded. Even
   padding top and bottom, a divider under the header, a height that leaves the page visible above it,
   and one scrolling list in between. Layout only; the sheet primitive (_BottomSheet) is untouched. */
@media (max-width: 60rem) {
    .oc-builder-card:has([data-oc-kind="agreement"]) #sections_sheet .oc-sheet-panel {
        max-height: 78dvh; border-radius: 1.25rem 1.25rem 0 0;
    }
    .oc-builder-card:has([data-oc-kind="agreement"]) #sections_sheet .oc-sheet-handle { padding: .6rem 0 .35rem; }
    .oc-builder-card:has([data-oc-kind="agreement"]) #sections_sheet .oc-sheet-header {
        align-items: center; padding: .35rem 1rem .85rem; border-bottom: 1px solid var(--line-2);
    }
    .oc-builder-card:has([data-oc-kind="agreement"]) #sections_sheet .oc-sheet-body {
        padding: .6rem .75rem 1rem;
    }
    .oc-builder-card:has([data-oc-kind="agreement"]) #sections_sheet .oc-sheet-sections { gap: .1rem; }
    .oc-builder-card:has([data-oc-kind="agreement"]) #sections_sheet .oc-sheet-section {
        min-height: 2.25rem; padding: .4rem .6rem; gap: .6rem;
        grid-template-columns: 1.25rem minmax(0, 1fr) 1.1rem; font-size: .875rem;
    }
    .oc-builder-card:has([data-oc-kind="agreement"]) #sections_sheet .oc-sheet-section-n { font-size: .68rem; }
    .oc-builder-card:has([data-oc-kind="agreement"]) #sections_sheet .oc-sheet-section .oc-section-state {
        width: 1.1rem; height: 1.1rem;
    }
    .oc-builder-card:has([data-oc-kind="agreement"]) #sections_sheet .oc-sheet-sections .oc-section-toggle {
        flex: 0 0 auto; width: 1.85rem; height: 1.85rem;
    }
    .oc-builder-card:has([data-oc-kind="agreement"]) #sections_sheet .oc-sheet-header h3 { font-size: .95rem; }
    /* Drag grip (proposal-section-reorder.js): touch-action none so a finger on it drags the row instead
       of scrolling the list; the row being moved lifts. */
    #sections_sheet .oc-sheet-drag {
        display: inline-flex; flex: 0 0 auto; align-items: center; justify-content: center;
        width: 1.6rem; height: 2.25rem; padding: 0; border: 0; background: transparent;
        color: var(--ink-400); cursor: grab; touch-action: none;
    }
    #sections_sheet .oc-sheet-drag:active { cursor: grabbing; }
    #sections_sheet [data-oc-sheet-reorder] > li.is-dragging {
        position: relative; z-index: 1; border-radius: 10px; background: var(--surface);
        box-shadow: 0 6px 18px rgba(15, 23, 42, .16);
    }
}
.oc-editor-title { flex: 1; margin: 0; }
/* margin-left:auto on the submit button rather than relying on this row's own space-between — the "Part
   of the template" note beside it (Builder.cshtml) only renders for a template-seeded section, and
   space-between with that note ABSENT (any manually-added section) left the lone button at the start of
   the row instead of the end. Same fix, same reasoning, as .oc-editor-delete just above. */
.oc-editor-actions { display: flex; align-items: center; gap: 1rem; }
.oc-editor-actions .btn[type="submit"] { margin-left: auto; }
/* Section name + its type badge (+ "Awaiting reply", when present), now the first thing IN the body
   rather than sharing the head with Delete. wrap because the badges need room next to a full-width name
   field on a narrow screen, not underneath it mid-word. */
.oc-editor-title-row { display: flex; align-items: center; gap: .75rem; flex-wrap: wrap; margin-bottom: 1.25rem; }
.oc-editor-title-row .oc-editor-title { flex: 1 1 16rem; }

.oc-row { display: grid; grid-template-columns: 1fr 1fr; gap: .5rem; margin-bottom: .5rem; align-items: center; }
.oc-row-3 { grid-template-columns: 1fr 1fr auto; }
.oc-row-4 { grid-template-columns: 1.4fr .6fr .8fr .8fr; }
.oc-row-price { grid-template-columns: 2fr .7fr .9fr .7fr; }
.oc-row-num { font-size: .8rem; font-weight: 700; color: #c7d2fe; font-variant-numeric: tabular-nums; }
/* SOW.MD §4's "FR-01, FR-02…": the prefix lives in CSS because proposal-repeat.js rewrites the number
   text on every add/remove, and would drop a prefix written into it. */
.oc-row-num-fr::before { content: "FR-"; }
.oc-row-indent { margin-left: 1rem; }

/* proposal-repeat.js appends a delete button to every row it decorates, as one more grid child — every
   .oc-row variant above sizes its columns exactly to its own fields, with no spare track for it. Scoped
   to rows INSIDE a repeating list (not Architecture's plain .oc-row/.oc-row-3, which get no button and
   must keep their original columns) via specificity rather than load order: [data-oc-repeat] > .oc-row
   is (0,2,0), stronger than either .oc-row-3 rule above regardless of which comes later in the file. */
[data-oc-repeat] > .oc-row { grid-template-columns: 1fr 1fr auto; }
[data-oc-repeat] > .oc-row-3 { grid-template-columns: 1fr 1fr auto auto; }
[data-oc-repeat] > .oc-row-4 { grid-template-columns: 1.4fr .6fr .8fr .8fr auto; }

/* A row that OPENS with .oc-row-num (Objectives, Next Steps) is a different shape from the rest: the
   rules above give the number its own 1fr track, same width as a real field, which is how "1" ended up
   a field's width away from the box it numbers. auto sizes the number to its own digits instead — "1"
   and Next Steps' zero-padded "01" both want a different width, and auto asks nothing of this file to
   know which. Higher specificity (0,3,0) than the plain .oc-row/.oc-row-3 rules above (0,2,0) is what
   lets this win regardless of source order, per the same reasoning in the comment above. */
[data-oc-repeat] > .oc-row:has(> .oc-row-num) { grid-template-columns: auto 1fr auto; }
[data-oc-repeat] > .oc-row-3:has(> .oc-row-num) { grid-template-columns: auto 1fr 1fr 1fr auto; }
.oc-row-remove { padding: .35rem .5rem; cursor: pointer; }
/* SOW.MD's tables (Integrations, Timeline): the heading row sits OUTSIDE the repeating list, so it never
   gets the delete button the rows do — with auto tracks the columns sized differently and each heading
   drifted off its field. Equal fixed tracks plus a reserved delete-button slot in BOTH keep them lined up. */
.oc-row.oc-row-table-2, [data-oc-repeat] > .oc-row.oc-row-table-2 { grid-template-columns: 1fr 1fr 2.5rem; }
.oc-row.oc-row-table-3, [data-oc-repeat] > .oc-row.oc-row-table-3 { grid-template-columns: 1fr 1fr 1fr 2.5rem; }
.oc-row-head.oc-row-table-2 > span, .oc-row-head.oc-row-table-3 > span { padding-left: .15rem; }
/* A template's labelled fields (Support, Data, Testing…) as a deletable list: the field, then its delete. */
[data-oc-repeat] > .oc-row.oc-row-field { grid-template-columns: 1fr 2.5rem; align-items: end; }
.oc-row-field > .field { margin-bottom: 0; }
/* Present but nothing to act on: the last row in a list, kept alive as "Add another"'s clone source.
   Same convention as the last-contact delete in _TabContacts.cshtml, and for the identical reason — a
   real disabled attribute would swallow the tooltip explaining why, along with the click. */
.oc-row-remove.is-unavailable { opacity: .4; cursor: not-allowed; }
.oc-subcard { border: 1px solid var(--line); border-radius: 10px; padding: .8rem; margin-bottom: .7rem; background: #fcfcfd; }
/* ---- Repeating editor rows on a narrow screen ------------------------------------------------------
   THE BUG THIS FIXES. A one-line version of this lived here and did nothing for the rows that needed it.
   It selected `.oc-row-3` (0,1,0), while every row inside a repeating list is also matched by
   `[data-oc-repeat] > .oc-row-3` (0,2,0) a few rules above — and a media query adds no specificity of its
   own, so the three-column grid won at every width. On a phone that left Findings, Metrics, Timeline and
   the payment instalments as three or four columns in about 300px: boxes roughly one character wide,
   where the placeholder was a single letter and what you typed was invisible.

   So the mobile rules have to be written against the SAME selectors, and placed after them.

   60rem, not 48rem: that is where .oc-editor drops the builder to one column, which is exactly where the
   editor pane is narrowest and these rows are tightest. */
@media (max-width: 60rem) {
    .oc-row, .oc-row-3, .oc-row-4, .oc-row-price,
    [data-oc-repeat] > .oc-row,
    [data-oc-repeat] > .oc-row-3,
    [data-oc-repeat] > .oc-row-4,
    [data-oc-repeat] > .oc-row-price,
    [data-oc-repeat] > .oc-row:has(> .oc-row-num),
    [data-oc-repeat] > .oc-row-3:has(> .oc-row-num),
    .oc-row.oc-row-table-2, [data-oc-repeat] > .oc-row.oc-row-table-2,
    .oc-row.oc-row-table-3, [data-oc-repeat] > .oc-row.oc-row-table-3 {
        grid-template-columns: 1fr;
    }
    /* Stacked, a column heading no longer sits over its field — the placeholders say what each is. */
    .oc-row-head.oc-row-table-2, .oc-row-head.oc-row-table-3 { display: none; }

    /* Stacked, one row runs straight into the next — six identical boxes with no way to see which three
       belong together. Each row becomes its own bordered block, which is the only thing still saying
       "these fields are one finding". */
    [data-oc-repeat] > .oc-row {
        gap: .45rem; padding: .7rem; border: 1px solid var(--line-2); border-radius: 10px;
    }
    /* Two children that must NOT become full-width bands of their own: the row's number, which is two
       digits, and the delete button proposal-repeat.js appends, which belongs at the row's end. */
    [data-oc-repeat] > .oc-row > .oc-row-num { justify-self: start; }
    [data-oc-repeat] > .oc-row > .oc-row-remove { justify-self: end; }

    /* The Builder (agreements, then proposals too) and New agreement's shared box, by request: no 1/2/3
       number, and a one-field row is just the field with its delete icon on the same line — no box
       around the row, no tinted tile behind the icon. Rows with several fields keep their block, minus
       the number and the tile. */
    :is(.oc-builder-card, #shared_agreement_fields) [data-oc-repeat] > .oc-row > .oc-row-num { display: none; }
    :is(.oc-builder-card, #shared_agreement_fields)
        [data-oc-repeat] > .oc-row:has(> .field-input):not(:has(> .field-input ~ .field-input)) {
        grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: .25rem;
        padding: 0; border: 0; border-radius: 0;
    }
    /* The rich-text toolbar on EVERY editor (_RichTextEditor), by request — first made for the Builder and
       New agreement's shared box, now app-wide so a new editor is compact on a phone without anyone
       remembering it (ui-patterns.md): ten phone-sized touch buttons wrapped onto two tall rows. One
       compact row instead — smaller icons, tighter gaps, and buttons that share the row's width equally
       (up to 2rem each), so all ten fit on one line even on a narrow phone. */
    .oc-richtext-toolbar {
        flex-wrap: nowrap; justify-content: space-between; gap: .05rem; padding: .25rem .3rem;
    }
    .oc-richtext-toolbar .btn.oc-icon-btn {
        flex: 1 1 0; width: auto; max-width: 2rem; height: 1.85rem; min-height: 0; min-width: 0; padding: 0;
        font-size: .78rem; justify-content: center;
    }
    .oc-richtext-toolbar .btn.oc-icon-btn i {
        width: .85rem; height: .85rem; font-size: .8rem;
    }
    .oc-richtext-toolbar > span[aria-hidden] {
        flex: 0 0 auto; height: 1rem; margin-inline: .15rem;
    }
    /* A labelled field row (FieldGroup: heading + resizable text box, e.g. SOW's Data / Environments):
       the delete icon sat in its own column beside the box, taking width from the text. By request it
       now sits at the right end of the field's HEADING line, and the box gets the full width. */
    :is(.oc-builder-card, #shared_agreement_fields) [data-oc-repeat] > .oc-row.oc-row-field {
        display: block; position: relative; padding: 0; border: 0; border-radius: 0;
    }
    :is(.oc-builder-card, #shared_agreement_fields) [data-oc-repeat] > .oc-row.oc-row-field > .field > .field-label {
        padding-right: 2.5rem;
    }
    :is(.oc-builder-card, #shared_agreement_fields) [data-oc-repeat] > .oc-row.oc-row-field > .oc-row-remove {
        position: absolute; top: -.45rem; right: 0;
    }
    :is(.oc-builder-card, #shared_agreement_fields) [data-oc-repeat] > .oc-row > .oc-row-remove,
    :is(.oc-builder-card, #shared_agreement_fields) [data-oc-repeat] > .oc-row > .oc-row-remove:hover {
        background: transparent; border: 0; box-shadow: none;
        width: 2.25rem; height: 2.25rem; min-height: 0; padding: 0; justify-content: center;
    }

    /* Column headers describe columns that no longer exist. Every input in these rows carries its own
       placeholder (Findings, Metrics, Timeline, instalments — checked), so the heading is the only thing
       lost, and a "WHEN / PHASE / WHAT HAPPENS" strip above a single-column stack is worse than nothing.
       Scoped to .oc-row.oc-row-head: the bare .oc-row-head is a section heading used elsewhere ("Who are
       they?", "Basic info") and must stay. */
    .oc-row.oc-row-head { display: none; }

    /* An indent that was reading as a sub-item beside its parent has nothing left to sit beside. */
    .oc-row-indent { margin-left: 0; }
}

.oc-totals { margin-top: 1rem; border-top: 1px solid var(--line); padding-top: .7rem; display: grid; gap: .3rem; max-width: 18rem; }
.oc-totals div { display: flex; justify-content: space-between; font-size: .85rem; color: var(--ink-500); }
.oc-totals-grand { border-top: 1px solid #cbd5e1; padding-top: .4rem; margin-top: .2rem; font-size: 1rem !important; color: var(--ink-900) !important; font-weight: 700; }
.oc-alert-list { margin: .35rem 0 0 1.1rem; padding: 0; font-size: .875rem; }

/* ---- Dismissible banners (oc-dismissible.js) -------------------------------------------------------
   .oc-alert is a flex row of icon + text. The close button is appended as a third child and must not be
   allowed to share the text's growth, or it stretches across whatever width is left. It inherits the
   alert's own colour rather than picking one: there are four alert hues and a grey x in an amber banner
   reads as disabled. */
.oc-alert > .oc-dismiss {
    flex: 0 0 auto; align-self: center; margin-left: auto;
    color: inherit; opacity: .65;
}
.oc-alert > .oc-dismiss:hover,
.oc-alert > .oc-dismiss:focus-visible {
    opacity: 1; color: inherit;
    background: color-mix(in srgb, currentColor 12%, transparent);
}
/* [hidden] is a UA rule (display:none) at specificity (0,1,0), and .oc-alert sets display:flex at the
   same weight — whichever loads last wins, which is not a thing to leave to chance. The same collision
   hid nothing when the signature preview was given [hidden] earlier. */
.oc-alert[hidden] { display: none !important; }
/* Checkbox sitting inside an editor row: keep it and its label on one line, never overflowing the cell. */
.oc-check-inline { align-items: center; white-space: nowrap; font-size: .8rem; color: var(--ink-600); }

/*
    A checkbox whose label EXPLAINS something, rather than naming it: a bold line plus a sentence or two
    underneath. The base rule above is right for its own job (a short label pinned on one line inside a
    tight editor cell) and wrong for this one — nowrap has nowhere to break a full sentence, so it runs
    straight out past the card's right edge. That is the overflow this fixes.

    THIRD time this trap has been patched: .oc-delete-scope and .oc-settings .oc-check-inline each carry
    their own copy of the same four declarations. This is the shared modifier both of those could fold
    into, so a fourth explanatory checkbox has a class to reach for instead of a fourth private reset.

    Everything here is required, and each line for its own reason:
      • display:flex   — a <label> is inline by default, so align-items would otherwise do nothing
      • white-space    — beats the base rule's nowrap (two classes vs one, so source order is irrelevant)
      • flex-start     — keeps the box beside the FIRST line once the text wraps to several
      • min-width: 0   — a flex item defaults to min-width:auto and refuses to shrink below its own
                         content width, which is a second, quieter way to get the same overflow
*/
.oc-check-inline.oc-check-block,
.oc-check-inline:has(> span > strong) {
    display: flex; gap: .6rem; align-items: flex-start; white-space: normal;
}
.oc-check-inline.oc-check-block > input[type="checkbox"],
.oc-check-inline:has(> span > strong) > input[type="checkbox"] { flex: none; margin-top: .2rem; }
.oc-check-inline.oc-check-block > span,
.oc-check-inline:has(> span > strong) > span { flex: 1 1 auto; min-width: 0; }
/* The name on its own line, the explanation under it — the two were running together as one long
   sentence, which is why the row needed the full width of the page in the first place. */
.oc-check-inline.oc-check-block > span > strong,
.oc-check-inline:has(> span > strong) > span > strong { display: block; color: var(--ink-700); }
.oc-check-inline.oc-check-block > span > .oc-muted,
.oc-check-inline:has(> span > strong) > span > .oc-muted { display: block; margin-top: .15rem; line-height: 1.5; }

/*
    WHY :has() as well as the class. A label wrapping a <strong> IS this pattern — the bold line is the
    setting's name and whatever follows it is the explanation. Twelve labels across Admin and the Portal
    are already written that way against the bare .oc-check-inline, each one an overflow waiting for a
    narrow enough card (the Portal's contract-consent label is 49 words). Matching the shape fixes all of
    them, and the next one someone writes, without twelve edits and a thirteenth omission.

    The explicit class stays for the case :has() cannot see: an explanatory label with no <strong> in it.
*/
.oc-row-3 { grid-template-columns: 1fr 1fr minmax(8rem, max-content); }
/* Column headers above a repeating editor row — tells staff what each unlabelled input is for. */
.oc-row-head { margin-bottom: .3rem; }
/* Basic info's own row-head, carrying the same .oc-autosave status the section editor shows — a plain
   .oc-row-head elsewhere has nothing to right-align against, so this is a separate modifier rather than
   changing the bare class every other "Basic info"/"Who are they?" heading also uses. */
.oc-row-head-with-status { display: flex; align-items: center; justify-content: space-between; gap: .75rem; }
.oc-row-head span { font-size: .7rem; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; color: var(--ink-400); }
/* Collapsible proposal details on the builder — opens automatically when send validation complains. */
/* .oc-check-inline is display:inline (a <label>'s browser default) by design — its usual job is a single
   checkbox sitting inline beside its own text. Left alone here, that meant several independent settings
   rows wrapped like words in a paragraph: two short ones sharing a line, a longer one bumped to its own —
   layout by how much each label's text happened to fit, not by "one setting per row". Each row is a flex
   item now, which forces the row regardless of the label's own inline nature. */
.oc-settings { padding: 0; display: flex; flex-direction: column; gap: .75rem; }
/* .oc-check-inline's own white-space:nowrap is right for its usual short editor-row label, wrong for a
   settings row whose text is a full sentence — on a narrow viewport that sentence had nowhere to wrap and
   ran the label past the card edge instead. Descendant selector, so it applies only inside a settings
   list, not the editor-row usage nowrap exists for. Made a real flex row (the base rule never is one, so
   align-items alone would do nothing) so the checkbox stays pinned top-left of however many lines the
   text wraps to, rather than the two drifting apart once the text is no longer one unbreakable run. */
.oc-settings .oc-check-inline {
  display: flex; gap: .6rem; white-space: normal; align-items: flex-start;
}
.oc-settings .oc-check-inline input[type="checkbox"] { flex: none; margin-top: .2rem; }
.oc-settings .oc-check-inline > span { flex: 1 1 auto; min-width: 0; }
.oc-settings-summary { display: flex; align-items: center; gap: .5rem; padding: .9rem 1.15rem; cursor: pointer;
  font-weight: 600; font-size: .9rem; color: var(--ink-700); list-style: none; }
.oc-settings-summary::-webkit-details-marker { display: none; }
.oc-settings[open] .oc-settings-summary { border-bottom: 1px solid var(--line-2); }
/* Public link, inside the same Proposal-details form/card rather than a detached card — set off from the
   fields above it with a divider so it still reads as its own block within the one card. */
.oc-public-link { border-top: 1px solid var(--line-2); padding-top: 1rem; margin-top: .25rem; }
/* A heading INSIDE a form, naming the group of fields under it. It had no rule at all, so Tailwind's
   preflight — which strips every heading back to inherited size and weight — left "Contact" rendering
   as ordinary body text sitting oddly above a field label. The rule above it separates the group from
   the one before without needing a second card. */
.oc-section-title { margin: 1.75rem 0 .9rem; padding-top: 1.25rem; border-top: 1px solid var(--line-2);
  font-size: .95rem; font-weight: 700; color: var(--ink-900); letter-spacing: -.005em; }
.oc-section-title:first-child { margin-top: 0; padding-top: 0; border-top: 0; }

.oc-input-copy-group { display: flex; gap: .5rem; align-items: stretch; }
.oc-input-copy-group .field-input { flex: 1; min-width: 0; }
.oc-input-copy-group .btn { flex: 0 0 auto; white-space: nowrap; }
.oc-list-plain { list-style: none; padding: 0; margin: .4rem 0 0; display: grid; gap: .3rem; font-size: .9rem; color: var(--ink-700); }
.oc-danger-note { color: #b91c1c; font-weight: 600; }
/* Read-only event timeline (proposal activity: sent/delivered/accepted/declined…). A connecting line runs
   behind the dots so the sequence reads as one continuous trail, not a disconnected list. */
.oc-timeline { list-style: none; margin: 0; padding: 0; }
.oc-timeline-item { display: flex; gap: .8rem; position: relative; padding-bottom: 1.15rem; }
.oc-timeline-item:last-child { padding-bottom: 0; }
.oc-timeline-item:not(:last-child)::before {
  content: ''; position: absolute; left: .9rem; top: 1.85rem; bottom: 0; width: 1px; background: var(--line); }
.oc-timeline-dot { flex: 0 0 auto; width: 1.85rem; height: 1.85rem; border-radius: 999px;
  display: flex; align-items: center; justify-content: center; position: relative; z-index: 1; }
.oc-timeline-dot.is-success { background: #dcfce7; color: #16a34a; }
.oc-timeline-dot.is-danger { background: #fee2e2; color: #dc2626; }
.oc-timeline-dot.is-warning { background: #fef3c7; color: #b45309; }
.oc-timeline-dot.is-info { background: #dbeafe; color: var(--info); animation: oc-pulse-ring 2.4s ease-in-out infinite; }
.oc-timeline-dot.is-slate { background: var(--line-2); color: var(--ink-500); }

/* The one purposeful motion in this pair of components: "info" always means the currently-active stage,
   never a past or future one, so a soft breathing ring is real information (this is moving right now), not
   decoration — the same reasoning that lets the timeline show state at all. Box-shadow, not scale, so nothing
   crowds its neighbours; disabled outright under reduced motion rather than left at a slower speed. */
@keyframes oc-pulse-ring {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--info) 35%, transparent); }
  50%      { box-shadow: 0 0 0 6px color-mix(in srgb, var(--info) 0%, transparent); }
}
@media (prefers-reduced-motion: reduce) {
  .oc-timeline-dot.is-info { animation: none; }
}
.oc-timeline-body { flex: 1 1 auto; min-width: 0; padding-top: .2rem; }
.oc-timeline-head { display: flex; align-items: baseline; justify-content: space-between; gap: .75rem; flex-wrap: wrap; }
.oc-timeline-head strong { font-size: .875rem; color: #1e293b; font-weight: 600; }
.oc-timeline-time { font-size: .78rem; color: var(--ink-400); white-space: nowrap; }
.oc-timeline-detail { margin: .2rem 0 0; font-size: .85rem; color: var(--ink-600); line-height: 1.55; word-break: break-word; }

/* ---------- Engagement stepper (Proposal -> Contract -> Project -> Invoice) ----------
   The horizontal sibling of .oc-timeline: same four .oc-timeline-dot state tints, so a client learns the
   colour language once and reads it the same way whether it is a chronological log or a stage strip. Only
   the layout (row instead of column) and the connecting line (segments, not one continuous rail) differ. */
.oc-stepper { display: flex; align-items: flex-start; gap: 0; padding: 1.25rem 1rem; margin-bottom: 1.5rem; }
.oc-stepper-step { display: flex; flex-direction: column; align-items: center; flex: 1 1 0; min-width: 0; position: relative; }
.oc-stepper-step:not(:last-child)::after {
  content: ''; position: absolute; top: .9rem; left: calc(50% + 1.4rem); right: calc(-50% + 1.4rem);
  height: 2px; background: var(--line); z-index: 0;
}
.oc-stepper-step.is-completed:not(:last-child)::after { background: var(--success); }
.oc-stepper-dot { width: 1.85rem; height: 1.85rem; }
.oc-stepper-label { margin-top: .6rem; font-size: .8rem; font-weight: 600; color: var(--ink-700); text-align: center; }
.oc-stepper-sub { font-size: .72rem; color: var(--ink-400); text-align: center; margin-top: .1rem; }

@media (max-width: 640px) {
  .oc-stepper-label { font-size: .7rem; }
  .oc-stepper-sub { display: none; }
  .oc-stepper-step:not(:last-child)::after { left: calc(50% + 1.1rem); right: calc(-50% + 1.1rem); }
}

/* ---------- App shell (app-owned) ----------------------------------------------------------------------
   Restored deliberately. The layouts use this shell rather than octa-core's _LayoutSidebar contract: the
   theme leaves the sidebar's position/inset/width/z-index to runtime-compiled Tailwind utilities, which is
   unreliable on the browser CDN build this app uses — the drawer opened, the overlay dimmed, and the panel
   never appeared. A documented divergence, not drift; revisit if octa-core moves that geometry into
   octacer-theme.css. Colours are TOKENS, so dark mode works even though the markup is app-owned. */
:root { --sidebar-w: 248px; }
/* Admin sidebar 20% narrower than the shared 248px (by request). Admin only: the Portal keeps 248px. */
html[data-theme-scope="admin"] { --sidebar-w: 198px; }
/* ...and keep "Octacer Clients" on one line in that narrower sidebar. */
html[data-theme-scope="admin"] .oc-sidebar:not(.is-collapsed) { & .oc-brand-row { gap: 0; } & .oc-brand { padding-inline: .4rem; gap: .55rem; } & .oc-brand-name { white-space: nowrap; } }

/* Set by portal.js the moment a portal page enters the browser's back/forward cache, so the snapshot it
   keeps has nothing readable in it. Without this, pressing Back after signing out paints the old page —
   dashboard, flash messages and all — for the frame or two before the forced reload lands. */
.oc-restored-hidden body { visibility: hidden; }

/* ---------- App shell ---------- */
.oc-shell { display: flex; min-height: 100vh; }

.oc-sidebar {
    position: fixed; top: 0; bottom: 0; left: 0; width: var(--sidebar-w);
    background: var(--surface); backdrop-filter: blur(12px);
    border-right: 1px solid var(--line);
    display: flex; flex-direction: column; padding: 1.1rem .8rem; z-index: 40;
}
.oc-brand { display: flex; align-items: center; gap: .65rem; padding: .35rem .55rem 1.1rem; text-decoration: none; }
.oc-brand-badge { width: 2.1rem; height: 2.1rem; border-radius: .65rem; display: grid; place-items: center; color: var(--primary-foreground); background: linear-gradient(180deg, var(--brand-500), var(--brand-700)); box-shadow: 0 2px 6px -1px rgba(79,70,229,.5); }
.oc-brand-badge i { width: 18px; height: 18px; }
.oc-brand-name { font-weight: 800; font-size: 1.02rem; letter-spacing: -.02em; color: var(--ink-900); line-height: 1.1; }
.oc-brand-name small { display: block; font-size: .64rem; font-weight: 600; color: var(--ink-400); letter-spacing: .03em; text-transform: uppercase; }

.oc-nav { display: flex; flex-direction: column; gap: .12rem; overflow-y: auto; }
.oc-nav-label { font-size: .64rem; text-transform: uppercase; letter-spacing: .07em; color: var(--ink-400); font-weight: 700; padding: .8rem .6rem .3rem; }
.oc-nav-link { display: flex; align-items: center; gap: .7rem; padding: .55rem .65rem; border-radius: .6rem; font-size: .875rem; font-weight: 500; color: var(--ink-600); text-decoration: none; transition: background .15s, color .15s; }
.oc-nav-link:hover { background: var(--line-2); color: var(--ink-900); }
.oc-nav-active { background: var(--brand-50); color: var(--brand-700); font-weight: 600; }
.oc-nav-link i { width: 18px; height: 18px; flex: 0 0 auto; }

.oc-side-foot { margin-top: auto; border-top: 1px solid var(--line-2); padding-top: .8rem; display: flex; align-items: center; gap: .6rem; }
.oc-avatar { width: 2.1rem; height: 2.1rem; border-radius: 50%; background: var(--brand-100); color: var(--brand-700); display: grid; place-items: center; font-weight: 700; font-size: .8rem; flex: 0 0 auto; }
.oc-side-user { flex: 1; min-width: 0; }
.oc-side-user .n { font-size: .82rem; font-weight: 600; color: var(--ink-900); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.oc-side-user .r { font-size: .7rem; color: var(--ink-400); }

.oc-content { flex: 1; margin-left: var(--sidebar-w); min-width: 0; }
.oc-main { max-width: 1180px; margin: 0 auto; padding: 1.9rem 2rem 4rem; }
/* Admin only: the 1180px cap, centred, left a wide empty strip between the sidebar and every page on a
   large screen. Admin pages now fill the width beside the sidebar and keep only the 2rem gutter; the
   Portal keeps its centred column. */
html[data-theme-scope="admin"] .oc-main { max-width: none; }
/* …and the side gutter tighter (2rem → 1.35rem), desktop only — below 1024px the narrow rules own it. */
@media (min-width: 1024px) {
    html[data-theme-scope="admin"] .oc-main { padding-left: 1.35rem; padding-right: 1.35rem; }
}

/* ---------- Collapsible sidebar (portal-ux §63–66) -----------------------------------------------
   Behavior is the theme's: octacer-sidebar.js (loaded by _ThemeHead) owns the mobile drawer, the
   desktop collapse toggle and its localStorage preference, Escape/focus/inert handling. It drives
   .is-open / .is-collapsed on [data-oc-sidebar] and html.oc-sidebar-collapsed, and octacer-theme.css
   already sizes the rail (4.5rem) and slides the drawer. What is left to the app is mapping those
   states onto this shell's own nav classes (.oc-nav-link / .oc-brand / .oc-side-foot), which predate
   the theme's .oc-sidebar-nav markup. */
:root { --sidebar-rail: 4.5rem; }

.oc-brand-row { display: flex; align-items: flex-start; justify-content: space-between; gap: .4rem; }
.oc-brand-row .oc-brand { flex: 1; min-width: 0; }
/* The drawer's own close affordance: pointless on desktop, where the sidebar is always present. */
.oc-sidebar-close { display: none; flex: 0 0 auto; margin-top: .3rem; }

.oc-sidebar-collapse { display: flex; align-items: center; gap: .7rem; width: 100%; margin-top: .5rem;
  padding: .55rem .65rem; border: 0; border-radius: .6rem; background: none; cursor: pointer;
  font-size: .82rem; font-weight: 500; color: var(--ink-400); transition: background .15s, color .15s; }
.oc-sidebar-collapse:hover { background: var(--line-2); color: var(--ink-900); }
.oc-sidebar-collapse i, .oc-sidebar-collapse svg { width: 18px; height: 18px; flex: 0 0 auto; transition: transform .2s ease; }

/* Collapsed rail: icons only. Every link keeps its title=, so the destination is still named on hover
   and to assistive tech — an unlabelled icon rail is the failure §66 calls out. */
.oc-sidebar.is-collapsed { padding-left: .55rem; padding-right: .55rem; }
.oc-sidebar.is-collapsed .oc-nav-link span,
.oc-sidebar.is-collapsed .oc-brand-name,
.oc-sidebar.is-collapsed .oc-side-user,
.oc-sidebar.is-collapsed .oc-nav-label,
.oc-sidebar.is-collapsed .oc-sidebar-collapse span { display: none; }
/* Belt and braces: nothing may spill out of a 4.5rem rail even mid-transition. */
.oc-sidebar.is-collapsed .oc-nav-link { overflow: hidden; gap: 0; }
.oc-sidebar.is-collapsed .oc-brand { justify-content: center; padding-left: 0; padding-right: 0; }
.oc-sidebar.is-collapsed .oc-nav-link,
.oc-sidebar.is-collapsed .oc-sidebar-collapse,
.oc-sidebar.is-collapsed .oc-side-foot { justify-content: center; }
/* The group headings are what separate Workspace from Admin; with their text gone, keep the grouping
   readable as a rule rather than letting the two sets run together into one undifferentiated strip. */
.oc-sidebar.is-collapsed .oc-nav-label { display: block; height: 1px; padding: 0; margin: .55rem .3rem;
  overflow: hidden; text-indent: -999em; background: var(--line-2); }
.oc-sidebar.is-collapsed .oc-sidebar-collapse i, .oc-sidebar.is-collapsed .oc-sidebar-collapse svg { transform: rotate(180deg); }
.oc-sidebar.is-collapsed .oc-side-foot form { display: none; }

html.oc-sidebar-collapsed .oc-content { margin-left: var(--sidebar-rail); transition: margin-left .2s ease; }
/* The collapsed rail itself. The theme sets this width too, but it now lives in @layer components, which this file's
   unlayered .oc-sidebar width always beats, so the collapsed sidebar stayed full width over the content.
   Desktop only: below 1024px the sidebar is a drawer and collapse does not apply. */
@media (min-width: 1024px) { html.oc-sidebar-collapsed .oc-sidebar { width: var(--sidebar-rail); } }

@media (max-width: 1023px) {
    /* Drawer mode: the sidebar is off-canvas, so the content pane owns the full width and the fixed
       hamburger needs clearance above the page heading. */
    .oc-content, html.oc-sidebar-collapsed .oc-content { margin-left: 0; }
    .oc-main { padding-top: 3.75rem; }
    /* octacer-theme.css's own --shadow-md (0 4px 6px -1px rgba(...,.07), 0 2px 4px -2px rgba(...,.05))
       is too subtle to read as "floating above" a heading or table scrolling directly underneath it —
       the reported bug was never transparency (the button's background is already solid #fff), it was
       a shadow too light to separate the two layers. Stronger shadow, matching the approved Option C
       mockup's own .menu treatment, on every mobile screen this button appears on. */
    .oc-sidebar-mobile-button { box-shadow: 0 6px 16px rgba(15, 23, 42, .12); }
    /* The shell's sidebar sits at z-40, the same layer as the drawer overlay; relying on DOM order to
       keep it above would break the moment either element moves. Lift it explicitly while off-canvas. */
    .oc-sidebar { z-index: 50; }
    .oc-sidebar-close { display: inline-flex; }
    .oc-sidebar-collapse { display: none; }
    .oc-sidebar.is-collapsed { width: var(--sidebar-w); padding-left: .8rem; padding-right: .8rem; }
    .oc-sidebar.is-collapsed .oc-nav-link span,
    .oc-sidebar.is-collapsed .oc-brand-name,
    .oc-sidebar.is-collapsed .oc-side-user,
    .oc-sidebar.is-collapsed .oc-side-foot form { display: revert; }
    .oc-sidebar.is-collapsed .oc-nav-link { overflow: visible; gap: .7rem; }
    .oc-sidebar.is-collapsed .oc-nav-label { display: block; height: auto; padding: .8rem .6rem .3rem;
      margin: 0; text-indent: 0; background: none; overflow: visible; }
    .oc-sidebar.is-collapsed .oc-nav-link, .oc-sidebar.is-collapsed .oc-brand { justify-content: flex-start; }
}
.oc-content--full { margin-left: 0; }

/* ---- Modal chrome ------------------------------------------------------------------------------
   The overlay/panel/open/close machinery is the theme's (.oc-modal / .oc-modal-panel +
   octacer-theme.js data-oc-modal-*). What lives here is only the app's own sizing and the
   header/body/footer bands, which the theme leaves to the caller as Tailwind utilities — spelling
   them once as classes keeps the ten Admin dialogs identical instead of repeating utility strings. */
.oc-modal-backdrop { background-color: rgba(15, 23, 42, .6); }

/* A form dialog wants room for two columns; the overlay centers it, so no top offset is needed. */
.oc-modal-card { max-width: 42rem; width: calc(100% - 2rem); box-shadow: 0 20px 50px -12px rgba(15, 23, 42, .45); }
/* A read-only text dialog is one column of prose, not a two-column form: 42rem leaves a short comment
   stranded in a mostly-empty box. This narrower card sits near a comfortable reading measure at any
   length, so the dialog looks deliberate whether the comment is one line or twenty. Height stays auto —
   the card is as tall as what it holds. */
.oc-modal-card-text { max-width: 30rem; }
/* Only the long-comment case needs a ceiling: scroll the text inside the card rather than let the
   dialog grow past the viewport. overflow-x: hidden is not decoration here — per spec, setting only
   overflow-y to a non-visible value makes the UA compute the other axis as auto too, so without this a
   long *unbroken* string (no spaces to wrap at) grew a HORIZONTAL scrollbar nobody asked for instead of
   just wrapping. Vertical is the only scroll direction this dialog should ever offer. */
.oc-modal-card-text .oc-modal-body { max-height: 60vh; overflow-y: auto; overflow-x: hidden; }
/* The client Edit dialog holds the full client form plus the contact editor below it — taller than a
   typical form dialog, so it gets the same scroll-inside-the-card treatment. */
#client_edit_modal .oc-modal-body { max-height: 70vh; overflow-y: auto; }
/* The Builder's Common Sections dialog holds New agreement's whole shared-sections card — full Builder
   editors, rich text and a signature pad — which needs more width than a form dialog, and scrolls inside
   the card so the Save button stays in view. */
.oc-modal-card-wide { max-width: 56rem; }
#common_sections_modal .oc-modal-body { max-height: 70vh; overflow-y: auto; overflow-x: hidden; }

/* Receipt preview: deliberately no max-width of its own (unlike .oc-modal-card/-text, both fixed widths).
   width:auto lets the panel shrink-wrap to whatever the image actually renders at — a small screenshot
   gets a small dialog, not a fixed-width card with dead space around it — capped by the viewport, never
   by an arbitrary card size, so nothing crops. */
.oc-modal-card-preview {
  width: auto; max-width: min(92vw, 64rem); max-height: 92vh;
  display: flex; flex-direction: column;
  box-shadow: 0 20px 50px -12px rgba(15, 23, 42, .45);
}
.oc-modal-card-preview .oc-modal-title { min-width: 0; }
.oc-preview-body {
  display: flex; align-items: center; justify-content: center;
  background: var(--surface-muted); overflow: auto; min-height: 8rem;
}
.oc-preview-img { display: block; max-width: min(88vw, 60rem); max-height: 80vh; width: auto; height: auto; }
.oc-preview-frame { display: block; width: min(88vw, 60rem); height: 80vh; border: 0; }

.oc-modal-header { display: flex; align-items: center; justify-content: space-between; gap: .75rem;
  padding: 1rem 1.25rem; border-bottom: 1px solid var(--line); }
.oc-modal-title { margin: 0; font-size: 1rem; font-weight: 600; color: var(--ink-700); }
.oc-modal-body { padding: 1.25rem; }
.oc-modal-footer { padding: 1rem 1.25rem; border-top: 1px solid var(--line); }

/* The client typed line breaks; keep them. In CSS, not an inline style attribute (§4: no style="" in .cshtml).
   overflow-wrap: anywhere is what actually saves this from the box that started this comment: pre-wrap
   only wraps at spaces, and free-typed text has no obligation to contain any — one long unbroken run
   (or a pasted URL/token) had nowhere to break and pushed the dialog into scrolling sideways instead of
   down, same failure the comment list already fixed the same way (clients.css, .oc-comment-list li p). */
.oc-modal-text {
    white-space: pre-wrap; overflow-wrap: anywhere;
    margin: 0; color: var(--ink-700); font-size: .925rem; line-height: 1.6;
}
/* Delete-confirmation body: a bare warning glyph beside the text, no filled alert box — the icon alone
   carries the severity so the dialog stays quiet until it matters. */
.oc-modal-warn { display: flex; align-items: flex-start; gap: .75rem; }
.oc-modal-warn > svg, .oc-modal-warn > i { flex: none; color: var(--danger); margin-top: .15rem; }
.oc-modal-warn p { margin: 0; color: var(--ink-700); font-size: .925rem; line-height: 1.6; }
/* The §12 impact line: what the action costs, weighted so it is read rather than skimmed past. */
.oc-modal-impact {
  margin: .85rem 0 0; padding: .6rem .75rem; border-radius: var(--radius);
  background: color-mix(in srgb, var(--warning) 14%, transparent);
  border-left: 3px solid var(--warning);
  color: var(--ink-900); font-size: .875rem; font-weight: 600; line-height: 1.5;
}

/* ---- Staff entrance on the client sign-in page ------------------------------------------------- */
/* Deliberately quiet: clients are the audience for this page, staff only need to find it once. */
.oc-staff-entry { margin-top: 1.75rem; }
.oc-staff-entry-rule { display: flex; align-items: center; gap: .75rem; margin-bottom: .85rem;
  font-size: .6875rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--ink-400); }
.oc-staff-entry-rule::before, .oc-staff-entry-rule::after { content: ""; flex: 1; height: 1px;
  background: var(--oc-line, var(--line)); }

/* ---- Dev mailbox preview ----------------------------------------------------------------------- */
/* Captured mail is rendered in a sandboxed frame, so it needs an explicit box: a sandboxed iframe has
   no intrinsic height and would otherwise collapse to the browser default 150px with its own scrollbar. */
.oc-mail-preview { display: block; width: 100%; height: 13rem; border: 0; border-top: 1px solid var(--line-2);
  background: #fff; }

/* ---- One-time sign-in code input --------------------------------------------------------------- */
/* Six digits read as a token, not prose: even-width digits, widely tracked and centred so a mistyped digit is
   obvious at a glance. */
.oc-otp-input { text-align: center; font-family: var(--font-sans); font-variant-numeric: tabular-nums;
  font-size: 1.5rem; font-weight: 600; letter-spacing: .5em; text-indent: .5em; padding-block: .7rem; }

/* ---------- Carried over from the pre-Tailwind-CDN theme (octa-core a13f714 dropped them) ----------
   octacer-theme.css no longer ships .oc-flash* or .oc-table-compact. They are app surfaces
   (_Flash.cshtml and the three compact tables), so they live here rather than in the theme copy. */
.oc-flash { align-items: center; }
/* Far right and vertically centred on the banner (by request — every dismissible banner, ui-patterns.md). */
.oc-flash-close {
    margin-left: auto; flex: none; align-self: center; background: none; border: 0; padding: .1rem; cursor: pointer;
    color: inherit; opacity: .6; line-height: 0;
}
.oc-flash-close:hover { opacity: 1; }

.oc-table-compact { font-size: .8rem; }
.oc-table-compact thead th { padding: .5rem .75rem; }
.oc-table-compact tbody td { padding: .625rem .75rem; }

/* The app shell's own drawer rules (hamburger clearance, z-index, close button) already live with the
   shell above. This only trims the horizontal gutters, which the shell rule does not cover. */
@media (max-width: 1023px) {
    .oc-main { padding-left: 1rem; padding-right: 1rem; }
}

/* ---------- Narrow-viewport layer ---------------------------------------------------------------------
   The shell is app-owned again (see "App shell" above) and carries its own drawer rules. These rules cover only the app's own view-level
   classes (.oc-page-head / .oc-section / .oc-form-*), which the theme has no knowledge of and therefore
   can never style. Breakpoints are the theme's own (1023 / 640 / 480) — inventing new ones is what makes
   a satellite drift (portal-ux §57: breakpoints are theme-owned). Colours are tokens, so this layer works
   in dark mode too. */
@media (max-width: 640px) {
    /* Title and actions stop competing for one row; actions wrap beneath, full width and thumb-sized. */
    .oc-page-head { flex-direction: column; align-items: stretch; gap: .75rem; }
    .oc-page-head .oc-head-actions { width: 100%; }
    .oc-page-head .oc-head-actions > .btn,
    .oc-page-head .oc-head-actions > a.btn,
    .oc-page-head .oc-head-actions > form > .btn { flex: 1 1 auto; justify-content: center; }
    .oc-page-title { font-size: 1.2rem; }
    .oc-page-sub { max-width: none; }

    /* Section headers: icon + title on one line, action beneath — stops a three-line wrap beside a button. */
    .oc-section { flex-wrap: wrap; row-gap: .6rem; }
    .oc-section .oc-head-actions { margin-left: 0; width: 100%; }
    .oc-section .oc-head-actions > .btn { flex: 1 1 auto; justify-content: center; }

    /* auto-fit with a 220px minimum cannot fit two columns in 375px; it was producing ragged wrapping. */
    .oc-form-grid { grid-template-columns: 1fr; }
    .oc-form-actions { flex-direction: column-reverse; align-items: stretch; gap: .5rem; }
    .oc-form-actions > .btn { width: 100%; justify-content: center; }

    /* Cards lose the wide gutters that eat a narrow viewport. */
    .oc-card > .oc-dl,
    .oc-card > .oc-list,
    .oc-card > .oc-empty,
    .oc-card > p { padding-left: .9rem; padding-right: .9rem; }

    /* A card must not clip the scroll region inside it, or the columns are unreachable again. */
    .oc-card:has(.oc-table-scroll) { overflow: visible; }
}

@media (max-width: 480px) {
    .oc-page-title { font-size: 1.1rem; }
    .oc-toolbar { flex-direction: column; align-items: stretch; }
    /* .oc-toolbar .field-group's flex:1 1 16rem sets a 16rem BASIS along the toolbar's main axis — a
       width, while the toolbar is a row. Flipping the toolbar to a column above just made that same
       16rem a HEIGHT instead, stretching the search box to a fixed 256px tall with the icon+input
       floating centred in empty space. Reset the basis (and drop the desktop max-width cap) now that
       the column layout already gives it the full row's width via align-items: stretch. */
    .oc-toolbar .field-group { flex-basis: auto; max-width: none; }
}


/* =======================================================================================================
   CLIENT PORTAL — portal-ux v2.0 surfaces
   ------------------------------------------------------------------------------------------------------
   Everything below is token-driven. That is the whole point: dark mode is a .dark class on <html>, and a
   Tailwind palette utility (text-gray-500, bg-gray-50) is a FIXED colour that does not know the class
   exists. Those utilities were what made the portal's secondary text sit at ~2:1 on a dark card and the
   locked-email box render as a white rectangle on a dark page. Tokens track both themes automatically,
   which is what accessibility.md means by "a hardcoded colour is a light/dark bug waiting to happen".
   ======================================================================================================= */

/* ---------- Text roles (replace the fixed-palette gray utilities in the portal views) ---------- */
.oc-t-body   { color: var(--ink-700); }
.oc-t-muted  { color: var(--ink-600); }
.oc-t-faint  { color: var(--ink-400); }
.oc-t-xs     { font-size: .75rem; line-height: 1.1rem; }
.oc-t-sm     { font-size: .875rem; }
.oc-surface-muted { background: var(--line-2); color: var(--ink-700); }

/* ---------- Sortable table headers (§19) ---------- */
.oc-sort {
  display: inline-flex; align-items: center; gap: .35rem;
  color: inherit; text-decoration: none; border-radius: .25rem;
  /* §167/WCAG 2.2 2.5.8: the row is the hit area, not just the glyph. */
  padding: .15rem .2rem; margin: -.15rem -.2rem;
}
.oc-sort:hover { color: var(--brand-700); }
.oc-sort:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
.oc-sort .oc-sort-ic { width: .7rem; height: .7rem; opacity: .35; transition: opacity .15s ease; }
.oc-sort:hover .oc-sort-ic { opacity: .7; }
.oc-sort.is-sorted { color: var(--brand-700); }
.oc-sort.is-sorted .oc-sort-ic { opacity: 1; }

/* ---------- List search (§28) ---------- */
.oc-search { position: relative; display: flex; align-items: center; min-width: 0; flex: 1 1 14rem; max-width: 22rem; }
.oc-search-ic {
  position: absolute; left: .7rem; width: .85rem; height: .85rem;
  color: var(--ink-400); pointer-events: none;
}
.oc-search-input { padding-left: 2rem; width: 100%; }
/* The browser's own clear affordance is the §28 "Clear"; it needs to be visible in dark mode too. */
.oc-search-input::-webkit-search-cancel-button { filter: grayscale(1) opacity(.6); cursor: pointer; }

/* ---------- Applied-filter chips (§31) ---------- */
.oc-chip-rail { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; margin: -.5rem 0 1.25rem; }
.oc-chip-rail-label { font-size: .75rem; font-weight: 600; color: var(--ink-400); text-transform: uppercase; letter-spacing: .04em; }
.oc-chip-removable { display: inline-flex; align-items: center; gap: .4rem; min-height: 1.75rem; }
.oc-chip-removable:hover { border-color: var(--danger); color: var(--danger); }
.oc-chip-removable strong { font-weight: 600; }
.oc-chip-clear { font-size: .8rem; }

/* ---------- Table scroll region (§95/§96/§79) ----------
   The theme owns .oc-table-scroll; these only cover the app's own composition of it inside .oc-card.
   Without a scroll region an 8-column table hands the PAGE a horizontal scrollbar on a phone, which is
   the failure §79 names. */
.oc-card > .oc-table-scroll { border-radius: 0 0 var(--radius-lg) var(--radius-lg); }
/* Bottom-only assumes a header sits above the scroll region inside the card — true wherever this was
   first written, false on every plain list page (Clients, Proposals, Contracts, Invoices, Receipts):
   there .oc-table-scroll is the card's ONLY child, flush against all four of its edges. Squaring the top
   two corners there fought the card's own rounded top, so the header row's flat-gray background band cut
   a hard right-angle into a curve — the border reads as broken exactly at the two top corners. Rounding
   all four whenever nothing precedes it is what makes the header follow the card's own frame instead of
   overriding it. */
.oc-card > .oc-table-scroll:first-child { border-radius: var(--radius-lg); }
.oc-table-scroll:focus-visible { outline: 2px solid var(--ring); outline-offset: -2px; }

/* The last row's hover highlight is a plain square rectangle (the theme's own .oc-table tbody tr:hover),
   but it sits inside a table-scroll whose OWN bottom corners are rounded — so the container clips the
   highlight's two bottom corners to that curve, leaving a small sliver at each one that never lights up.
   That is what read as "hover doesn't fully reach the bottom corners". Rounding the last row's own two
   end cells to match is what makes the highlight follow the same curve instead of being cut by it. */
.oc-table tbody tr:last-child:hover td:first-child { border-bottom-left-radius: var(--radius-lg); }
.oc-table tbody tr:last-child:hover td:last-child { border-bottom-right-radius: var(--radius-lg); }

/* Auto layout hands 100% of a short table's leftover width to whichever column has no explicit width —
   one column stretches, the header band still reaches the card edge, but the columns read as lopsided.
   .oc-table-fixed opts a table into table-layout:fixed so every column keeps an exact, proportional share
   instead, while the row still fills the card like every other table. The compound selector beats the
   bare .oc-col-actions{width:1%} theme-adjacent rule above without needing !important. */
.oc-table-fixed { table-layout: fixed; }
.oc-table-fixed .oc-col-title { width: 22%; }
.oc-table-fixed .oc-col-status { width: 13%; }
.oc-table-fixed .oc-col-total { width: 14%; }
.oc-table-fixed .oc-col-started { width: 17%; }
/* Up to three actions can land here now (Contract link + Preview + PDF, once an accepted proposal has
   produced a contract) — 24% clipped the row the moment all three were present, cutting the PDF button
   off past the card edge with no scrollbar to reveal it. */
.oc-table-fixed .oc-col-actions { width: 34%; }

/* Admin proposals grid — nine columns in one row, which is the whole problem.

   FIRST ATTEMPT, and why it failed: fixed proportions alone. Auto layout had been handing the leftover
   width to whichever column declared none, so the reference wrapped mid-value and the dates broke over
   two lines. Fixed proportions cured that and introduced a worse fault: table-layout:fixed will not
   shrink a BUTTON. "View feedback (1)" is ~175px of intrinsic width, 13% of the row was ~150px, so the
   button overflowed its cell and sat on top of the Sent column, with the row's icons landing under the
   dates.

   THE FIX IS ROOM, NOT WRAPPING. Three things buy it, in order of how much they give back:
     1. a smaller type scale for this grid only (.78rem against the theme's .85rem), because nine columns
        of dense reference data is a different reading job from a four-column list;
     2. tighter cell padding (.6rem/.7rem against .75rem/.875rem) — nine columns means eighteen paddings,
        so this alone returns about 50px of usable row;
     3. proportions re-cut around the widest FIXED thing in each column rather than around the text.
   Feedback goes 13% -> 16% because the button is the widest fixed object in the table; the title gives
   the width up, because prose is the one thing that can afford a second line.

   min-width is raised with them: below it the grid scrolls inside .oc-table-scroll instead of
   compressing, which is what stops these proportions collapsing back into collisions on a laptop. */
/* ════════════════════════════════════════════════════════════════════════════════════════════════════
   ADMIN TABLE DENSITY SYSTEM
   One set of rules for Clients, Proposals, Contracts, Invoices and Receipts. Add .oc-table-dense to the
   table and a width class per column; nothing else is decided per table.

   WHY IT IS ONE SYSTEM. Each grid was previously sized on its own, so five tables that sit one click
   apart had five different densities — different row heights, different type, and in two cases columns
   that collided because table-layout:fixed will not shrink a button. Fixing them one at a time is what
   produced that; the density belongs to the Admin portal, not to a page.

   THE ORDER OF ATTACK, and it matters: type, then column widths, then padding. Widening a column or
   padding a cell to cure an overlap only moves the overlap; shrinking the type gives every column room
   at once. Nothing here wraps — a wrapped cell makes its whole ROW taller, which is what breaks the one
   thing a dense grid has to have, a consistent row height. Long prose truncates with an ellipsis and
   keeps its full text in a title attribute instead.
   ════════════════════════════════════════════════════════════════════════════════════════════════════ */
.oc-table-dense { table-layout: fixed; font-size: .75rem; }

/* 12px body, 12px header. The header stays uppercase but drops most of its tracking: at this size the
   theme's .04em was pushing "OUTSTANDING" wider than the figures beneath it. */
.oc-table-dense thead th {
    font-size: .75rem; letter-spacing: .02em; padding: .5rem .55rem; line-height: 1.25;
}
.oc-table-dense tbody td { padding: .45rem .55rem; line-height: 1.35; }
.oc-table-dense thead th:first-child,
.oc-table-dense tbody td:first-child { padding-left: 1rem; }
.oc-table-dense thead th:last-child,
.oc-table-dense tbody td:last-child { padding-right: 1rem; }

/* SINGLE LINE IS THE DEFAULT, for every header and every cell. The exceptions opt out below, and there
   is one: .oc-col-text, which truncates. */
.oc-table-dense thead th,
.oc-table-dense tbody td { white-space: nowrap; vertical-align: middle; }

/* The prose columns — a name, a title, a filename. Clipped with an ellipsis so a long value costs the
   row nothing; the full string stays in the cell's title attribute, so nothing is lost. */
.oc-table-dense td.oc-col-text { overflow: hidden; text-overflow: ellipsis; }
.oc-table-dense td.oc-col-text > a,
.oc-table-dense td.oc-col-text > span { display: inline; }

/* Belt and braces for the status column specifically: it just had its own real width restored (see the
   redistribution note above), but a status cell that ran out of room again — a narrower viewport, a
   future longer label — should visibly say so with an ellipsis, not hard-clip mid-word with nothing to
   show anything was cut. min-width:0 is what lets a flex child (the badge) actually shrink and hand its
   overflow to text-overflow instead of forcing the column wider than its fixed share. */
.oc-table-dense td.oc-col-status { overflow: hidden; text-overflow: ellipsis; }
.oc-table-dense td.oc-col-status .badge { max-width: 100%; min-width: 0; overflow: hidden; text-overflow: ellipsis; }

/* A small glyph in front of a value, marking what KIND of thing the row is (a client's building or
   person). Inline rather than a flex row, so the cell keeps truncating with an ellipsis — a flex
   container would have made the name a flex item and lost that. */
.oc-table-dense td .oc-cell-icon { color: var(--ink-400); margin-right: .3rem; }
.oc-table-dense td .oc-cell-icon i { width: 12px; height: 12px; vertical-align: -1px; }

/* A muted suffix on the same line as the value it qualifies ("GHL SETUP · 16 sections"). This is what
   replaced the second <div> those cells used to carry: the information stays, the row does not double
   in height, and every table's rows measure the same. */
.oc-table-dense .oc-cell-note { color: var(--ink-400); font-size: .6875rem; }

/* Controls follow the type down, so a button cannot be wider than the column holding it. */
.oc-table-dense .btn-sm { font-size: .7rem; padding: .25rem .5rem; }
.oc-table-dense .btn-sm.oc-icon-btn { padding: .25rem; }
/* A long enum label ("Hourly / Time & Materials") in a column sized for the common case is what let a
   billing-type badge run straight into the Status column beside it — the badge itself has no truncation
   of its own, so the text simply kept going. Bounded to its cell's own width and clipped instead: not a
   redesign of the badge (it stays the theme's own inline-flex pill everywhere else in the app — this is
   scoped to dense-table cells only), just the same "truncate rather than collide" rule the table's other
   columns already follow. */
.oc-table-dense .badge { font-size: .6875rem; padding: .15rem .45rem; max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
/* flex-start, matching the left-aligned ACTIONS header — see the note on .oc-col-actions above. This
   rule sits after that one and would otherwise win on source order alone. */
.oc-table-dense .oc-row-actions { gap: .1rem; flex-wrap: nowrap; justify-content: flex-start; }
/* Left, matching the ACTIONS header — the right-aligned rule that used to sit here contradicted the
   alignment set above and only lost on specificity, which is not a thing to leave standing. */
.oc-table-dense .oc-col-actions { text-align: left; }

/* The Contracts grid's publish switch (Portal column) is an inline-flex label — toggle plus its own
   "ACTIVE"/"INACTIVE" word, tracked and uppercased for a settings row with room to spare — and
   inline-flex sizes to that content whatever the column's width says. At the tile scale it quietly
   overflowed its cell and rendered on top of the Actions column next to it: not a wrap, an actual
   collision. Tightened down to the density system's own control size — smaller track, no hit-area
   padding, tighter tracking on the word — the same way .btn-sm and .badge already scale down on this
   line, rather than solved by giving the column more room than the rest of the row can spare. */
.oc-table-dense .oc-tile-switch { gap: .3rem; padding: 0; letter-spacing: .02em; }
.oc-table-dense .oc-tile-switch .oc-toggle { width: 1.75rem; height: 1rem; }
.oc-table-dense .oc-tile-switch .oc-toggle-track::before { height: .72rem; width: .72rem; left: .14rem; top: .14rem; }
.oc-table-dense .oc-tile-switch .oc-toggle input:checked + .oc-toggle-track::before { transform: translateX(.75rem); }

/* Figures line up on the decimal point, which is the only way a column of money can be compared. */
.oc-table-dense .oc-col-num { text-align: right; font-variant-numeric: tabular-nums; }

/* A wide grid may reach into .oc-main's 2rem side padding — +64px, taken only where that padding
   exists. Below the breakpoint .oc-main drops to 1rem and the card stays put. */
@media (min-width: 1200px) {
    .oc-card-bleed { margin-inline: -2rem; }
    html[data-theme-scope="admin"] .oc-card-bleed { margin-inline: -1.35rem; } /* matches the Admin gutter */
}

/* ── Per-table column widths ─────────────────────────────────────────────────────────────────────────
   Each is a share of the row, summing to 100, and each is cut to the widest FIXED thing in its column —
   which for a count is the HEADER, not the two-digit figure under it. Only the prose column gives width
   away, because it is the one that can truncate without losing anything. */

/* Clients — 9 columns, measured rather than split evenly.
   Client · Email · Phone · Status · Country · Contacts · Proposals · Since · Actions.

   IT USED TO BE AN EVEN SPLIT, and that ended when Email arrived. An address is long and, unlike a count
   or a status word, the half you can see is not the half that identifies it — "mail.haiderali94…" and
   "mail.haiderali95…" are the same string until the part that got cut. Phone has the same property from
   the other end: the country code is shared by every row and the digits that tell two clients apart are
   the last ones. So the two contact columns take the room the short ones were sitting in — a status
   badge, a two-letter country and a one-digit count never needed an even ninth.

   The widths sum to exactly 100; keep it that way by hand when a column is added or dropped.

   Client -20% and Email -30% from their original 18/26, by request, freeing 11.4 points; Proposals +30%
   and Contacts +10% from their original shared 5 spend 2 of those points; the remaining 9.4 points are
   spread across Phone/Status/Country/Since/Actions in proportion to their own original share, so the
   table's overall balance (each column still cut to what its own content actually needs) holds rather
   than being handed arbitrarily to whichever column was touched. Phone -30% and Status -5% from their
   15.7/10.8 (the values just above), by a later request, freed a further 4.7 + 0.5 = 5.2 points, all of
   it added to Proposals by that same request. */
.oc-table-clients .oc-col-name      { width: 14.4%; }
.oc-table-clients .oc-col-email     { width: 18.2%; }
.oc-table-clients .oc-col-phone     { width: 11.0%; }
.oc-table-clients .oc-col-status    { width: 10.3%; }
.oc-table-clients .oc-col-country   { width: 8.4%; }
.oc-table-clients .oc-col-contacts  { width: 5.5%; }
.oc-table-clients .oc-col-proposals { width: 11.7%; }
.oc-table-clients .oc-col-since     { width: 9.6%; }
.oc-table-clients .oc-col-actions   { width: 10.9%; }

/* Alignment, by request — scoped to THIS table only, so the other four keep counts right-aligned
   (tabular-nums, the normal rule for a figure meant to be compared down a column) and actions flush to
   the row's own right edge. */
.oc-table-clients .oc-col-count { text-align: center; }
.oc-table-clients .oc-col-actions { text-align: left; }
.oc-table-clients .oc-col-actions .oc-row-actions { justify-content: flex-start; }

/* Proposals — 9 columns, the most of any Admin table, so the widths below are measured rather than
   guessed (real .getBoundingClientRect()/.scrollWidth on a live row, not eyeballed):

     Reference "OCT-2026-0008" needs 110px; the 11% this used to carry gave it only 104 — a mono
     reference silently overflowing its own cell by 6px. 12% covers it.
     Date needs 86px for "09 Oct 2026"; 8% (76px) ran the value straight into the Actions column with no
     gap at all — the exact defect reported. 10% covers it with room to spare for a longer format.
     Billing type's four labels run from 87px ("Fixed Project") to 152px ("Hourly / Time & Materials") —
     one outlier nearly double the rest. Sizing the column to the outlier would cost the row 4% for one
     value; 14% fits three of the four labels in full and lets .oc-table-dense .badge (above) truncate
     the fourth instead of running it into Status, which is the same "truncate rather than expand the
     table" rule every other column in this system already follows.

   The four extra points above baseline (+1 Reference, +2 Date, +3 Billing type... offset by -1 net from
   Status, which needed none) come from Title and Client, the two columns here that were already
   truncating (.oc-col-text) and so were always the ones with real slack to give — a real proposal title
   or client name simply truncates a little sooner than it did before; nothing here was overflowing to
   begin with. */
/* The Reference column was removed from the table (Index.cshtml) but its 12% share was left behind here,
   unclaimed — table-layout:fixed reads these percentages straight off the class, so a rule with no
   matching column any more is not "harmless dead CSS", it is 12% of the table's width going nowhere.
   Every other column was squeezed by that same 12%, and Status — already the narrowest at 9% — was the
   one that actually ran out of room: "Changes requested" (the longest status label) clipped mid-word
   with no ellipsis to even show something was cut. Redistributed below, Status now getting the largest
   share of what came back since it is the column that actually failed. */
/* Notes split out of Feedback (Index.cshtml) — they were one column showing two unrelated things (the
   decision note vs. the ongoing questions thread), so this repeats the same redistribution exercise as
   above rather than letting the table run over 100%. Feedback gives back the most: it went from holding
   a whole quote AND a button to holding just the button, so it needed far less than its old 16%. */
/* TITLE IS THE WIDEST COLUMN, deliberately. It is what somebody scans this list FOR, and it is
   the one value here that is free text of any length — "Untitled agreement hello" was being cut
   at eleven characters while a status badge and a billing model, both of which are a fixed short
   vocabulary, each held more room than it did. The space came from Notes/Feedback/Value, which
   are glanceable at any width. Widths still sum to exactly 100. */
/* Proposal 20% narrower (22 → 17.6) and the freed width given to Notes (12 → 16.4), by request. */
.oc-table-proposals .oc-col-title    { width: 17.6%; }
.oc-table-proposals .oc-col-client   { width: 12%; }
.oc-table-proposals .oc-col-pricing  { width: 11%; }
.oc-table-proposals .oc-col-status   { width: 10%; }
/* Centred, header and cell together — by request. th./td.-qualified for the same reason the Contracts
   actions column is: the generic dense-grid rules carry a type selector, so a bare two-class rule here
   would be silently outranked and only half of this would apply. The empty-state dash centres with it,
   which is the case that looked most adrift left-aligned in a 15% column. */
.oc-table-proposals th.oc-col-notes,
.oc-table-proposals td.oc-col-notes  { width: 16.4%; text-align: center; }
.oc-table-proposals .oc-col-feedback { width: 12%; }
.oc-table-proposals .oc-col-date     { width: 9%; }
.oc-table-proposals .oc-col-actions  { width: 12%; }

/* Contracts — 8 columns, down from 11 once Signed, Expires and PDF came out.

   Billing type, Pricing and Status were briefly all equal at 13%, matched to "BILLING TYPE" (the
   longest of the three headers) — Billing type gives two of its own points back now, since that equal
   width read as more space than the badge itself needed, and Pricing/Status take a point each: they
   were already the tighter of the three, now with a little more room around "$87,931" and the status
   badge. Billing type stays no lower than 11%, the same floor "BILLING TYPE" needed the first time this
   column was sized to its own header (below the earlier per-table note in Proposals).

   Client Portal is 12% — the header word itself ("Client Portal") measures that; the switch alone would
   need far less, but a header is never truncated the way a body cell is, so it is the header that sets
   the floor here.

   Title and Client gave up the points this cost (14% → 11% each, still truncating oc-col-text columns,
   so a long value simply clips a little sooner than before) and Actions gave up one more — still Download
   + a LABELLED Edit + Delete, so 18% is the floor already proven for that, not the ceiling.

   Actions dropped again since then, to 13% — Preview and Download moved to the contract's own Details
   page, so this column is only ever an icon-only Edit + Delete now, and 18% was sized for three controls
   including a labelled one. The 5 points it gave up went to Title (11% → 16%): the one column that most
   often actually needs it, since a contract's title is free text staff typed, not a fixed vocabulary like
   Type or Status. */
/* TITLE IS THE WIDEST COLUMN, deliberately. It is what somebody scans this list FOR, and it is
   the one value here that is free text of any length — "Untitled agreement hello" was being cut
   at eleven characters while a status badge and a billing model, both of which are a fixed short
   vocabulary, each held more room than it did. The space came from Notes/Feedback/Value, which
   are glanceable at any width. Widths still sum to exactly 100.
   Title gave up 40% of its own share (24% → 14.4%) to Client (12% → 21.6%), by request — a batch's
   lead row now carries a client name AND a "+N more" chip in that column (see the batch-grouping
   rules below), and 12% was clipping both. Client then gave back 20% of ITS OWN share (21.6% →
   17.28%) to Title (14.4% → 18.72%), by a second request. */
.oc-table-contracts .oc-col-title   { width: 18.72%; }
.oc-table-contracts .oc-col-client  { width: 17.28%; }
.oc-table-contracts .oc-col-type    { width: 8%; }
.oc-table-contracts .oc-col-pricing { width: 10%; }
/* Pricing prints a dash as often as a real figure (Money.Pricing — no hourly rate set yet). Right-aligned
   like a real number column, a lone "—" hugs the far edge under a header sitting at the same edge; centred
   instead, both the header and every value (dash or "$50/hr") sit on the same middle line. */
.oc-table-contracts .oc-col-value   { width: 11%; text-align: center; }
.oc-table-contracts .oc-col-status  { width: 12%; }
.oc-table-contracts .oc-col-portal  { width: 10%; text-align: center; }
/* "ACTIONS" centred in its header, and the two icon buttons beneath it centred as one compact group —
   not space-evenly (that was sized for three controls including a labelled Edit; with two bare icons
   left, it pushed them out towards the cell's edges instead of reading as one tight group under the
   centred header).

   th./td.-qualified, not just .oc-col-actions: the generic dense-grid rule a few hundred lines up
   (`.oc-table-dense th.oc-col-actions, .oc-table-dense td.oc-col-actions { text-align: left }`) carries
   one more type selector than a bare two-class rule, so on specificity alone it silently beat this block
   no matter where in the file this sat — header stayed left while the buttons below it (a 3-class
   selector, `.oc-table-contracts .oc-col-actions .oc-row-actions`) still won their own fight and centred,
   leaving the header and its buttons on two different edges. Matching that same th/td specificity here is
   what actually lets this table's own centring apply to the header, not just the row. */
.oc-table-contracts th.oc-col-actions,
.oc-table-contracts td.oc-col-actions { width: 13%; text-align: center; }
.oc-table-contracts .oc-col-actions .oc-row-actions { justify-content: center; }
/* Two bare icon buttons now (Edit, Delete) — the shared base .oc-row-actions gap (.5rem, above) read as
   too loose for that pair once space-evenly stopped spreading them apart on its own. */
.oc-table-contracts .oc-row-actions { gap: .25rem; }

/* Optical alignment for Type and Billing type: a badge carries its own .45rem of horizontal padding
   (.oc-table-dense .badge, above), so its visible TEXT starts .45rem right of the cell's actual left
   edge — the plain header label above it has no such padding, so "TYPE"/"BILLING TYPE" line up with the
   cell edge while "Agreement"/"Milestone Based" sit a few pixels further in. Pulling the badge left by
   exactly its own padding brings the WORD back under the header; the pill's rounded background still
   extends slightly past it, same as it always did — the cell still has enough padding of its own
   (.55rem) that the background never reaches the column before it. */
.oc-table-contracts .oc-col-type .badge,
.oc-table-contracts .oc-col-pricing .badge { margin-left: -.45rem; }

/* Agreement batch grouping (AgreementIntake), by request — agreements drafted together in one "New
   agreement" action collapse into one row (the batch's oldest document, in full) with a chevron that
   reveals the rest. See AgreementBatchRow and the data-oc-tree-* markup in Contracts/Index.cshtml — the
   toggle itself is the SAME tree-row JS primitives/_TreeTable already uses (octacer-table.js), so this
   is markup + styling only, no new script. */
.oc-table-contracts tr.is-batched td:first-child {
    /* A continuous rail down the left edge of a batch's rows (its lead + every hidden member) — the
       whole block reads as one unit before the "+N more" chip is even read. */
    box-shadow: inset 3px 0 0 var(--brand-500);
}
/* The whole cell is the click/tap target (role=button), not just the chevron — a small icon alone was
   too easy to miss on a dense row. */
.oc-batch-lead { display: flex; align-items: center; gap: .5rem; min-width: 0; cursor: pointer; border-radius: .4rem; margin: -.25rem; padding: .25rem; }
.oc-batch-lead:hover { background: var(--surface-muted); }
.oc-batch-lead:focus-visible { outline: 2px solid var(--brand-500); outline-offset: 1px; }
/* Smaller, by request — the chevron read as oversized next to the row's other icon buttons. */
.oc-batch-toggle { width: 1.25rem; height: 1.25rem; flex: 0 0 auto; }
/* min-width:0 on every flex step down to the text itself — without it a flex child's default
   min-width:auto refuses to shrink below its content's natural width, and the ellipsis below never
   actually triggers; the client name just pushes the cell wider or gets hard-clipped by the table's own
   overflow:hidden with no "…" to show it happened. */
.oc-batch-client { display: flex; flex-direction: column; gap: .15rem; min-width: 0; }
.oc-batch-client-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.oc-batch-pill {
    display: inline-flex; align-items: center; width: fit-content; max-width: 100%; font-size: .625rem;
    font-weight: 600; color: var(--brand-700); background: var(--brand-50); padding: .1rem .4rem;
    border-radius: 999px; border: 1px solid color-mix(in srgb, var(--brand-500) 16%, transparent);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.dark .oc-batch-pill { color: var(--brand-100); background: rgba(99,102,241,.16); }
/* A ticked-but-not-drafted type: same badge shape as a real one, dimmed so it never reads as an actual
   status — the row's own "Not started yet"/"Not started" text already says what it is. */
.oc-table-contracts tr.is-pending .badge.is-pending-badge { opacity: .6; }

/* Invoices — 9 columns. "Outstanding" is the longest header in the whole admin area.

   Actions holds a LABELLED "Send invoice" button beside the Open eye and Delete icons on a Draft
   invoice — the same shape of problem measured and fixed on Contracts' Edit button, and by the same
   arithmetic it does not fit in a narrow share. 17% was confirmed live against a real Draft row; it
   climbs to 20% here to also cover a wider internal gap and true centring, the same treatment applied to
   Contracts' Actions column. Client gives up the difference — still a truncating oc-col-text column, so
   a long client name simply clips a little sooner than before. */
.oc-table-invoices .oc-col-number  { width: 12%; }
.oc-table-invoices .oc-col-client  { width: 14%; }
.oc-table-invoices .oc-col-amount  { width: 10%; }
.oc-table-invoices .oc-col-pricing { width: 11%; }   /* Pricing type, by request */
.oc-table-invoices .oc-col-status  { width: 10%; }
.oc-table-invoices .oc-col-methods { width: 11%; }
.oc-table-invoices .oc-col-date    { width: 10%; }   /* Created and Due date, by request */
.oc-table-invoices .oc-col-actions { width: 12%; text-align: center; }
/* Headings all left (by request) — Amount's right-aligned heading read as out of line; Actions stays centred. */
.oc-table.oc-table-invoices thead th { text-align: left; }
.oc-table.oc-table-invoices thead th.oc-col-actions { text-align: center; }
/* Values start from the left too, under their headings (Amount included); Actions stays centred. */
.oc-table.oc-table-invoices tbody td { text-align: left; }
.oc-table.oc-table-invoices tbody td.oc-col-actions { text-align: center; }
/* Amount, Payment method and Due: centred, heading and values alike (by request). */
.oc-table.oc-table-invoices :is(thead th, tbody td):is(.oc-col-amount, .oc-col-pricing, .oc-col-methods, .oc-col-date) { text-align: center; }
/* Portal's own invoice list (Areas/Portal/Views/Invoices/Index.cshtml) is a different table — this Admin
   rule above never reached it. Payment method centred here too, heading and values (by request); the rest
   of that table's columns are untouched. */
html[data-theme-scope="portal"] .oc-table.oc-table-portal-invoices :is(thead th, tbody td).oc-col-methods { text-align: center; }
/* Actions: centred, heading and buttons alike (by request) — the row's buttons vary in count (View/PDF,
   sometimes Payments too), so a fixed .text-right left them looking scattered rather than aligned. */
html[data-theme-scope="portal"] .oc-table.oc-table-portal-invoices :is(thead th, tbody td).oc-col-actions { text-align: center; }
html[data-theme-scope="portal"] .oc-table.oc-table-portal-invoices .oc-col-actions .oc-row-actions { justify-content: center; }
/* Invoice amounts in the list read like the invoice builder's summary (by request): the text face with
   aligned digits, not the mono number face, and always two decimals (Money.Exact) — "$4,250.00". */
.oc-invoice-figure { font-family: inherit; font-variant-numeric: tabular-nums; }
/* The whole row opens the invoice (data-oc-jump) — say so with the pointer and a hover tint. */
.oc-table-invoices tr[data-oc-jump] { cursor: pointer; }
.oc-table-invoices tr[data-oc-jump]:hover td { background: var(--line-2); }
/* space-evenly spread two icons the same as three, so a row with no Send button (anything past Draft)
   put its eye and delete icons a fifth of the row apart — a stray gap, not a control group. Centred
   with a fixed gap groups whatever is actually rendered, 2 controls or 3. */
.oc-table-invoices .oc-col-actions .oc-row-actions { justify-content: center; }
.oc-table-invoices .oc-row-actions { gap: .55rem; }

/* Receipts — 8 columns (Client/Invoice/Amount/Method/Receipt/Submitted/Status/Actions, by request), and
   the widest action set: accept, plus three icons/an overflow menu beside it. */
/* Actions 46% narrower (19.7 → 10.635), by request — it was mostly empty space; the freed 9.065 shared
   equally by the other seven columns (+1.295 each). Still 100% in total. Fixed layout so these widths hold;
   a row with every action (View, Download, Reject, Accept, Delete) wraps onto a second line INSIDE the
   column rather than running past it, out from under the heading's background. */
.oc-table-receipts { table-layout: fixed; }
.oc-table-receipts .oc-col-client  { width: 14.295%; }
.oc-table-receipts .oc-col-invoice { width: 12.295%; }
.oc-table-receipts .oc-col-amount  { width: 11.295%; }
.oc-table-receipts .oc-col-method  { width: 11.295%; }
.oc-table-receipts .oc-col-file    { width: 15.695%; }
.oc-table-receipts .oc-col-sent    { width: 13.195%; }
.oc-table-receipts .oc-col-status  { width: 11.295%; }
.oc-table-receipts .oc-col-actions { width: 10.635%; }
.oc-table-receipts td.oc-col-actions .oc-row-actions { flex-wrap: wrap; justify-content: flex-end; gap: .25rem; }
.oc-table-receipts thead th.oc-col-actions { background: var(--line-2); }
.oc-table-receipts td.oc-col-file { overflow: hidden; text-overflow: ellipsis; }
/* Amount and Method: centred, heading and values alike (by request) — matches the same treatment already
   given the Invoices list's own Amount/Payment method/Due columns. */
.oc-table-receipts :is(thead th, tbody td):is(.oc-col-amount, .oc-col-method) { text-align: center; }
/* Delete: icon only, no tinted box (by request) — local to this table; .oc-icon-danger itself is shared
   with other delete/danger icons across the app and stays as it is everywhere else. The hover tint (theme,
   admin-scoped) is untouched, so pressing it still gives feedback. */
.oc-table-receipts .oc-col-actions .oc-icon-danger { background: none; }

/* A notice under a form field — for the case where the value is not malformed, it just already belongs
   to something. Amber rather than red on purpose: nothing has gone wrong, and a red block the width of
   the form reads as a system error, which is how a message meant to help gets scrolled past.

   Sized and coloured to sit BELOW an input without competing with it: the field's own red outline (from
   the ModelState error) is what points at the value; this carries the sentence and the way out. */
.oc-field-notice {
    /* Two rows, stacked: icon+sentence, then the way out — see the row rules below for why. */
    display: flex; flex-direction: column; gap: .55rem;
    margin-top: .5rem; padding: .7rem .8rem;
    /* The theme's own warning pair, mixed the same way .oc-alert-warning mixes its border — so this
       notice is the same amber in light AND dark without a second palette to keep in step. */
    border: 1px solid color-mix(in srgb, var(--warning-subtle-foreground) 30%, transparent);
    border-radius: var(--radius);
    background: var(--warning-subtle); color: var(--warning-subtle-foreground);
    font-size: .8rem; line-height: 1.5;
}
/* Icon top-aligned against the sentence, not vertically centred against it — centring reads fine for one
   line and looks off-balance the moment the message wraps to two or three, pulling the glyph down away
   from the text it is marking. */
.oc-field-notice-row { display: flex; align-items: flex-start; gap: .5rem; }
.oc-field-notice-row > i {
    flex: 0 0 auto;
    /* Nudges the glyph down to the text's cap-height instead of its own box's top edge, at this
       particular icon size and line-height — a wash at one line, visible once the icon sits beside a
       genuinely multi-line paragraph. */
    margin-top: .15rem;
}
.oc-field-notice-text { margin: 0; font-weight: 600; }
/* Indented to align under the SENTENCE, not the icon beside it — a button sitting flush with the left
   edge read as its own, unrelated control once the row above it was two icon-and-text lines tall instead
   of one, rather than the second half of the same notice. */
.oc-field-notice-actions {
    display: flex; flex-wrap: wrap; gap: .4rem;
    padding-left: calc(1rem + .5rem);
}
/* The buttons inside stay compact — this is a notice, not a card. */
.oc-field-notice-actions .btn { font-size: .75rem; padding: .25rem .6rem; }
.oc-field-notice-actions form { margin: 0; }

/* ---------- Modal rendering ----------
   Belt and braces for the fetched dialogs. app.js now opens them only after their content lands, so an
   empty panel should never be visible — but .oc-modal-panel carries the card's background, border and
   shadow, so if one ever IS opened empty it paints as a thin line across a dimmed page. :empty makes
   that impossible rather than merely unlikely. */
.oc-modal-panel:empty { display: none; }

/* A short fade-and-lift on open. Enough to read as "this appeared" rather than a snap, short enough
   that nothing feels delayed — 120ms, and only on the panel, so the backdrop is already there when the
   card arrives and the two never look out of step. Transform and opacity only: both composite, so there
   is no layout or repaint work during the animation. */
@keyframes oc-modal-in {
    from { opacity: 0; transform: translateY(6px) scale(.985); }
    to   { opacity: 1; transform: none; }
}
.oc-modal.flex > .oc-modal-panel {
    animation: oc-modal-in 120ms var(--ease-out, cubic-bezier(.22, 1, .36, 1)) both;
    will-change: opacity, transform;
}
@media (prefers-reduced-motion: reduce) {
    .oc-modal.flex > .oc-modal-panel { animation: none; }
}

/* ---------- Focusable error summary (§45/§166) ---------- */
/* Tighter than the generic .oc-alert-danger it rides on: this box interrupts a two-field sign-in form,
   not a page of content, so it only gets to be as loud as the mistake actually is. Token-driven throughout
   (--danger-subtle / --danger-subtle-foreground, already tuned per light/dark by the theme), never a
   literal color — correct in both automatically, same as everywhere else this pattern is used. */
.oc-error-summary {
    display: flex; align-items: flex-start; gap: .6rem;
    padding: .7rem .85rem; border-radius: var(--radius);
}
.oc-error-summary:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
/* The icon reads clearly at a size that still leaves the words the loudest thing in the box — the same
   glyph at w-5 (20px) was the single biggest reason the box read as heavier than the one-line mistake
   it was reporting. */
.oc-error-summary > svg, .oc-error-summary > i { flex: none; margin-top: .1rem; }
.oc-error-summary strong { display: block; font-size: .85rem; font-weight: 600; line-height: 1.3; }
.oc-error-summary p { margin: .2rem 0 0; font-size: .8rem; line-height: 1.45; font-weight: 400; }
.oc-error-summary ul { margin: .3rem 0 0; padding-left: 1.05rem; list-style: disc; font-size: .8rem; line-height: 1.45; }
.oc-error-summary a { color: inherit; font-weight: 600; text-decoration: underline; text-underline-offset: 2px; }

/* Confirms a resend click actually registered (AccountController.Verify's own comment covers why this
   cannot use the shared flash mechanism). Quiet on purpose: success tokens, not danger ones, and no box
   at all — a full alert for "we did the thing you asked" would outweigh the news. */
.oc-resend-confirm { display: flex; align-items: center; gap: .4rem; color: var(--success-subtle-foreground); }
.oc-resend-confirm svg, .oc-resend-confirm i { flex: none; color: var(--success); }

/* ---------- Session countdown banner (forms.md idle-session) ---------- */
.oc-session-warn { position: sticky; top: 0; z-index: 30; margin-bottom: 1rem; }
.oc-session-warn[hidden] { display: none; }
.oc-session-count { font-variant-numeric: tabular-nums; font-weight: 700; }

/* ---------- Buttons mid-action (§9): width must not collapse when the label changes ---------- */
.btn[data-oc-busy="true"] { pointer-events: none; opacity: .75; }
.btn[data-oc-busy="true"] .oc-btn-label { visibility: hidden; }
.btn[data-oc-busy="true"] .oc-btn-busy { display: inline-flex; }
.oc-btn-busy { display: none; position: absolute; inset: 0; align-items: center; justify-content: center; gap: .4rem; }
.btn:has(.oc-btn-busy) { position: relative; }

/* ---------- Long values (§97/§148) ----------
   A 300-character document description must not stretch a table row off the page. Clamped to a fixed
   number of lines, with the full text on the title attribute so nothing is actually hidden from the
   reader — truncation without a reveal is what §97 forbids. */
.oc-clamp-1, .oc-clamp-2 { display: -webkit-box; -webkit-box-orient: vertical; overflow: hidden; }
.oc-clamp-1 { -webkit-line-clamp: 1; line-clamp: 1; }
.oc-clamp-2 { -webkit-line-clamp: 2; line-clamp: 2; }

/* ---------- Skip link (§93) ----------
   Off-screen until focused, then a real, visible target — the standard pattern. Without it a keyboard
   user tabs the entire sidebar before reaching the page content, on every page. */
.oc-skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  padding: .6rem 1rem; border-radius: 0 0 var(--radius) 0;
  background: var(--brand-600); color: var(--primary-foreground);
  font-size: .85rem; font-weight: 600; text-decoration: none;
}
.oc-skip-link:focus-visible { left: 0; outline: 2px solid var(--ring); outline-offset: 2px; }

/* ---------- Progressive disclosure on the priced-options form ----------
   Two fields are enough for a fixed fee; quantity, cadence, tax and optionality describe cases most
   proposals never hit. <details> hides them with no script and no inline handler (portal-ux §4, P0). */
.oc-disclosure { border: 1px solid var(--line); border-radius: var(--radius); margin: .35rem 0 1rem; }
.oc-disclosure > summary {
  cursor: pointer; padding: .6rem .85rem; font-size: .82rem; font-weight: 600; color: var(--ink-600);
  list-style: none; display: flex; align-items: center; gap: .45rem;
}
.oc-disclosure > summary::-webkit-details-marker { display: none; }
.oc-disclosure > summary::before { content: "+"; font-weight: 700; color: var(--brand-600); }
.oc-disclosure[open] > summary::before { content: "\2212"; }
.oc-disclosure > summary:hover { color: var(--ink-900); }
.oc-disclosure > summary:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
.oc-disclosure[open] > summary { border-bottom: 1px solid var(--line-2); }
.oc-disclosure > *:not(summary) { padding: 0 .85rem; }
.oc-disclosure > *:last-child { padding-bottom: .85rem; }

/* ---------- Editable table rows (priced options) ---------- */
.oc-mt-xs { margin-top: .3rem; }

/* Landing target for a link that carries a #fragment. Without the offset the browser aligns the element
   to the very top of the viewport, which tucks its heading under anything sticky and makes the jump look
   like it went to the wrong place. */
.oc-scroll-anchor { scroll-margin-top: 1.5rem; }

/* A control that is present but has nothing to act on. Reads as unavailable, still receives the click —
   which is the whole point: it answers when pressed (see data-oc-unavailable in app.js) instead of
   sitting there inert.

   Deliberately NOT .btn:disabled, which sets pointer-events: none and would swallow the click along with
   any chance of explaining itself. The cursor says "no" before the press, and the toast says why after. */
.oc-brand-octacer .is-unavailable,
.is-unavailable {
    opacity: .55;
    cursor: not-allowed;
    box-shadow: none;
}
.is-unavailable:hover { opacity: .7; }
/* Numbers read right-aligned in a table, so the input they are edited in does too (portal-ux §21). */
.oc-input-num { text-align: right; max-width: 7.5rem; margin-left: auto; }

/* ---------- Contract signature modal (contract-signature.js / contract-signature-image.js) ----------
   The native file input, styled, looks like nothing else in the app (browser chrome, not theme) — so
   both file pickers here are a real <label for=...> driving a visually-hidden input (.sr-only keeps it
   in the constraint-validation tree, unlike display:none), styled to match .oc-list-item/.oc-dropzone's
   card language instead. */
.oc-file-picker {
    display: flex; flex-direction: column; align-items: center; justify-content: center; gap: .375rem;
    width: 100%; padding: 1.1rem 1rem; text-align: center; cursor: pointer;
    border: 1.5px dashed var(--line-strong); border-radius: var(--radius); background: var(--surface-muted);
    transition: border-color .15s ease, background .15s ease;
}
.oc-file-picker:hover { border-color: var(--brand-500); background: var(--brand-50); }
.oc-file-picker:focus-within { outline: none; box-shadow: var(--ring); }
.oc-file-picker.has-file { border-style: solid; border-color: color-mix(in srgb, var(--success-subtle-foreground) 40%, transparent); background: var(--success-subtle); }
.oc-file-picker-icon {
    display: inline-flex; align-items: center; justify-content: center; width: 2.25rem; height: 2.25rem;
    border-radius: 999px; background: var(--surface); color: var(--ink-500); box-shadow: var(--shadow-sm);
}
.oc-file-picker.has-file .oc-file-picker-icon { color: var(--success-subtle-foreground); }
.oc-file-picker-name { font-size: .8125rem; font-weight: 600; color: var(--ink-900); word-break: break-word; }
.oc-file-picker-hint { font-size: .72rem; color: var(--ink-500); }

.oc-signature-preview {
    display: flex; align-items: center; gap: .625rem; margin-top: .625rem; padding: .625rem .75rem;
    border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface-muted);
    font-size: .8125rem; color: var(--ink-700);
}
.oc-signature-preview img { height: 2.25rem; max-width: 6rem; object-fit: contain; border-radius: .375rem; background: #fff; border: 1px solid var(--line); }
.oc-signature-preview span { flex: 1 1 auto; min-width: 0; }
.oc-signature-preview .btn { flex: 0 0 auto; }
/* Same fix as proposal.css: display:flex above outranks the browser's [hidden] rule, and
   signature-pad.js hides the preview with that attribute until a file is chosen. */
.oc-signature-preview[hidden] { display: none; }

.oc-signature-pad {
    display: block; width: 100%; max-width: 26.25rem; height: auto; aspect-ratio: 420 / 160;
    border: 1.5px dashed var(--line-strong); border-radius: var(--radius); background: #fff; cursor: crosshair;
    touch-action: none;
}
.oc-table td .field-input-sm,
.oc-table td .field-select { width: 100%; }
.oc-table td .oc-input-num { width: 100%; }

/* ---------- Roomier term lists (opt-in — .oc-dl's own base rule is shared by five other pages) ----------
   Contract Overview's own cards (portal-ux readability pass): the base .oc-dl's dt/dd sizes sit close
   together (.82rem/.875rem) with a tight .5rem row gap, which reads fine for two or three rows but
   crowds a card holding five. Wider gap and a clearer size/weight split between label and value, so the
   eye can tell "this is the label" from "this is the answer" without reading either. */
.oc-dl-relaxed { gap: .85rem 1.25rem; }
.oc-dl-relaxed dt { font-size: .78rem; }
.oc-dl-relaxed dd { font-size: .95rem; font-weight: 600; }

/* ---------- Priced options: a list of items, not a spreadsheet ----------
   The screen used to be an always-open table of every field on every row. Most proposals are one fixed
   fee, so the default is now a line you can read — name, price, tax, cadence — and the form only appears
   when you ask for it. */
.oc-price-item { border: 1px solid var(--line); border-radius: var(--radius); padding: .85rem 1rem; margin-bottom: .7rem; }
.oc-price-item-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
.oc-price-item-name { font-weight: 600; color: var(--ink-900); }

/* The line's own figure, under its fields. Right-aligned so the eye can add the column up. */
.oc-price-item-total {
    margin-top: .75rem; padding-top: .75rem; border-top: 1px solid var(--line-2);
    text-align: right; font-size: .85rem; color: var(--ink-500);
}
.oc-price-item-total strong { color: var(--ink-900); font-weight: 700; }

/* Subtotal / tax / total for the whole section. A definition list because that is what it is: three
   labelled figures, not a table of records. */
/* A standing note that warns rather than merely informs. Bare .is-warning is not a text colour in the
   theme — it only means something compounded onto a table cell (see the note above .oc-table td.is-warning)
   — so this is its own class rather than a borrowed one that silently does nothing. */
.oc-note-warning { color: var(--warning); }

.oc-price-summary { display: flex; flex-direction: column; gap: .45rem; }
.oc-price-summary > div { display: flex; justify-content: space-between; align-items: baseline; gap: 1rem; }
.oc-price-summary dt { color: var(--ink-500); font-size: .875rem; }
.oc-price-summary dd { color: var(--ink-900); font-weight: 600; }
.oc-price-summary .is-total {
    border-top: 1px solid var(--line); padding-top: .55rem; margin-top: .15rem;
}
.oc-price-summary .is-total dt { color: var(--ink-900); font-weight: 600; }
.oc-price-summary .is-total dd { font-size: 1.05rem; font-weight: 700; }
.oc-price-item-facts { display: flex; flex-wrap: wrap; gap: .35rem; margin-top: .3rem; font-size: .85rem; color: var(--ink-600); }
.oc-price-item-facts strong { color: var(--ink-900); font-size: .95rem; }
.oc-price-item-form { border-top: 1px solid var(--line-2); margin-top: .85rem; padding-top: .85rem; }

/* ---------- Pricing method picker ----------
   Step one of pricing a proposal: four ways a client can be billed, each its own card. The radio strip
   further down does the same job for someone CHANGING the method later; this is the first-time choice, so
   each option gets a real hit area and its explanation on the face of it rather than in a title attribute.

   auto-fit rather than a fixed four columns: at a narrow builder width they fall to two and then one,
   which is what keeps the card from crushing its own sentence. */
.oc-method-grid {
    display: grid; gap: .75rem;
    grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
}
.oc-method-card {
    display: flex; flex-direction: column; align-items: flex-start; gap: .3rem;
    padding: .9rem; text-align: left; cursor: pointer;
    border: 1px solid var(--line); border-radius: var(--radius);
    background: var(--surface); color: var(--ink-900);
    transition: border-color .12s ease, background-color .12s ease, transform .12s ease;
}
.oc-method-card:hover { border-color: var(--primary); background: var(--surface-muted); }
.oc-method-card:active { transform: translateY(1px); }
.oc-method-card-ic { color: var(--primary); }
.oc-method-card-t { font-weight: 600; }
.oc-method-card-b { font-size: .8rem; color: var(--ink-500); text-wrap: pretty; }

/* ---------- Radio choice set (pricing type) ----------
   Three options whose meaning IS the decision, so each carries its explanation instead of hiding it
   inside a select the reader has to open to compare (portal-ux §47-§52). */
.oc-choice-set { border: 0; padding: 0; margin: 0 0 1rem; display: grid; gap: .5rem; }
.oc-choice-set > legend { padding: 0; margin-bottom: .4rem; }
.oc-choice { display: flex; align-items: flex-start; gap: .65rem; padding: .7rem .85rem; cursor: pointer;
  border: 1px solid var(--line); border-radius: var(--radius); transition: border-color .15s ease, background .15s ease; }
.oc-choice:hover { border-color: var(--brand-600); background: var(--line-2); }
.oc-choice:has(input:checked) { border-color: var(--brand-600); background: color-mix(in srgb, var(--brand-500) 10%, transparent); }
.oc-choice:has(input:focus-visible) { outline: 2px solid var(--ring); outline-offset: 2px; }
.oc-choice input { margin-top: .15rem; flex: none; }
.oc-choice > span { display: flex; flex-direction: column; gap: .1rem; }
.oc-choice strong { font-size: .9rem; color: var(--ink-900); }

/* ---------- The single save bar ----------
   Sticky, because one Save for a long page is only an improvement if you can reach it from anywhere on
   that page. */
.oc-save-bar { position: sticky; bottom: 0; z-index: 20;
  display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap;
  padding: .85rem 1.15rem; margin-top: 1.25rem;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md); }

/* ---------- Right-aligned table headers ----------
   The theme sets `.oc-table thead th { text-align: left }` at specificity (0,2,1); Tailwind's `.text-right`
   is (0,1,0) and loses. So every numeric column had a left-aligned HEADER over right-aligned CELLS —
   Agreed total, Amount, Outstanding, Value, Size. Matching the theme's specificity fixes all of them
   (portal-ux §21: numbers and their headings align right, consistently). */
.oc-table thead th.text-right { text-align: right; }
.oc-table thead th.text-right .oc-sort { justify-content: flex-end; }

/* ---------- Contract signing ---------- */
.oc-block { display: block; }
/* The typed signature looks like a signature, so the act reads as signing rather than filling a field. */
.oc-signature { font-family: "Segoe Script", "Brush Script MT", cursive; font-size: 1.15rem; }

/* ---------- Dark-mode corrections for surfaces the theme leaves to the app ---------- */
.dark .oc-skeleton { background: #1e293b; }
.dark .oc-skeleton::after { background: linear-gradient(90deg, transparent, rgba(255,255,255,.08), transparent); }
.dark .oc-subcard { background: var(--surface); }
/* =======================================================================================================
   MOBILE DRAWER — final word.

   This block is last in the file and uses !important on the two properties that decide whether the phone
   layout works at all. That is deliberate: the sidebar and content offset are set in several places (this
   file's shell, octacer-theme.css, and Tailwind utilities on the element), and chasing which one wins in
   which order is what made this take all afternoon. Below 1024px there is exactly one correct answer, so
   it is stated once, last, and unambiguously.

   Behaviour is the standard app drawer: content full-width, panel off-canvas, hamburger opens it over a
   dimmed backdrop, and it closes on the X, the backdrop, or Escape (octacer-sidebar.js owns those).
   ======================================================================================================= */
@media (max-width: 1023px) {
    /* Content owns the full width — nothing is reserved for a panel that is off-canvas. */
    .oc-content,
    html.oc-sidebar-collapsed .oc-content { margin-left: 0 !important; }

    /* Clearance for the fixed hamburger, and tighter gutters than the desktop 2rem. */
    .oc-main { padding: 3.75rem 1rem 3rem !important; max-width: none; }

    /* The panel: off-canvas until octacer-sidebar.js adds .is-open. Never wider than the screen. */
    .oc-sidebar {
        width: min(var(--sidebar-w), 85vw);
        transform: translateX(-100%);
        visibility: hidden;
        z-index: 60;                       /* above the backdrop below */
        box-shadow: var(--shadow-lg);
    }
    .oc-sidebar.is-open { transform: translateX(0); visibility: visible; }

    /* A collapsed rail is a desktop idea; in drawer mode the panel is always full width. */
    .oc-sidebar.is-collapsed { width: min(var(--sidebar-w), 85vw); }

    /* Backdrop sits under the panel and over the page. */
    .oc-sidebar-overlay { z-index: 55; }

    /* Drawer-only controls. */
    .oc-sidebar-mobile-button { display: inline-flex; }
    .oc-sidebar-close, .oc-sidebar [data-oc-sidebar-close] { display: inline-flex; }
    .oc-sidebar-collapse { display: none; }
}

@media (min-width: 1024px) {
    /* Desktop: the panel is permanent, so no drawer chrome. */
    .oc-sidebar-mobile-button { display: none; }
    .oc-sidebar-close, .oc-sidebar [data-oc-sidebar-close] { display: none; }
}

/* Carried over from main during the merge: used by Proposals/Builder.cshtml. The rest of
   main's block was a relocation of rules already present above (tokenised for dark mode). */
.oc-save-cancel-row .btn { min-width: 9.5rem; }

/* ---------- Dashboard ----------------------------------------------------------------------------------
   The theme owns .oc-stats-grid / .oc-stat / .oc-stat-icon / .oc-card-head. App-owned here: the tinted
   icon chips, the money and funnel grids, the attention accent, and the activity feed — the colour and
   rhythm that make the page scannable instead of a stack of equal-weight bars. All tokens, so dark holds.

   NOTE .oc-stat is `display:flex; align-items:center` — it expects ICON + a single wrapper <div>. Three
   sibling divs get flexed into a row, which is why the first cut rendered as full-width bars. */
.oc-stat-sub { margin-top: .2rem; font-size: .75rem; color: var(--ink-400); }

/* The theme's .oc-stat is `justify-content: space-between`, which pushes the icon and the figure to
   opposite ends of the card — fine for a wide two-column tile, wrong for the compact KPI shape where the
   chip should sit right beside its number. Left-align and let the gap do the spacing. */
/* The theme's grid is a hard `repeat(4, ...)` — correct for exactly four cards, but a page with five
   (Clients: On this page / Active / Prospect / Onboarding / Inactive) always strands the fifth alone on
   its own row below a wide gap. auto-fit sizes the row to however many cards actually exist instead of
   assuming four, so 3, 4, 5+ cards all sit on one line on a screen wide enough to hold them. Scoped to
   above the theme's own mobile breakpoint (640px) so its tuned 2-col/1-col mobile behavior is untouched —
   this only fixes the wide-screen wrapping, not the narrow one, which was never broken. */
@media (min-width: 641px) {
  .oc-stats-grid { grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); }
}
.oc-stats-grid > .oc-stat { justify-content: flex-start; gap: .9rem; padding: 1rem 1.1rem; }
.oc-stats-grid > .oc-stat .oc-stat-value { margin-top: 0; font-size: clamp(1.35rem, 4.5vw, 1.7rem); }
.oc-stats-grid > .oc-stat .oc-stat-label { margin-top: .1rem; }
/* Proposals page only (by request): the four count boxes 15% smaller — padding, icon and type. */
.oc-stats-grid.oc-stats-compact > .oc-stat { padding: .85rem .95rem; gap: .75rem; }
.oc-stats-grid.oc-stats-compact > .oc-stat .oc-stat-value { font-size: clamp(1.15rem, 3.8vw, 1.45rem); }
.oc-stats-grid.oc-stats-compact > .oc-stat .oc-stat-label { font-size: .69rem; }
.oc-stats-grid.oc-stats-compact .oc-stat-icon { width: 2.2rem; height: 2.2rem; border-radius: .65rem; }
.oc-stats-grid.oc-stats-compact .oc-stat-icon i { font-size: .9rem; }
/* Dashboard KPI row (by request): smaller cards — tighter padding, a smaller chip, the sub-line close under
   its label. Type keeps its Fluent roles (figure Title 3, label/sub Caption 1); only the box shrinks. */
.oc-stats-grid.oc-dash-kpis > .oc-stat { padding: .625rem .875rem; gap: .625rem; }
.oc-stats-grid.oc-dash-kpis .oc-stat-icon { width: 2rem; height: 2rem; border-radius: .5rem; }
.oc-stats-grid.oc-dash-kpis .oc-stat-icon i { font-size: .875rem; }
.oc-stats-grid.oc-dash-kpis .oc-stat-icon svg { width: .875rem; height: .875rem; }
.oc-stats-grid.oc-dash-kpis .oc-stat-sub { margin-top: 0; }

/* ---------- A stat card that is also a filter ----------
   Same card, now an <a>: it has to stop looking like link text (inherit colour, no underline) while
   gaining the two things a control owes the user — a pointer, and a visible focus ring for anyone who
   never touches the mouse. The theme's .oc-stat already lifts on hover, which reads as pressable. */
a.oc-stat-filter { text-decoration: none; color: inherit; cursor: pointer; }
a.oc-stat-filter:focus-visible { outline: none; border-color: var(--brand-500); box-shadow: var(--ring); }

/* The selected one has to be obvious at a glance, because it is the reason the list below is short.
   Border plus a tinted ground rather than a colour swap: the figure and label must stay as readable
   selected as not. */
a.oc-stat-filter.is-active {
    border-color: var(--brand-500);
    background: color-mix(in srgb, var(--brand-600) 7%, var(--surface));
    box-shadow: var(--shadow-sm);
}
.dark a.oc-stat-filter.is-active { background: color-mix(in srgb, var(--brand-500) 16%, var(--surface)); }

/* Icon chips. A Font Awesome <i> renders a FONT GLYPH, so width/height only sizes an empty box and the
   glyph floats off-centre inside it — size it with font-size and let the flex centring do its job. */
.oc-stat-icon { width: 2.6rem; height: 2.6rem; border-radius: .75rem; }
.oc-stat-icon i { font-size: 1.05rem; line-height: 1; }
.oc-stat-icon svg { width: 1.05rem; height: 1.05rem; }

/* Tints derived from the BASE status colour with color-mix, not from the `-subtle` token pairs. The base
   colours are identical in light and dark, so a translucent mix over the card surface lands correctly in
   both modes automatically. The -subtle pairs would not: --brand-50/-700 have no dark override at all,
   which is why the wallet chip stayed near-white on a dark card. */
.oc-stat-icon-brand   { background: color-mix(in srgb, var(--brand-600) 14%, transparent); color: var(--brand-600); }
.oc-stat-icon-danger  { background: color-mix(in srgb, var(--danger)    16%, transparent); color: var(--danger); }
.oc-stat-icon-success { background: color-mix(in srgb, var(--success)   16%, transparent); color: var(--success); }
.oc-stat-icon-warning { background: color-mix(in srgb, var(--warning)   18%, transparent); color: var(--warning); }
.oc-stat-icon-muted   { background: color-mix(in srgb, var(--ink-500)   14%, transparent); color: var(--ink-500); }
/* Clients' Leads card — the one tone that had no style, so its icon sat on no tint at all. */
.oc-stat-icon-info    { background: color-mix(in srgb, var(--info, #0ea5e9) 16%, transparent); color: var(--info, #0ea5e9); }

/* Dark mode needs a touch more fill and a lighter glyph, or a 14% wash on a near-black card disappears. */
.dark .oc-stat-icon-brand   { background: color-mix(in srgb, var(--brand-500) 24%, transparent); color: #a5b4fc; }
.dark .oc-stat-icon-danger  { background: color-mix(in srgb, var(--danger)    26%, transparent); color: #fca5a5; }
.dark .oc-stat-icon-success { background: color-mix(in srgb, var(--success)   26%, transparent); color: #6ee7b7; }
.dark .oc-stat-icon-warning { background: color-mix(in srgb, var(--warning)   28%, transparent); color: #fcd34d; }
.dark .oc-stat-icon-muted   { background: color-mix(in srgb, var(--ink-400)   20%, transparent); color: var(--ink-400); }

/* Dashboard composition. The page is a set of SUMMARY panels, so the layout carries the hierarchy:
   the attention queue is widest, activity rides beside it, the two summary widgets share a row, and only
   the table — whose five columns are genuinely compared across rows — takes the full width. */
.oc-dash-kpis { margin-bottom: .75rem; gap: .75rem; }
/* stretch, not start: paired panels should end on the same line even when one has more rows than the
   other. The panel is a column flex so its header stays put and the content fills what is left. */
.oc-dash-row { display: grid; gap: .75rem; margin-bottom: .75rem; align-items: stretch; }
.oc-dash-row { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); }   /* ~66 / 33 */
.oc-dash-row.is-even { grid-template-columns: repeat(2, minmax(0, 1fr)); }
/* A panel whose companion is absent (no finance.read, no audit) takes the row rather than leaving a hole. */
.oc-dash-row.is-single { grid-template-columns: minmax(0, 1fr); }
.oc-dash-panel { margin-bottom: 0; display: flex; flex-direction: column; }
/* The scroll region and the key/value list take the slack, so a short panel pads out rather than
   leaving its card floating above the row's baseline. */
.oc-dash-panel > .oc-table-scroll, .oc-dash-panel > .oc-kv,
.oc-dash-panel > .oc-attention-list { flex: 1 1 auto; }
.oc-dash-panel > .oc-empty { flex: 1 1 auto; display: flex; flex-direction: column;
  align-items: center; justify-content: center; }
.oc-dash-panel .oc-card-head { padding: .7rem 1rem; }
.oc-dash-panel .oc-table-compact thead th { padding: .45rem .75rem; }
.oc-dash-panel .oc-table-compact tbody td { padding: .5rem .75rem; }

/* The header link, wherever it appears, reads the same on every panel. */
.oc-card-link { font-size: .8rem; font-weight: 600; white-space: nowrap; }
.oc-card-foot { padding: .45rem 1rem; border-top: 1px solid var(--line-2); }

/* Money and Pipeline: label left, figure right, one row each — the shape a summary is read in. */
.oc-kv { margin: 0; padding: .35rem .35rem .5rem; }
.oc-kv > div { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem;
  padding: .42rem .65rem; border-radius: .45rem; }
.oc-kv > div:hover { background: var(--line-2); }
.oc-kv dt { font-size: .82rem; color: var(--ink-600); }
.oc-kv dd { margin: 0; font-size: .95rem; font-weight: 700; color: var(--ink-900); font-variant-numeric: tabular-nums; }
/* A zero is information, so it stays legible — just quieter than a number that needs acting on. */
.oc-kv > div.is-zero dt, .oc-kv > div.is-zero dd { color: var(--ink-400); font-weight: 500; }

/* Long client and project names must clip, not stretch the panel out of its column. */
.oc-cell-clip { max-width: 15rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.oc-empty-sm { padding: 1.75rem 1rem; }

/* ---- Queue rows -------------------------------------------------------------------------------------
   Used by the CLIENT PORTAL's "Needs attention" and "Active projects" panels, which sit side by side.
   Each row is read and acted on rather than compared down a column, and at half width there was nowhere
   for five table columns to go — so it is a list (§125). The reason wraps in full; it is the whole point
   of the row and must never be the thing that gets truncated. */
.oc-queue { list-style: none; margin: 0; padding: 0; }
.oc-queue-item { display: flex; align-items: flex-start; gap: .7rem;
  padding: .7rem 1rem .7rem .8rem; border-bottom: 1px solid var(--line-2);
  border-left: 3px solid transparent; transition: background .15s ease; }
.oc-queue-item:last-child { border-bottom: 0; }
.oc-queue-item:hover { background: var(--line-2); }

/* Severity as a left edge, so urgency registers before the badge is read. */
.oc-attn-0 { border-left-color: var(--danger); }
.oc-attn-1 { border-left-color: var(--warning); }

.oc-queue-icon { flex: 0 0 auto; width: 1.6rem; height: 1.6rem; margin-top: .1rem; border-radius: .45rem;
  display: grid; place-items: center; background: var(--line-2); color: var(--ink-500); }
/* Html.Icon renders <i class="fa-solid fa-x w-4 h-4">: the w-4/h-4 box is 1rem square while the glyph is narrower,
   so the glyph sat off-centre inside the tile. Auto size + line-height 1 lets the tile (already a centring grid
   or flex box) centre the glyph itself — the same fix as .oc-stat-icon / .oc-icon-box. Portal "Needs attention"
   rows (.oc-queue-icon) and the Admin Dashboard ones (.oc-attention-ic). */
.oc-queue-icon i { font-size: .8rem; width: auto; height: auto; line-height: 1; }
.oc-attention-ic i { width: auto; height: auto; line-height: 1; }
.oc-attn-0 .oc-queue-icon { background: color-mix(in srgb, var(--danger) 14%, transparent); color: var(--danger); }
.oc-attn-1 .oc-queue-icon { background: color-mix(in srgb, var(--warning) 16%, transparent); color: var(--warning); }

.oc-queue-body { min-width: 0; flex: 1 1 auto; }
.oc-queue-top { display: flex; align-items: baseline; flex-wrap: wrap; gap: .35rem; }
.oc-queue-title { font-size: .875rem; font-weight: 600; color: var(--ink-900); text-decoration: none;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
.oc-queue-title:hover { color: var(--brand-700); }
.oc-queue-sep { color: var(--ink-400); }
.oc-queue-client { font-size: .78rem; color: var(--ink-600); }
.oc-queue-reason { margin: .15rem 0 0; font-size: .8rem; line-height: 1.4; color: var(--ink-600); }
.oc-queue-meter { margin-top: .45rem; }

.oc-queue-meta { flex: 0 0 auto; display: flex; flex-direction: column; align-items: flex-end; gap: .25rem; }

@media (max-width: 520px) {
    /* Narrow: the badge moves up beside the title rather than squeezing the reason into a sliver. */
    .oc-queue-item { flex-wrap: wrap; }
    .oc-queue-meta { flex-direction: row; align-items: center; width: 100%; padding-left: 2.3rem; }
}

/* Client-visible progress, as a bar rather than a fraction to read. */
.oc-progress { width: 100%; max-width: 6rem; height: .35rem; border-radius: 999px; background: var(--line-2); overflow: hidden; }
.oc-progress-fill { display: block; height: 100%; border-radius: 999px; background: var(--brand-600); }

/* NEEDS ATTENTION — what replaced the activity feed. Every row is a whole-width link: the count is the
   thing being clicked, and a target the width of the card is the easier one to hit than a trailing
   "view" button. Urgent rows (overdue, expiring) carry the danger tone on the icon and the figure only —
   colour marks WHICH rows matter, and a row painted red end to end stops marking anything. */
.oc-attention-list { list-style: none; margin: 0; padding: 0; }
.oc-attention-list li { border-bottom: 1px solid var(--line-2); }
.oc-attention-list li:last-child { border-bottom: 0; }
.oc-attention-row {
    display: flex; align-items: center; gap: .75rem; padding: .7rem 1.25rem;
    color: inherit; text-decoration: none; transition: background-color .12s ease;
}
.oc-attention-row:hover { background: var(--surface-muted); }
.oc-attention-ic {
    flex: 0 0 auto; width: 2rem; height: 2rem; border-radius: .6rem;
    display: inline-flex; align-items: center; justify-content: center;
    background: var(--brand-50); color: var(--primary);
}
/* --danger-subtle, not a literal: it is redefined for dark mode, where a pale red panel would glow. */
.oc-attention-ic.is-urgent { background: var(--danger-subtle); color: var(--danger); }
.oc-attention-body { display: flex; flex-direction: column; min-width: 0; }
.oc-attention-label { font-size: .875rem; font-weight: 600; color: var(--ink-900); }
.oc-attention-detail { font-size: .78rem; color: var(--ink-500); }
/* margin-left:auto rather than a spacer element, so the count and chevron stay together on the right
   however long the label wraps. */
.oc-attention-count { margin-left: auto; font-size: 1.05rem; font-weight: 700; color: var(--ink-900); }
.oc-attention-count.is-urgent { color: var(--danger); }
.oc-attention-go { flex: 0 0 auto; color: var(--ink-400); }
.oc-attention-row:hover .oc-attention-go { color: var(--primary); }

/* The all-clear. Same bar as the tip, in the success tone — it is good news, not advice. */
.oc-tipbar-ok { border-color: var(--success); }
.oc-tipbar-ok .oc-tipbar-icon { color: var(--success); }

/* A pipeline stage with something in it is a link to that filtered list; the figure beside it is not,
   so only the words carry the underline. */
.oc-kv-linked dt a { text-decoration: none; }
.oc-kv-linked dt a:hover { text-decoration: underline; }

/* Tablet: the asymmetric row loses its second column before the even row does — activity is the least
   important panel, so it is the first to drop under. */
@media (max-width: 1100px) {
    .oc-dash-row { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 820px) {
    .oc-dash-row.is-even { grid-template-columns: minmax(0, 1fr); }
    .oc-cell-clip { max-width: 11rem; }
}

/* ---------- Record tiles (card grid replacing a list table) --------------------------------------------
   portal-ux §96 warns against auto-converting tables to cards, because a table preserves column
   comparison. This is a deliberate, permanent choice for the CLIENT list specifically: a client is a
   profile people recognise by name and status, not a row anyone scans across ten aligned columns. The
   tiles keep identity, status, the same facts and the SAME permission rules as the table did.
   Compact by design — a list of tiles is only useful if several fit on one screen. Tokens throughout,
   so light and dark both hold. */
/* No align-items: start — the row stretches every card to the tallest in it, so a card with no summary
   line ends level with one that has three. The footer's margin-top:auto pins the actions to the bottom
   of whatever height the row settles on. */
.oc-card-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .75rem; align-items: stretch; }

.oc-tile { display: flex; flex-direction: column; min-width: 0;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--shadow-sm); transition: box-shadow .2s, transform .2s, border-color .2s; }
.oc-tile:hover { box-shadow: var(--shadow-md); transform: translateY(-1px); border-color: var(--line-strong); }

.oc-tile-head { display: flex; align-items: center; gap: .55rem; padding: .7rem .8rem .6rem; }
.oc-tile-avatar { width: 1.9rem; height: 1.9rem; border-radius: .55rem; flex: 0 0 auto;
  display: grid; place-items: center; font-size: .68rem; font-weight: 700;
  background: color-mix(in srgb, var(--brand-600) 14%, transparent); color: var(--brand-600); }
.dark .oc-tile-avatar { background: color-mix(in srgb, var(--brand-500) 24%, transparent); color: #a5b4fc; }
.oc-tile-name { display: block; font-size: .9rem; font-weight: 700; color: var(--ink-900); text-decoration: none;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.oc-tile-name:hover { color: var(--brand-700); }
.oc-tile-slug { font-size: .68rem; color: var(--ink-400); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.oc-tile-badge { margin-left: auto; flex: 0 0 auto; }

/* All six facts in one 3x2 grid — four label/value pairs plus the two counts, which stay links. */
.oc-tile-facts { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .5rem .6rem;
  align-content: start; flex: 1 1 auto;
  margin: 0; padding: .6rem .8rem; border-top: 1px solid var(--line-2); }
.oc-tile-facts > div { min-width: 0; }
.oc-tile-facts dt { font-size: .62rem; text-transform: uppercase; letter-spacing: .04em; color: var(--ink-400); }
.oc-tile-facts dd { margin: .05rem 0 0; font-size: .8rem; color: var(--ink-700);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.oc-tile-facts dd .oc-link { font-weight: 700; }

/* Icon-only actions: small, quiet, and each with an accessible name in the markup (§6). */
.oc-tile-foot { display: flex; align-items: center; gap: .15rem; margin-top: auto;
  padding: .35rem .5rem; border-top: 1px solid var(--line-2); }
.oc-tile-action { display: inline-flex; align-items: center; justify-content: center;
  width: 1.85rem; height: 1.85rem; border: 0; background: none; cursor: pointer; border-radius: .45rem;
  color: var(--ink-500); text-decoration: none; }
.oc-tile-action:hover { background: var(--line-2); color: var(--ink-900); }
.oc-tile-action i { font-size: .85rem; }
.oc-tile-action.is-danger { color: var(--danger); margin-left: auto; }
.oc-tile-action.is-danger:hover { background: color-mix(in srgb, var(--danger) 12%, transparent); }

/* Touch targets must not shrink below the theme's coarse-pointer minimum. */
@media (pointer: coarse) { .oc-tile-action { width: 2.5rem; height: 2.5rem; } }

/* The delivery switch and its Delivered button share the footer with the icon actions, so they are
   sized to that row rather than to a form: the theme's .oc-toggle at its own scale, and a label small
   enough that the pair reads as one control instead of two competing buttons. The whole label is the
   hit area — the visible 40x22 track alone is under the pointer minimum. */
.oc-tile-switch { display: inline-flex; align-items: center; gap: .4rem; cursor: pointer;
  padding: .25rem .4rem; border-radius: .45rem; font-size: .7rem; font-weight: 700;
  letter-spacing: .06em; text-transform: uppercase; color: var(--ink-500); }
.oc-tile-switch:hover { background: var(--line-2); color: var(--ink-900); }
.oc-tile-switch .oc-toggle { width: 2.05rem; height: 1.15rem; }
.oc-tile-switch .oc-toggle-track::before { height: .85rem; width: .85rem; left: .15rem; top: .15rem; }
.oc-tile-switch .oc-toggle input:checked + .oc-toggle-track::before { transform: translateX(.9rem); }
.oc-tile-deliver { font-size: .7rem; padding: .3rem .55rem; gap: .3rem; }
.oc-tile-deliver:hover { color: var(--success); }

@media (max-width: 1100px) { .oc-card-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 700px)  { .oc-card-grid { grid-template-columns: 1fr; } }

/* ---- Record-card additions: the shapes the other five lists need that Clients did not ----
   Same tile, different payload. Every list page now renders cards, so anything a table cell used
   to carry has to have a home here — an icon instead of initials, a line that WRAPS for free text,
   and a figure that is allowed to be the loudest thing on the card. */

/* Identity is an icon, not initials, wherever the record has no name to abbreviate. */
.oc-tile-icon { width: 1.9rem; height: 1.9rem; border-radius: .55rem; flex: 0 0 auto;
  display: grid; place-items: center;
  background: color-mix(in srgb, var(--brand-600) 12%, transparent); color: var(--brand-600); }
.dark .oc-tile-icon { background: color-mix(in srgb, var(--brand-500) 22%, transparent); color: #a5b4fc; }
.oc-tile-icon i { font-size: .85rem; }
/* Tinted by state where the state is the point: an overdue invoice, an unpublished contract. */
.oc-tile-icon.is-danger  { background: color-mix(in srgb, var(--danger) 14%, transparent);  color: var(--danger); }
.oc-tile-icon.is-warning { background: color-mix(in srgb, var(--warning) 16%, transparent); color: var(--warning); }
.oc-tile-icon.is-success { background: color-mix(in srgb, var(--success) 14%, transparent); color: var(--success); }

/* The client line under the title. A link, unlike .oc-tile-slug, which is a plain reference. */
.oc-tile-sub { display: block; font-size: .7rem; color: var(--ink-500); text-decoration: none;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
a.oc-tile-sub:hover { color: var(--brand-700); text-decoration: underline; }

/* Free text — a proposal's feedback, a project's summary. It WRAPS: this is the content that the
   table clipped into uselessness, so it is never the thing that gets truncated again. Clamped to
   three lines so one wordy row cannot stretch its whole grid row. */
.oc-tile-note { margin: 0; padding: .5rem .8rem 0; font-size: .75rem; line-height: 1.45; color: var(--ink-600);
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
.oc-tile-note.is-warning { color: var(--warning); }
/* Feedback is the note AND the way into the full text, so it is a button that looks like the note. */
.oc-tile-note-button { width: 100%; border: 0; background: none; cursor: pointer; font: inherit; }
.oc-tile-note-button:hover { color: var(--brand-700); }

/* The figure a money card exists for. Sits between head and facts. */
.oc-tile-money { display: flex; align-items: baseline; justify-content: space-between; gap: .5rem;
  padding: .45rem .8rem .1rem; }
.oc-tile-amount { font-size: 1.15rem; font-weight: 700; color: var(--ink-900); letter-spacing: -.01em; }
.oc-tile-amount-sub { font-size: .7rem; color: var(--ink-500); white-space: nowrap; }
.oc-tile-amount-sub.is-danger { color: var(--danger); font-weight: 600; }

/* A fact that is a problem, not a value: a past target date, nothing shared with the client. */
.oc-tile-facts dd.is-danger  { color: var(--danger); font-weight: 600; }
.oc-tile-facts dd.is-warning { color: var(--warning); font-weight: 600; }

/* Footer actions that POST. The form is a wrapper with no box of its own. */
.oc-tile-foot form { margin: 0; display: inline-flex; }

/* Cards are not paginated, so the count has to say when the list is longer than what is shown —
   silence here would read as "this is all of it". */
.oc-list-note { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap;
  margin-top: .75rem; font-size: .75rem; color: var(--ink-500); }
.oc-list-note i { font-size: .75rem; }


/* The tab label already names the section, so a panel carries only its action row — right-aligned so it
   sits under the tablist without a redundant heading. */
.oc-tab-actions { display: flex; justify-content: flex-end; margin-bottom: .75rem; }

/* ---- Dashboard: create menu, welcome panel, tip bar ------------------------------------------------
   The dashboard opens on an EMPTY workspace as often as a full one — a fresh install, or the moment
   after a clear-out — and the empty case is the one these three exist for. */

.oc-create-menu { position: relative; }
.oc-create-toggle { display: inline-flex; align-items: center; gap: .4rem; }
.oc-create-caret { display: inline-flex; margin-left: .1rem; opacity: .8; }
.oc-create-caret i { font-size: .7rem; }
.oc-create-list { position: absolute; right: 0; top: calc(100% + .35rem); z-index: 50;
  min-width: 13rem; padding: .3rem;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--shadow-md); }
.oc-create-list a { display: flex; align-items: center; gap: .55rem;
  padding: .5rem .6rem; border-radius: .45rem;
  font-size: .85rem; color: var(--ink-700); text-decoration: none; white-space: nowrap; }
.oc-create-list a:hover { background: var(--line-2); color: var(--ink-900); }
.oc-create-list a i { width: 1rem; font-size: .8rem; color: var(--ink-400); }
.oc-create-list a:hover i { color: var(--brand-600); }

/* The welcome panel. Centred and generous on purpose: on an empty workspace it IS the page, so it
   carries the weight the summary panels would have had. */
.oc-welcome { text-align: center; padding: 2.5rem 1.5rem; margin-bottom: .75rem; }
.oc-welcome-icon { width: 3.25rem; height: 3.25rem; margin: 0 auto .9rem; border-radius: .9rem;
  display: grid; place-items: center;
  background: color-mix(in srgb, var(--brand-600) 10%, transparent); color: var(--brand-600); }
.oc-welcome-icon i { font-size: 1.4rem; }
.oc-welcome-title { margin: 0; font-size: 1.15rem; font-weight: 700; color: var(--ink-900); letter-spacing: -.01em; }
.oc-welcome-text { max-width: 46rem; margin: .5rem auto 0; font-size: .875rem; line-height: 1.6; color: var(--ink-500); }
.oc-welcome-actions { display: flex; flex-wrap: wrap; justify-content: center; gap: .5rem; margin-top: 1.25rem; }
.oc-welcome-note { margin: 1rem 0 0; font-size: .82rem; color: var(--ink-400); }

/* The tip bar. Quiet by design — it sits under everything and must never compete with the KPI row. */
.oc-tipbar { display: flex; align-items: center; gap: .75rem;
  padding: .8rem 1rem; border-radius: var(--radius);
  background: color-mix(in srgb, var(--brand-600) 6%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--brand-600) 16%, transparent); }
.oc-tipbar-icon { flex: 0 0 auto; width: 2rem; height: 2rem; border-radius: .55rem;
  display: grid; place-items: center;
  background: color-mix(in srgb, var(--brand-600) 14%, transparent); color: var(--brand-600); }
.oc-tipbar-icon i { font-size: .85rem; }
.oc-tipbar-title { font-size: .85rem; font-weight: 600; color: var(--ink-900); }
.oc-tipbar-text { font-size: .78rem; color: var(--ink-500); margin-top: .1rem; }
.oc-tipbar-action { margin-left: auto; flex: 0 0 auto; }

@media (max-width: 640px) {
    .oc-tipbar { flex-wrap: wrap; }
    .oc-tipbar-action { margin-left: 0; width: 100%; }
}

/* Tabs that NAVIGATE. The theme's .oc-tab is a button for Alpine panel switching; the archived list is
   a separate request with its own URL, so these are anchors wearing the same clothes — one tab strip
   in the app, whichever mechanism sits behind it. */
.oc-tabs-nav { margin-bottom: 1rem; }
a.oc-tab { text-decoration: none; display: inline-flex; align-items: center; gap: .4rem; }
a.oc-tab i { font-size: .8rem; }


/* ═══════════════════════════════════════════════════════════════════════════════════════════════════
   ARRIVAL — the one authored moment
   ═══════════════════════════════════════════════════════════════════════════════════════════════════
   Every piece of motion this app already had was REACTIVE: hover (.oc-table tbody tr, .oc-ktile),
   focus-within, oc-menu-in, oc-popover-in, oc-shimmer, oc-spin. All of it waits for the user to touch
   something first, so a freshly loaded page is completely inert — which is the single biggest reason a
   correctly-built portal still reads as a generic admin template.

   This is deliberately ONE choreography, not a set of separate effects: every element below animates
   the same keyframe on the same easing over the same duration, and only the delay differs. That is
   what makes it read as one page arriving rather than as decoration sprinkled per component.

   The keyframe declares only `from`, so the destination is each element's real resting style — nothing
   here ever sets opacity:0 as a base declaration, and a browser that runs no animation at all shows a
   fully visible page. */
:root {
    /* Exponential ease-out: most of the distance is covered immediately, so the movement reads as
       arriving rather than sliding. One easing for the whole choreography. */
    --oc-ease-out: cubic-bezier(.16, 1, .3, 1);
    --oc-arrive: 460ms;
    --oc-step: 34ms;
}

@keyframes oc-arrive { from { opacity: 0; transform: translateY(7px); } }

/* backwards fill holds the pre-animation state through the stagger delay; without it every row would
   flash in at full opacity and then animate, which defeats the sequence. */
.oc-table tbody tr,
.oc-record-list > .oc-record,
.oc-ktiles > .oc-ktile {
    animation: oc-arrive var(--oc-arrive) var(--oc-ease-out) backwards;
}

/* The stagger. CSS cannot loop, so the sequence is written out and then capped — past ~10 rows the
   delay stops growing, because a 40-row invoice table must not take 1.4s to finish appearing. */
.oc-table tbody tr:nth-child(1),  .oc-record-list > .oc-record:nth-child(1),  .oc-ktiles > .oc-ktile:nth-child(1)  { animation-delay: 0ms; }
.oc-table tbody tr:nth-child(2),  .oc-record-list > .oc-record:nth-child(2),  .oc-ktiles > .oc-ktile:nth-child(2)  { animation-delay: calc(var(--oc-step) * 1); }
.oc-table tbody tr:nth-child(3),  .oc-record-list > .oc-record:nth-child(3),  .oc-ktiles > .oc-ktile:nth-child(3)  { animation-delay: calc(var(--oc-step) * 2); }
.oc-table tbody tr:nth-child(4),  .oc-record-list > .oc-record:nth-child(4),  .oc-ktiles > .oc-ktile:nth-child(4)  { animation-delay: calc(var(--oc-step) * 3); }
.oc-table tbody tr:nth-child(5),  .oc-record-list > .oc-record:nth-child(5),  .oc-ktiles > .oc-ktile:nth-child(5)  { animation-delay: calc(var(--oc-step) * 4); }
.oc-table tbody tr:nth-child(6),  .oc-record-list > .oc-record:nth-child(6)  { animation-delay: calc(var(--oc-step) * 5); }
.oc-table tbody tr:nth-child(7),  .oc-record-list > .oc-record:nth-child(7)  { animation-delay: calc(var(--oc-step) * 6); }
.oc-table tbody tr:nth-child(8),  .oc-record-list > .oc-record:nth-child(8)  { animation-delay: calc(var(--oc-step) * 7); }
.oc-table tbody tr:nth-child(n+9), .oc-record-list > .oc-record:nth-child(n+9) { animation-delay: calc(var(--oc-step) * 8); }

/* The completed rail draws itself in from the node it leaves, on the same easing as everything else. */
.oc-stepper-step.is-completed:not(:last-child)::after {
    transform-origin: left center;
    animation: oc-rail 620ms var(--oc-ease-out) backwards;
}
@keyframes oc-rail { from { transform: scaleX(0); } }


/* ── Row hover ───────────────────────────────────────────────────────────────────────────────────
   Row actions used to sit at .62 opacity until the row was approached, on the theory that a table at
   rest reads better as data than as a wall of buttons. It cost more than it bought: a primary action
   rendered in a washed-out version of its own colour looks disabled, and "is this greyed out or just
   waiting for me?" is a worse question than a slightly busier table. Full strength at rest now; the
   row still lifts on hover, which is what marks the row you are on.

   Gated on a real hover pointer — on touch there is no "approach" to respond to. */
@media (hover: hover) and (pointer: fine) {
    /* border-collapse is `separate` on .oc-table (theme line 352), so a transform on the row does not
       double up collapsed borders — the lift is safe here in a way it would not be on a collapsed table. */
    .oc-table tbody tr { transition: background .12s ease, transform .18s var(--oc-ease-out); }
    .oc-table tbody tr:hover { transform: translateY(-1px); }
}


/* ── Material ────────────────────────────────────────────────────────────────────────────────────
   A 1px border around a flat fill is a diagram of a card, not a surface. One hairline of light along
   the top edge is how a real material catches ambient light, and it is the difference most visible in
   dark mode, where the plain bordered rectangle is the strongest "assembled from a template" tell.
   Inset, offset, no blur — a highlight, not a glow. */
.oc-card, .oc-ktile { box-shadow: var(--shadow-sm), inset 0 1px 0 rgba(255, 255, 255, .05); }
.dark .oc-card, .dark .oc-ktile { box-shadow: var(--shadow-sm), inset 0 1px 0 rgba(255, 255, 255, .07); }
.oc-ktile:hover { box-shadow: var(--shadow-md), inset 0 1px 0 rgba(255, 255, 255, .08); }

/* The corner radial blob was decoration standing in for depth — a zero-offset colored halo that reads
   as a smudge at any size, and the same smudge on every tile. The edge light above replaces it. */
.oc-ktile::after { content: none; }


/* ── Browser surfaces ────────────────────────────────────────────────────────────────────────────
   Selection colour, the page scrollbar and the figures in tabular data all ship as browser defaults
   that belong to no design system. They are the cheapest signal that a page was authored rather than
   assembled, and the easiest to leave untouched.

   Numerals: font-variant-numeric affects only digits, so setting it on the table is safe for prose
   cells and fixes every numeric AND date column at once — previously only cells that opted in via
   .oc-number/.oc-mono got fixed-width figures, so a date column's "1"s were visibly narrow. */
.oc-table { font-variant-numeric: tabular-nums; }
.oc-ktile-v { font-variant-numeric: tabular-nums; }

::selection { background: color-mix(in srgb, var(--brand-500) 30%, transparent); }
html { scrollbar-width: thin; scrollbar-color: var(--line-strong) transparent; }


/* Motion is an enhancement to a page that is already complete and already visible, so removing it can
   never remove content: the arrival keyframes simply do not run, and every element sits at the resting
   style the `from`-only keyframes were animating towards. */
@media (prefers-reduced-motion: reduce) {
    .oc-table tbody tr,
    .oc-record-list > .oc-record,
    .oc-ktiles > .oc-ktile,
    .oc-stepper-step.is-completed:not(:last-child)::after { animation: none; }
    .oc-table tbody tr:hover { transform: none; }
}


/* ═══════════════════════════════════════════════════════════════════════════════════════════════════
   AT REST — what the page looks like in a still, before anything moves
   ═══════════════════════════════════════════════════════════════════════════════════════════════════
   The arrival layer above only exists while the page is loading, and the hover layer only while a
   pointer is on a row. Neither reaches a screenshot. Everything in this block is unconditional: it
   changes the composition you see standing still.

   No new colours — every value below resolves to a token the theme already defines. */

/* ── Table: rhythm, hierarchy, and a header that reads as designed ─────────────────────────────────
   Three separate problems, all visible in a still:

   1. DENSITY. .75rem of vertical cell padding puts a row's text closer to the row above it than to
      its own label, so a five-row table reads as one dense block instead of five records.
   2. HIERARCHY. Every cell was --ink-700, including the first, so the column that identifies the
      record ("HubSpot") carried exactly the same weight as a date. The eye had nothing to land on.
   3. THE HEADER. .04em of tracking on a .7rem uppercase label is not enough to separate the letters
      at that size, so the header read as a smudge rather than as a deliberate label row.

   :not(.oc-table-dense) ON THE SIZE-BEARING RULES BELOW, AND IT MATTERS. Both of these select on exactly
   one class plus two element names, which is the SAME specificity as `.oc-table-dense tbody td` /
   `.oc-table-dense thead th` — a tie the cascade breaks by source order, and this block sits later in the
   file. The five Admin grids (Clients, Proposals, Contracts, Invoices, Receipts) all carry BOTH classes,
   so every one of them was silently rendering at THIS rule's padding and header size — 15.2px/14px cells
   and a 10.9px header — while the density system's own 7.2px/8.8px and 12px sat in the stylesheet doing
   nothing. That is the actual cause of the tables reading as loosely and unevenly spaced despite the
   density system existing at all: it never won. Untangled by carving the two rules apart, rather than by
   reordering — a future rule landing between them would silently reopen the same tie. */
.oc-table:not(.oc-table-dense) tbody td { padding: .95rem .875rem; }

/* The identity column, promoted. Only the first cell, and only in a real data table — the strongest,
   cheapest hierarchy move available, because it needs no colour and no extra element. Kept for EVERY
   table, dense or not: it never fought the density system (no padding here to tie on), and the
   hierarchy it gives a record's own name is exactly as welcome at 12px as at the size this was written for. */
.oc-table tbody td:first-child { color: var(--ink-900); font-weight: 600; }

.oc-table thead th {
    font-weight: 700;
    border-bottom-color: var(--line-strong);
}
.oc-table:not(.oc-table-dense) thead th {
    font-size: .68rem;
    letter-spacing: .09em;
    padding-top: .7rem;
    padding-bottom: .7rem;
}

/* Row separators survive dark mode. --line-2 is tuned for a white surface; against the dark app
   background it lands at roughly the background's own value, which is why rows looked like free-
   floating text with no record boundaries at all in the screenshot. */
.dark .oc-table tbody td { border-bottom-color: rgba(148, 163, 184, .16); }

/* ── The list footer is a footer ───────────────────────────────────────────────────────────────────
   The rule separating "Showing 1–1 of 1 proposals" from the last row existed already, but drawn in
   --line-2 — the same token the row separators used, and the same reason it vanished against the dark
   ground. Promoting it to --line gives the footer a visible top edge, and matching the cells' 1.25rem
   horizontal padding aligns the count with the first column instead of sitting inset from it. */
.oc-pagination {
    border-top-color: var(--line);
    padding: .85rem 1.25rem;
}
.dark .oc-pagination { border-top-color: rgba(148, 163, 184, .16); }

/* A card grid has no row above the pager for that top rule to separate — nothing but empty space, so it
   reads as a stray line floating under the last card. Used on Projects (.oc-pager-flush), the one list
   rendered as cards instead of a table. */
.oc-pager-flush .oc-pagination { border-top: 0; padding-top: 0; }

/* ── Page title at display scale ───────────────────────────────────────────────────────────────────
   1.4rem is a heading among headings. A page title is the one piece of type on the screen that has no
   competition, and giving it real size is the fastest way a layout stops looking like a template with
   the content swapped out. Tracking tightens as it grows, which is what keeps it from looking merely
   enlarged. */
.oc-page-title {
    font-size: clamp(1.55rem, 1.15rem + 1.5vw, 2rem);
    letter-spacing: -.03em;
    line-height: 1.12;
}
.oc-page-sub { font-size: .925rem; margin-top: .4rem; }
.oc-page-head { margin-bottom: 1.75rem; }

/* Restated because the rule it overrides (line ~218) is declared EARLIER in this file and has equal
   specificity to the block above — the centred auth card keeps its own smaller title. */
.oc-center-card .oc-page-title { font-size: 1.35rem; }

/* ── Card edges that hold in dark mode ─────────────────────────────────────────────────────────────
   A drop shadow is a light-mode device: --shadow-sm is a near-black at 4–6% alpha, which is invisible
   against a dark ground. Dark surfaces separate by EDGE, not by shadow, so the border carries the
   depth there instead. */
.dark .oc-card, .dark .oc-ktile { border-color: var(--line-strong); }


/* ═══════════════════════════════════════════════════════════════════════════════════════════════════
   ENGAGEMENT STEPPER — the linear cascade
   ═══════════════════════════════════════════════════════════════════════════════════════════════════
   Proposal → Contract → Project → Invoice is a strictly linear chain: a stage cannot complete before
   the one feeding it. The strip should SHOW that, so it animates as a relay rather than four things
   appearing at once — each tick draws, the rail leaving that node draws behind it, and only then does
   the next node begin. Watching it once tells a client how the engagement is sequenced, which is the
   only reason to animate a progress indicator at all.

   The stepper is one shared component (EngagementStepperViewComponent, invoked from _LayoutPortal),
   so this same cascade and the same completed/current/upcoming reading appear on the Proposals,
   Contracts, Projects and Invoices pages without any of them knowing about it. */

/* Resting state is the FINISHED tick (dashoffset 0); the keyframe animates in from undrawn. So a
   browser that runs no animation shows four correct ticks, never four blank circles. */
.oc-stepper-tick { width: 1rem; height: 1rem; overflow: visible; }
.oc-stepper-tick path {
    stroke-dasharray: 1;
    stroke-dashoffset: 0;
    animation: oc-tick 380ms var(--oc-ease-out) backwards;
}
@keyframes oc-tick { from { stroke-dashoffset: 1; } }

/* Retimed from the standalone 620ms draw so the rail finishes inside one stage's slot — at 620ms a
   segment was still drawing while the NEXT tick had already started, which reads as two things
   happening at once instead of a handover. */
.oc-stepper-step.is-completed:not(:last-child)::after {
    animation: oc-rail 300ms var(--oc-ease-out) backwards;
}

/* The relay. 320ms per stage: tick draws at the top of the slot, the rail leaving it starts ~240ms in
   so the line appears to be pulled out of the node that just completed. Written out because there are
   exactly four stages and there always will be — the chain is the domain, not a list length. */
.oc-stepper-step:nth-child(1) .oc-stepper-tick path { animation-delay: 60ms; }
.oc-stepper-step:nth-child(2) .oc-stepper-tick path { animation-delay: 380ms; }
.oc-stepper-step:nth-child(3) .oc-stepper-tick path { animation-delay: 700ms; }
.oc-stepper-step:nth-child(4) .oc-stepper-tick path { animation-delay: 1020ms; }
.oc-stepper-step:nth-child(1)::after { animation-delay: 300ms; }
.oc-stepper-step:nth-child(2)::after { animation-delay: 620ms; }
.oc-stepper-step:nth-child(3)::after { animation-delay: 940ms; }

/* ── Incomplete has to LOOK incomplete ────────────────────────────────────────────────────────────
   .oc-timeline-dot.is-slate is a filled grey disc, which is the right reading in a chronological log
   where every entry has already happened. In a stage strip it is wrong: a filled disc is the same
   visual weight as a filled success disc, so "not started" and "done" differed only by hue — and hue
   alone is not a state for anyone who cannot separate the two.

   A hollow ring is the convention because it reads as an empty container: something that is waiting
   to be filled. Scoped to .oc-stepper-dot so the Milestones timeline keeps its own filled discs. */
.oc-stepper-dot.is-slate {
    background: transparent;
    box-shadow: inset 0 0 0 2px var(--line-strong);
    color: var(--ink-400);
}

/* The rail INTO an unreached stage stays hairline and unfilled, so the eye can see how far the chain
   has actually got without reading a single label. */
.oc-stepper-step:not(.is-completed):not(:last-child)::after { height: 1px; }

@media (prefers-reduced-motion: reduce) {
    .oc-stepper-tick path { animation: none; }
}


/* ── Stepper: glyph centring, and a hover that acknowledges the pointer ──────────────────────────────
   The circle was already centring its child (.oc-timeline-dot is a centred flex box), but the child is
   a Font Awesome <i> — an inline-block carrying a FONT GLYPH, and a glyph sits on the text baseline
   rather than in the middle of its own box. So the box landed dead centre and the mark inside it rode
   high. Tailwind's w-3.5/h-3.5 fixes the box's size, which is exactly what makes the offset consistent
   and visible rather than random.

   Making the <i> a centred grid that fills the circle moves the glyph itself to the centre, and
   line-height:1 stops the font's own leading reintroducing the shift. Scoped to .oc-stepper-dot so the
   Milestones timeline keeps rendering exactly as it does today. */
.oc-stepper-dot > i {
    display: grid;
    place-items: center;
    width: 100%;
    height: 100%;
    font-size: .78rem;
    line-height: 1;
}

/* The drawn tick is an SVG, already centred by its viewBox, so it only needs the box centred too. */
.oc-stepper-dot > .oc-stepper-tick { display: block; margin: auto; }

@media (hover: hover) and (pointer: fine) {
    /* The whole step is the hover target, not the 1.85rem circle — a target that small is a game of
       aim, and the label underneath is part of the same thing the pointer is pointing at. */
    .oc-stepper-dot { transition: transform .18s var(--oc-ease-out), box-shadow .18s var(--oc-ease-out); }
    .oc-stepper-step .oc-stepper-label { transition: color .18s var(--oc-ease-out); }

    /* Only the stages that have actually been reached respond. An upcoming stage answering to the
       pointer would be promising something to click, and there is nothing there yet. */
    .oc-stepper-step:hover .oc-stepper-dot.is-success { transform: scale(1.1); }
    .oc-stepper-step:hover .oc-stepper-dot.is-success { box-shadow: 0 4px 10px -4px rgba(2, 6, 23, .3); }

    /* is-info carries the breathing oc-pulse-ring, which owns box-shadow for the whole of its run — a
       hover shadow here would be overridden mid-animation and flicker. Transform is a free channel, so
       the active stage grows without touching what the pulse is already animating. */
    .oc-stepper-step:hover .oc-stepper-dot.is-info { transform: scale(1.1); }

    .oc-stepper-step:hover .oc-stepper-dot.is-success ~ .oc-stepper-label,
    .oc-stepper-step:hover .oc-stepper-dot.is-info ~ .oc-stepper-label { color: var(--ink-900); }
}

@media (prefers-reduced-motion: reduce) {
    .oc-stepper-step:hover .oc-stepper-dot.is-success,
    .oc-stepper-step:hover .oc-stepper-dot.is-info { transform: none; }
}


/* ── The feedback column on the admin proposals table ────────────────────────────────────────────────
   The client's comment is free-form prose in a fixed-width cell, so it is clamped to two lines and the
   modal carries the rest — the same arrangement the card layout used, minus the room to wrap. Capped
   rather than left to size itself: one long comment would otherwise set the width of every column beside
   it, which is how a table full of short fields ends up with a single unreadable paragraph in the middle.

   A button, not a div with a click handler, so it is focusable and announces itself. */
/* A COLUMN, EXPLICITLY. Dense table cells are `white-space: nowrap`, so this cell's two children — a
   quoted opener and the button under it — were laid out side by side on one line, overflowed the 15%
   column and drew over the dates beside them; the button's own margin-top did nothing, because there was
   no block flow for it to act in. Flex column restores the stack whatever the cell holds, so the layout
   cannot break again the next time something is added here. align-items keeps the button its natural
   width rather than stretching it across the cell. */
.oc-feedback-cell {
    width: 100%; min-width: 9rem;
    display: flex; flex-direction: column; gap: .3rem;
    white-space: normal;
}
/* One line, clipped: a count in the label must not be able to widen the control past its column. */
.oc-feedback-btn { max-width: 100%; overflow: hidden; white-space: nowrap; }
.oc-feedback-quote {
    width: 100%;
    padding: 0;
    border: 0;
    background: none;
    cursor: pointer;
    font: inherit;
    color: var(--ink-700);
    /* ONE line, not two. This button always sits above the row's "View feedback" control, so a two-line
       clamp made a proposal with feedback sit visibly taller than every other row in the same table —
       the one place the density system's "nothing wraps, so every row is the same height" rule was
       broken. Nothing is lost by clipping sooner: the full quote is what the modal this button opens is
       for (see the comment above), so one line is enough to recognise it and click through. */
    display: -webkit-box;
    -webkit-line-clamp: 1;
    -webkit-box-orient: vertical;
    overflow: hidden;
    transition: color .15s var(--oc-ease-out);
}
.oc-feedback-quote:hover { color: var(--brand-700); }

/* ── Proposal comments — admin side (Part 13) ─────────────────────────────────────────────────────────
   The client's questions/comments (Parts 7-11) land here for staff to answer. One thread per section
   plus a general one, each a short flat list (no reply-to-a-reply nesting — see ProposalComment). */
.oc-comment-threads { display: flex; flex-direction: column; gap: 1.25rem; }
.oc-comment-thread { border: 1px solid var(--line); border-radius: var(--radius); padding: .9rem 1rem; }
.oc-comment-thread.is-awaiting { border-color: var(--warning); }
.oc-comment-thread-head { display: flex; align-items: center; gap: .5rem; margin-bottom: .6rem; }
.oc-comment-thread-title { font-weight: 700; font-size: .88rem; color: var(--ink-900); }

.oc-comment-list { list-style: none; margin: 0 0 .75rem; padding: 0; display: grid; gap: .6rem; }
.oc-comment-list li { border-radius: var(--radius); padding: .55rem .75rem; background: var(--surface-muted); }
.oc-comment-list li.is-staff { background: var(--brand-50); border: 1px solid var(--brand-100); }
.dark .oc-comment-list li.is-staff { background: rgba(99, 102, 241, .12); border-color: rgba(99, 102, 241, .24); }
.oc-comment-author {
    display: block; font-size: .68rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
    color: var(--ink-400); margin-bottom: .2rem;
}
.oc-comment-list li.is-staff .oc-comment-author { color: var(--brand-700); }
/* overflow-wrap: anywhere is doing real work here, not tidying. A comment is text somebody typed, and
   nothing stops them pasting an unbroken 400-character string — which had no break opportunity, so it
   set the paragraph's min-content width and pushed the whole card off the right of the page. "anywhere"
   rather than "break-word" because only the former also shrinks min-content, which is what stops the
   container being widened in the first place.

   pre-wrap keeps the line breaks they typed, and matches how the same message renders in the margin
   notes on the Review screen — the same comment should not read as two different shapes. */
.oc-comment-list li p {
    margin: 0; font-size: .85rem; line-height: 1.5; color: var(--ink-700);
    overflow-wrap: anywhere;
    white-space: pre-wrap;
}
/* Grid and flex children default to min-width:auto, which refuses to shrink below their content —
   belt and braces so the wrap above cannot be overridden by the track sizing. */
.oc-comment-list li { min-width: 0; }
.oc-comment-list li time { display: block; margin-top: .3rem; font-size: .68rem; color: var(--ink-400); }

.oc-comment-reply-form { display: flex; align-items: flex-end; gap: .6rem; }
.oc-comment-reply-form .field { flex: 1 1 auto; }

/* Edit toggle on a staff-authored comment — small and quiet by default (it is a correction, not a new
   action), sitting right under the message it edits. */
.oc-comment-edit-toggle { margin-top: .3rem; padding: .15rem .4rem; font-size: .68rem; color: var(--ink-400); }
.oc-comment-edit-toggle:hover { color: var(--brand-700); }
.oc-comment-edit-form { margin-top: .3rem; display: flex; flex-direction: column; gap: .4rem; }
.oc-comment-edit-actions { display: flex; justify-content: flex-end; gap: .4rem; }

/* ── Octacer brand — client portal (lime on near-black) — ACTIVE ─────────────────────────────────────
   Re-enabled per the Client Portal UI/UX specification §2/§4, which names the lime-on-near-black shell
   as the target visual direction. It had previously been reverted by request; the whole override is
   driven by `oc-brand-octacer` on <body> in _LayoutPortal.cshtml and _LayoutPortalAuth.cshtml, so
   removing that one class from those two files reverts the portal to the shared indigo scheme without
   touching a single colour value here.

   octacer-theme.css itself invites the override: "Public brand tokens. Apps may override these once in
   their own theme layer." It is scoped to body.oc-brand-octacer (set only by the Portal area's layouts)
   so admin is never affected either way. The palette matches the signed proposal PDF (proposal.css's
   --pp-lime/--pp-lime-ink); note the public proposal document keeps its lime regardless — it has its own
   stylesheet and was never driven by this block. */
.oc-brand-octacer {
    /*
        SAMPLED FROM octacer.com, not derived. Walking every element's computed styles on the live site
        returns exactly three greens — #a3dc2f (383 uses), #6b9e1e (64) and #3a7c21 (52) — plus alpha
        steps of the first. So that is the whole ramp here, and the two shades this block used to carry
        that the site does not use anywhere (#bbef44, #4f7716) are gone. A fourth shade is not a richer
        palette, it is the portal quietly looking like a different product.
    */
    /* The two pale steps a light-theme chip needs, MIXED from the primary rather than picked by eye —
       so the palette has exactly one green in it and these can never drift to a different hue. */
    --brand-50:  color-mix(in srgb, #3a7c21 8%, #ffffff);
    --brand-100: color-mix(in srgb, #3a7c21 18%, #ffffff);
    --brand-500: #a3dc2f;
    --brand-600: #6b9e1e;
    --brand-700: #3a7c21;
    /* Was a duplicate of 700, which left a filled button with no darker state to press into. Mixed
       toward black rather than picked, so it stays the same hue. */
    --brand-800: color-mix(in srgb, #3a7c21 84%, #000000);

    /*
        LIGHT MODE. Sampled again, this time asking what the site does on its LIGHT sections rather than
        what its accent is in the abstract, and the answer is unambiguous: every green FILL on
        octacer.com sits on #040408 or #141414, and on #ffffff / #f8fafc the green appears only as TEXT
        at #3a7c21 ("Browse all ten solutions", "text-green-700"). The one exception is a 6px bullet dot.
        There is no lime button on a white ground anywhere on the site.

        That is also the accessible reading. Lime carries a relative luminance of .59, so on white it is
        a 1.9:1 fill: legible only because its label is black, and unmistakably loud — the "harsh parrot
        green". #3a7c21 is 5.2:1 on white with a white label at 5.2:1 the other way, so the same brand
        green works as a fill AND as text in light mode.

        Dark mode is unchanged and still lime on near-black: see .dark .oc-brand-octacer below.
    */
    --primary: var(--brand-700);
    --primary-active: var(--brand-800);
    --primary-foreground: #ffffff;

    --ring: 0 0 0 4px rgba(58, 124, 33, .22);
    --chart-1: var(--brand-600);
    /* Light mode's gradient stops before the lime. .oc-gradient-brand and .oc-hero print a WHITE label
       on this fill, and white on #a3dc2f is 1.6:1 — so a ramp that ended in lime made its own text
       illegible at the bright end. Both remaining stops carry white past AA. */
    --gradient-brand: linear-gradient(135deg, var(--brand-800) 0%, var(--brand-700) 60%, var(--brand-600) 100%);

    /* The site's own light grounds, sampled: #f8fafc behind a section and #ffffff for a card, with
       #f9fafb as the step between. The green-tinted ground this used to carry (#f7f9f3 / #f4f7ed) was
       reasoned from the spec's "slight green-gray" rather than measured, and a tinted ground under a
       green accent is half of why light mode read as loud — the tint pushes every neutral toward the
       accent, so nothing is left to be quiet against. Cards stay pure white either way. */
    --bg: #f8fafc;
    --surface-muted: #f9fafb;

    /* Tailwind's colour utilities (bg-popover, bg-card, border-border…) read --color-* tokens that
       _ThemeHead declares on :root as var(--popover) etc. — and a var() resolves where it is DECLARED, so
       they kept the theme's :root slate-blue values and never saw the overrides above (this body class
       is below :root). The portal's bottom sheets (bg-popover) were navy on the near-black page because
       of it. Re-declared here so they resolve against this block's values, in both themes. */
    --color-background: var(--bg);
    --color-foreground: var(--ink-900);
    --color-card: var(--surface);
    --color-card-foreground: var(--ink-900);
    --color-popover: var(--popover);
    --color-popover-foreground: var(--ink-900);
    --color-primary: var(--primary);
    --color-primary-foreground: var(--primary-foreground);
    --color-secondary: var(--surface-muted);
    --color-secondary-foreground: var(--ink-700);
    --color-muted: var(--surface-muted);
    --color-muted-foreground: var(--ink-500);
    --color-accent: var(--surface-muted);
    --color-accent-foreground: var(--ink-900);
    --color-border: var(--line);
    --color-input: var(--input);
    --color-ring: var(--brand-500);
    --color-sidebar: var(--surface);
    --color-sidebar-foreground: var(--ink-600);
    --color-sidebar-primary: var(--primary);
    --color-sidebar-primary-foreground: var(--primary-foreground);
    --color-sidebar-accent: var(--surface-muted);
    --color-sidebar-accent-foreground: var(--ink-900);
    --color-sidebar-border: var(--line);
    --color-brand-50: var(--brand-50);
    --color-brand-100: var(--brand-100);
    --color-brand-500: var(--brand-500);
    --color-brand-600: var(--brand-600);
    --color-brand-700: var(--brand-700);
    --color-brand-800: var(--brand-800);
}

/* The other half of the identity — "near-black", not the theme's default slate-near-black — applies only
   in dark mode. Light mode keeps the theme's white/slate surfaces; swapping those to near-black too was
   never asked for and would fight the theme's own light-mode contrast throughout every component. */
.dark .oc-brand-octacer {
    /*
        The dark theme used to step the accent UP to #bbef44 on the theory that a near-black ground needs
        a brighter lime. The site does the opposite: #a3dc2f is its accent on white AND on its near-black
        hero, unchanged. Stepping up read as a second, more acid green — the "different shades" and the
        button being "too strong, bright and distracting" are the same one-token cause.
    */
    --primary: #a3dc2f;
    --primary-active: #6b9e1e;
    --primary-foreground: #000000;
    /* The theme's .dark block hard-codes these three as indigo literals rather than deriving them from
       --primary, so without this the phone "More" sheet's current tile (and anything else tinted with
       them) showed an indigo-blue wash inside the lime portal. */
    --primary-ink: #a3dc2f;
    --primary-tint: rgba(163, 220, 47, .12);
    --primary-tint-border: rgba(163, 220, 47, .35);

    /* The site's own grounds, sampled: #040408 behind the page, #0b0e14 for a raised surface. Anything
       above those is the site's other device — white at a low alpha — rather than a new grey. */
    --bg: #040408;
    --surface: #0b0e14;
    --popover: color-mix(in srgb, #ffffff 4%, #0b0e14);
    --surface-muted: color-mix(in srgb, #ffffff 3%, #0b0e14);
    --chart-1: #a3dc2f;
    --gradient-brand: linear-gradient(135deg, #3a7c21 0%, #6b9e1e 52%, #a3dc2f 100%);

    /* The design system's dark borders and muted ink are slate — a blue-grey that reads visibly cool
       against octacer.com's neutral near-black. Re-based on the same neutral ramp as the surfaces so a
       card edge looks like the edge of THIS ground rather than a panel borrowed from the indigo theme.
       Kept low-contrast on purpose: §2 asks for restraint, and a hairline is a separation, not a frame. */
    /*
        octacer.com draws every edge as WHITE AT A LOW ALPHA (10% for a hairline, 20% for a raised card's
        edge) and every body paragraph as white at 65%. That is why its dark sections read as one
        material: the borders and the text are the same light, dimmed, rather than separate greys mixed
        by hand. Matched here — which also drops the last of the cool slate that made a card edge look
        borrowed from the indigo theme.
    */
    --line: rgba(255, 255, 255, .10);
    --line-2: rgba(255, 255, 255, .06);
    --line-strong: rgba(255, 255, 255, .20);
    --input: rgba(255, 255, 255, .22);
    --ink-700: rgba(255, 255, 255, .82);
    --ink-600: rgba(255, 255, 255, .65);
    --ink-500: rgba(255, 255, 255, .52);
    --ink-400: rgba(255, 255, 255, .40);
}

/* The two decoratives the tokens above cannot reach, because both hard-code indigo rather than reading
   a brand variable. Overridden here rather than at the source: .oc-app-bg's glow lives in
   octacer-theme.css, which is octa-core-owned and never hand-edited in a satellite (CLAUDE.md §8), and
   the orb's own rule stays intact so removing `oc-brand-octacer` still reverts everything in one move.
   Spec §2 allows "subtle green gradients/glows only in hero/action areas" — this is the page ground and
   the login panel, so the alphas stay near-invisible; lime carries far more luminance than indigo, so
   the same alpha reads brighter and needs no increase. */
/* ── Accent tints ────────────────────────────────────────────────────────────────────────────────────
   Every tint below used to be a literal rgba(163, 220, 47, ...) — the lime — inside a rule that applies
   in BOTH themes. So in light mode a pill's background stayed lime while its label had already moved to
   the deep green, which is the incoherence the whole exercise is about. Mixed from --oc-lime instead:
   one accent, and the fills, borders and glows follow it into whichever mode is on. */
/*
    The drifting lime grid octacer.com runs behind its own hero (its `.bg-type-8`: 80px cells at 8% lime,
    sliding 40px — half a cell, so the loop is seamless — over 15s ease-in-out via its `grid-float`).
    Same cell size, same alpha, same distance, same duration: this is the site's texture, not an
    interpretation of it.

    It sits on the page GROUND, under the cards, which is how the site uses it too — cards keep their
    solid --surface fill and float above it, so nothing is ever read through a moving texture. That is
    also why this went here rather than onto the cards themselves: a texture repeated inside every card
    would tile the pattern into boxes and put movement directly behind body copy.

    Token-driven rather than the site's literal rgba(163, 220, 47, …), per the note above: a hardcoded
    lime here would keep a lime grid in light mode while every other accent had moved to the deep green.

    The two radial washes are unchanged and stay STILL — only the two line layers drift, which is exactly
    how grid-float is built (it animates its grid layers and pins its base). Animating background-position
    repaints a gradient fill rather than compositing a layer; at 40px over 15s on one element painting
    nothing but flat gradients, that is the same cost the site itself ships, and it buys not adding a
    whole extra fixed layer to the app shell.

    Deliberately NOT background-attachment: fixed. Anchoring the grid to the viewport looks tempting on a
    long dashboard, but fixed attachment makes the browser repaint the background on every scroll frame
    (and iOS Safari does not honour it anyway), which is a real scroll cost for a purely decorative layer.
    The site does not use it either — its grid scrolls with the page, as the paper the cards sit on.
*/
.oc-brand-octacer.oc-app-bg {
    background-image:
        linear-gradient(0deg, color-mix(in srgb, var(--oc-lime) 8%, transparent) 1px, transparent 1px),
        linear-gradient(90deg, color-mix(in srgb, var(--oc-lime) 8%, transparent) 1px, transparent 1px),
        radial-gradient(60rem 60rem at 110% -10%, color-mix(in srgb, var(--oc-lime) 6%, transparent), transparent 60%),
        radial-gradient(40rem 40rem at -10% 10%, color-mix(in srgb, var(--oc-lime) 4%, transparent), transparent 55%);
    background-size: 80px 80px, 80px 80px, 100% 100%, 100% 100%;
}
@media (prefers-reduced-motion: no-preference) {
    .oc-brand-octacer.oc-app-bg { animation: oc-grid-float 15s ease-in-out infinite; }
}
/* Reduced motion keeps the grid, simply parked — same rule as the hero glow: the texture is design, the
   drift is the enhancement. */
@keyframes oc-grid-float {
    0%, 100% { background-position: 0 0, 0 0, 0 0, 0 0; }
    50%      { background-position: 40px 40px, 40px 40px, 0 0, 0 0; }
}

/* ── Operating-core constellation ────────────────────────────────────────────────────────────────────
   The second half of octacer.com's hero: the "operating core" — a pulsing lime core inside concentric
   rings, one of them dashed and slowly turning. Values are the site's own (its inline-styled `olc-*`
   animations): core pulse 4s ease-in-out between .12 and .22 opacity, dashed lime ring at
   rgba(163,220,47,.114) turning on a 28s reverse cycle, faint white ring at .04 on 45s.

   What is deliberately NOT reproduced: the labelled icon nodes (VERIFY, SIGNAL, EXECUTE …) and the
   event chips (ORDER CONFIRMED, TASK ASSIGNED …). Those are the site's marketing CONTENT — words about
   a product pipeline. Painted behind a client's own dashboard they would be decoration pretending to be
   status, which is the one thing a portal showing real project state must never do. What carries over
   is the motif's motion, not its vocabulary.

   Two pseudo-elements on the shell body, both fixed, both pointer-events:none, both under every card
   (z-index 0 against .oc-main's own stacking). Rotation and opacity only — compositor-only properties,
   so this costs no layout and no repaint of anything above it.

   Anchored off the right edge and sized in vmin so it scales with the viewport and stays behind the
   rail rather than under the reading column. */
.oc-brand-octacer.oc-app-bg::before,
.oc-brand-octacer.oc-app-bg::after {
    content: ""; position: fixed; pointer-events: none; z-index: 0;
    top: 46%; right: -14vmin; width: 58vmin; height: 58vmin; margin-top: -29vmin;
    border-radius: 50%;
}
/* The rings. The dashed one is the only one whose rotation is actually visible — a solid circle turning
   looks identical at every angle, which is why the site dashes the ring it animates. The outer solid
   ring rides along via box-shadow rather than a second element. */
.oc-brand-octacer.oc-app-bg::before {
    border: 1px dashed color-mix(in srgb, var(--oc-lime) 11%, transparent);
    box-shadow: 0 0 0 12vmin color-mix(in srgb, var(--oc-lime) 2%, transparent),
                inset 0 0 0 1px color-mix(in srgb, var(--ink-900) 4%, transparent);
}
/* The core: a lime glow at the centre of those rings, breathing on the site's own 4s cycle. */
.oc-brand-octacer.oc-app-bg::after {
    background: radial-gradient(circle at 50% 50%,
        color-mix(in srgb, var(--oc-lime) 55%, transparent) 0,
        color-mix(in srgb, var(--oc-lime) 18%, transparent) 18%,
        transparent 46%);
    opacity: .16;
}
@media (prefers-reduced-motion: no-preference) {
    .oc-brand-octacer.oc-app-bg::before { animation: oc-orbit 28s linear infinite reverse; }
    .oc-brand-octacer.oc-app-bg::after  { animation: oc-core-pulse 4s ease-in-out infinite; }
}
@keyframes oc-orbit {
    from { transform: rotate(0deg); }
    to   { transform: rotate(360deg); }
}
@keyframes oc-core-pulse {
    0%, 100% { opacity: .12; }
    50%      { opacity: .22; }
}
/* Below the shell's own breakpoint the rail stacks under the reading column, so the constellation would
   sit directly behind body copy. Off, rather than faded further. */
@media (max-width: 900px) {
    .oc-brand-octacer.oc-app-bg::before,
    .oc-brand-octacer.oc-app-bg::after { display: none; }
}

/* ════════════════════════════════════════════════════════════════════════════════════════════════════
   CLIENT PORTAL SHELL — reference-mockup implementation
   Everything below is scoped to .oc-brand-octacer, so it reaches only the Portal area's two layouts and
   the one-class revert documented above still removes all of it. Admin is untouched throughout.

   Three shared tokens the mockups imply but the design system has no name for: the lime at full strength
   for text/icons on dark (--oc-lime), the tinted fill behind an active or accented surface
   (--oc-lime-wash), and the ground the lime sits on in a CTA (--oc-on-lime).
   ════════════════════════════════════════════════════════════════════════════════════════════════════ */
/*
    These three are the whole accent surface of the portal shell: the selected nav tab, the avatar, the
    card-title icons, the bell and its count, the links, the pills, the progress fill and the logo all
    read them and nothing else. So they are where light mode is decided, once — changing the tab, then
    the button, then the logo one rule at a time is how a theme ends up with four greens in it.

    The NAME stays --oc-lime because every call site means "the brand accent on this ground". What
    changes is that the ground is now part of the answer: lime is a dark-mode colour (10.6:1 on the
    site's #040408, and the site itself never fills with it on white), and #3a7c21 is the light-mode one
    (5.2:1 on white, and the exact green the site's own light sections use as text).
*/
.oc-brand-octacer {
    --oc-lime: #3a7c21;
    --oc-lime-wash: rgba(58, 124, 33, .09);
    /* White on #3a7c21 is 5.2:1 — the same figure as the green on white, so a filled chip and a green
       label are equally legible. Black on it would be 2.4:1 and fail. */
    --oc-on-lime: #ffffff;
}
.dark .oc-brand-octacer {
    /*
        No dark-mode step-up from lime either. #a3dc2f on the site's #040408 measures 10.6:1 — far past
        the 4.5:1 AA needs for text — so brightening it bought contrast nobody was short of and cost the
        one thing the accent has to have: being the same green everywhere.
    */
    --oc-lime: #a3dc2f;
    --oc-lime-wash: rgba(163, 220, 47, .12);
    /* Pure black on lime, as the site's buttons do it. */
    --oc-on-lime: #000000;
    --ring: 0 0 0 4px rgba(163, 220, 47, .28);
    --brand-50:  color-mix(in srgb, #a3dc2f 12%, #0b0e14);
    --brand-100: color-mix(in srgb, #a3dc2f 22%, #0b0e14);
}

/* The logo takes the same accent as everything else. The mark's stroke is currentColor
   (_OctacerRing.cshtml), so this one declaration is what makes it deep green on a white sidebar and
   lime on a near-black one — the site prints its mark in lime on near-black and its green as #3a7c21
   on white, and a lime ring beside a near-black wordmark on white was the mismatch. */
.oc-brand-octacer svg.oc-ring { color: var(--oc-lime); }

/* ── Sidebar ─────────────────────────────────────────────────────────────────────────────────────────
   The badge holds the LOGO now, so the tile it used to be goes: octacer.com sets its mark straight on the
   page ground, lime strokes and nothing behind them. A filled chip under a stroked ring reads as two
   nested shapes, and the lime-on-lime cancels the mark's own colour out. */
.oc-brand-octacer .oc-brand-badge {
    background: none;
    box-shadow: none;
    border-radius: 0;
    width: 2.1rem;
    height: 2.1rem;
}
.oc-brand-octacer .oc-brand-badge .oc-ring { width: 100%; height: 100%; display: block; }

/* The contact card and Help & Contact show Octacer, the company, where an account manager's initials
   used to sit (no staff names in the portal, by request). Same rule as the sidebar badge: the mark on
   the page ground, not inside the lime initials chip — a lime chip under a lime ring cancels the mark out. */
.oc-brand-octacer .oc-avatar-brand { background: none; color: var(--oc-lime); }
.oc-brand-octacer .oc-avatar-brand .oc-ring { width: 100%; height: 100%; display: block; }

/* The sign-in panel's mark: same asset, sized to the frosted plate it sits on. */
.oc-brand-octacer .oc-auth-logo .oc-ring { width: 2.25rem; height: 2.25rem; display: block; }
/* No frosted plate behind it either. The theme's translucent white tile was framing what is already a
   finished mark, and on the near-black panel below it read as a light square with a logo inside it. */
.oc-brand-octacer .oc-auth-logo {
    background: none; border: 0; box-shadow: none; backdrop-filter: none; padding: 0;
}

/*
    The sign-in panel's ground. It was a full-bleed mid-green wash (the brand ramp used as a gradient),
    which is the single most off-brand surface in the portal: octacer.com has no green field anywhere —
    its hero is the same near-black as the rest of the site, with lime arriving as a soft radial glow and
    as accents. Rebuilt from the site's own two grounds, with the glow at the alpha its washes use.
*/
.oc-brand-octacer .oc-auth-brand {
    background:
        radial-gradient(38rem 38rem at 18% 12%, color-mix(in srgb, var(--oc-lime) 16%, transparent), transparent 62%),
        radial-gradient(30rem 30rem at 85% 88%, color-mix(in srgb, var(--oc-lime) 9%, transparent), transparent 60%),
        linear-gradient(160deg, #0b0e14 0%, #040408 100%);
}
/* The orbs are the theme's own decoration on that panel: one was pure white, the other indigo. Both
   become the brand lime at a low alpha, so the panel has one accent hue rather than three. */
.oc-brand-octacer .oc-auth-orb-1 { background: color-mix(in srgb, var(--oc-lime) 55%, transparent); }
.oc-brand-octacer .oc-auth-orb-2 { background: color-mix(in srgb, var(--oc-lime) 35%, transparent); }

/* ── Notification bell ───────────────────────────────────────────────────────────────────────────────
   The portal's top-bar bell and its dropdown. Sized and coloured from the same tokens as everything else
   in this layer, so it inherits both themes without a second palette. */
.oc-brand-octacer .oc-bell { position: relative; display: inline-flex; }
.oc-brand-octacer .oc-bell-btn {
    position: relative; appearance: none; cursor: pointer;
    display: inline-grid; place-items: center;
    width: 2.35rem; height: 2.35rem; border-radius: .7rem;
    border: 1px solid var(--line); background: var(--surface); color: var(--ink-600);
    font-size: .95rem;
    transition: color .15s ease, border-color .15s ease, background .15s ease;
}
.oc-brand-octacer .oc-bell-btn:hover { color: var(--oc-lime); border-color: var(--line-strong); }
.oc-brand-octacer .oc-bell-btn:focus-visible { outline: 2px solid var(--oc-lime); outline-offset: 2px; }

/* The count sits ON the icon, so it reads as an annotation of the bell rather than a second control.
   Black on lime: white on it fails contrast at this size like everywhere else. */
.oc-brand-octacer .oc-bell-count {
    position: absolute; top: -.3rem; right: -.3rem;
    min-width: 1.15rem; height: 1.15rem; padding: 0 .25rem;
    display: grid; place-items: center;
    border-radius: 999px; background: var(--oc-lime); color: var(--oc-on-lime);
    font-size: .68rem; font-weight: 800; line-height: 1;
    box-shadow: 0 0 0 2px var(--bg);
}

.oc-brand-octacer .oc-bell-panel {
    position: absolute; top: calc(100% + .5rem); right: 0; z-index: 60;
    width: 22rem; max-width: calc(100vw - 2rem);
    background: var(--surface); border: 1px solid var(--line); border-radius: .9rem;
    box-shadow: 0 24px 48px -24px rgba(0, 0, 0, .55);
    overflow: hidden;
}
.oc-brand-octacer .oc-bell-head {
    display: flex; align-items: center; justify-content: space-between; gap: .75rem;
    padding: .8rem .95rem; border-bottom: 1px solid var(--line);
}
.oc-brand-octacer .oc-bell-title { font-size: .82rem; font-weight: 800; letter-spacing: -.01em; color: var(--ink-900); }
.oc-brand-octacer .oc-bell-clear {
    appearance: none; border: 0; background: none; cursor: pointer; padding: 0;
    font-size: .75rem; font-weight: 700; color: var(--oc-lime);
}
.oc-brand-octacer .oc-bell-clear:hover { text-decoration: underline; }

/* Capped and scrollable: twenty entries must not grow a dropdown past the viewport. */
.oc-brand-octacer .oc-bell-list {
    list-style: none; margin: 0; padding: 0; max-height: 24rem; overflow-y: auto;
}
.oc-brand-octacer .oc-bell-item + .oc-bell-item { border-top: 1px solid var(--line-2); }

/* The whole row is the button — a small link inside a wide row is a target people miss. */
.oc-brand-octacer .oc-bell-row {
    appearance: none; border: 0; background: none; cursor: pointer;
    width: 100%; text-align: left;
    display: grid; grid-template-columns: 2rem minmax(0, 1fr) auto; gap: .7rem;
    align-items: start; padding: .8rem .95rem;
    font: inherit; color: inherit;
}
.oc-brand-octacer .oc-bell-row:hover { background: var(--surface-muted); }
.oc-brand-octacer .oc-bell-row:focus-visible { outline: 2px solid var(--oc-lime); outline-offset: -2px; }
.oc-brand-octacer .oc-bell-ic {
    width: 2rem; height: 2rem; border-radius: .55rem; display: grid; place-items: center;
    background: var(--oc-lime-wash); color: var(--oc-lime); font-size: .8rem;
}
.oc-brand-octacer .oc-bell-body { display: grid; gap: .1rem; min-width: 0; }
.oc-brand-octacer .oc-bell-area {
    font-size: .64rem; font-weight: 800; text-transform: uppercase; letter-spacing: .09em;
    color: var(--ink-500);
}
.oc-brand-octacer .oc-bell-text { font-size: .84rem; line-height: 1.45; color: var(--ink-700); overflow-wrap: anywhere; }
.oc-brand-octacer .oc-bell-detail { font-size: .78rem; color: var(--ink-500); overflow-wrap: anywhere; }
.oc-brand-octacer .oc-bell-when { font-size: .7rem; color: var(--ink-400); }

/* Unread: a dot plus a heavier title. Two signals, because colour alone is not one (WCAG 1.4.1). */
.oc-brand-octacer .oc-bell-item.is-unread .oc-bell-text { font-weight: 700; color: var(--ink-900); }
.oc-brand-octacer .oc-bell-dot {
    width: .45rem; height: .45rem; border-radius: 50%; background: var(--oc-lime); margin-top: .45rem;
}

.oc-brand-octacer .oc-bell-empty { padding: 1.6rem 1.2rem; text-align: center; }
.oc-brand-octacer .oc-bell-empty-icon {
    display: inline-grid; place-items: center; width: 2.4rem; height: 2.4rem; border-radius: .7rem;
    background: var(--surface-muted); color: var(--ink-500); margin-bottom: .6rem;
}
.oc-brand-octacer .oc-bell-empty-title { margin: 0; font-size: .88rem; font-weight: 700; color: var(--ink-900); }
.oc-brand-octacer .oc-bell-empty-text { margin: .3rem 0 0; font-size: .78rem; line-height: 1.55; color: var(--ink-500); }

.oc-brand-octacer .oc-bell-foot {
    display: flex; align-items: center; justify-content: center; gap: .4rem;
    padding: .7rem; border-top: 1px solid var(--line);
    font-size: .78rem; font-weight: 700; color: var(--oc-lime); text-decoration: none;
}
.oc-brand-octacer .oc-bell-foot:hover { background: var(--surface-muted); }

/* The same rows on the full page: a card rather than a dropdown, and no height cap. */
.oc-brand-octacer .oc-bell-list-page {
    max-height: none;
    background: var(--surface); border: 1px solid var(--line); border-radius: .9rem; overflow: hidden;
}
.oc-brand-octacer .oc-bell-list-page .oc-bell-row { padding: 1rem 1.1rem; }

@media (max-width: 480px) {
    /* Anchored to the viewport edge instead of the button, so a 22rem panel cannot hang off-screen. */
    .oc-brand-octacer .oc-bell-panel { position: fixed; top: 4rem; right: 1rem; left: 1rem; width: auto; }
}

/* ── Sign-in screen ──────────────────────────────────────────────────────────────────────────────────
   The page was two sentences on one side and four unbounded controls on the other, both floating in the
   middle of a very large black rectangle. Everything below adds the two things it was missing: an EDGE
   around the thing you are asked to fill in, and a reason to fill it in. No new colours — the lime, the
   two grounds and the white-alpha ramp are the ones sampled off octacer.com. */

/* The brand column is a column now: mark at the top, the pitch in the middle, footer at the bottom,
   spaced rather than stretched apart by justify-content alone. */
/*
    The panel is a flex column with space-between, which distributes three blocks and does NOT stop them
    running into each other once they are taller than the panel — the footer landed on top of the third
    feature row. So the footer is pushed down by auto margin instead, the middle block is allowed to
    shrink (min-height: 0, which a flex item does not get by default), and the type is tuned to fit a
    700px-tall window with the points in place.
*/
.oc-brand-octacer .oc-auth-brand-inner { justify-content: flex-start; gap: 1.75rem; }
.oc-brand-octacer .oc-auth-brand-head { margin-bottom: 0; }
.oc-brand-octacer .oc-auth-pitch { max-width: 34rem; min-height: 0; margin-block: auto; }

/*
    ONE CENTRE LINE FOR BOTH HALVES (desktop only — below 1024px the brand panel is not rendered).

    Centring each side within its own column does NOT put the two blocks opposite each other, because
    each column carries different furniture: the left has the mark above and the © line below, the right
    has the help note below. Centring the CONTENTS therefore centres "content plus furniture", and each
    block ends up offset by half the difference — the left sitting lower than the card by ~50px, which is
    the misalignment in the screenshot.

    So the furniture comes out of the flow: the mark pins to the top, and each side's footer line pins to
    the bottom. What is left in flow on each side is the one block that matters, and `auto` margins then
    centre both against the same axis — the viewport's. It also makes the two bottom lines a matched pair,
    one per column, at the same height.
*/
@media (min-width: 1024px) {
    .oc-brand-octacer .oc-auth-brand-inner { justify-content: center; }
    .oc-brand-octacer .oc-auth-brand-head {
        position: absolute; top: 3rem; left: 3rem; margin: 0;
    }
    .oc-brand-octacer .oc-auth-brand-foot {
        position: absolute; bottom: 3rem; left: 3rem; right: 3rem; margin: 0;
    }
    /* Now the only in-flow child, so its own auto margins have the whole panel to centre against. */
    .oc-brand-octacer .oc-auth-pitch { margin-block: auto; }

    .oc-brand-octacer .oc-auth-form { position: relative; }
    .oc-brand-octacer .oc-auth-help {
        position: absolute; bottom: 3rem; left: 1.5rem; right: 1.5rem; margin: 0;
    }
}
.oc-brand-octacer .oc-auth-brand-title { font-size: clamp(1.6rem, 1rem + 1.65vw, 2.35rem); letter-spacing: -.035em; }
.oc-brand-octacer .oc-auth-brand-sub { margin-top: .7rem; font-size: .98rem; line-height: 1.6; }

/* What is behind the login. Each row is icon + (label / line), so the labels align and the whole set
   scans as three things rather than a paragraph in a list. */
.oc-brand-octacer .oc-auth-points {
    list-style: none; margin: 1.85rem 0 0; padding: 0; display: grid; gap: 1rem; max-width: 30rem;
}
.oc-brand-octacer .oc-auth-point { display: grid; grid-template-columns: 2.35rem 1fr; gap: .9rem; align-items: start; }
.oc-brand-octacer .oc-auth-point b { display: block; font-size: .95rem; font-weight: 700; color: #fff; letter-spacing: -.01em; }
.oc-brand-octacer .oc-auth-point > span:last-child { font-size: .875rem; line-height: 1.55; color: rgba(255, 255, 255, .62); }
/* The lime appears here as a ring and a glyph, never as a fill: three filled lime tiles down the side
   of the panel would out-shout the one lime thing on the page that is meant to be pressed. */
.oc-brand-octacer .oc-auth-point-ic {
    width: 2.35rem; height: 2.35rem; border-radius: .7rem; display: grid; place-items: center;
    color: var(--oc-lime); font-size: .95rem;
    background: var(--oc-lime-wash);
    border: 1px solid color-mix(in srgb, var(--oc-lime) 22%, transparent);
}

.oc-brand-octacer .oc-auth-brand-foot {
    display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap;
}
.oc-brand-octacer .oc-auth-brand-secure { display: inline-flex; align-items: center; gap: .4rem; color: rgba(255, 255, 255, .62); }
.oc-brand-octacer .oc-auth-brand-secure i { color: var(--oc-lime); }

/* ── The form side ─────────────────────────────────────────────────────────────────────────────────── */
.oc-brand-octacer .oc-auth-form-inner { max-width: 26rem; }

/* The card. Same surface and hairline as every card inside the portal, one step of elevation so it
   reads as the object in front of the ground rather than a region of it. */
.oc-brand-octacer .oc-auth-card {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: 1.1rem;
    padding: 2rem 1.85rem;
    box-shadow: 0 24px 48px -28px rgba(0, 0, 0, .8);
}

/* "No password needed", said before the empty field can be misread as a missing password box. */
.oc-brand-octacer .oc-auth-chip {
    display: inline-flex; align-items: center; gap: .4rem; margin-bottom: 1.1rem;
    padding: .3rem .7rem; border-radius: 999px;
    font-size: .72rem; font-weight: 700; text-transform: uppercase; letter-spacing: .08em;
    color: var(--oc-lime);
    background: var(--oc-lime-wash);
    border: 1px solid color-mix(in srgb, var(--oc-lime) 24%, transparent);
}
.oc-brand-octacer .oc-auth-title {
    margin: 0; font-size: 1.55rem; font-weight: 800; letter-spacing: -.03em; color: var(--ink-900);
}
.oc-brand-octacer .oc-auth-lede { margin: .55rem 0 0; font-size: .9rem; line-height: 1.6; color: var(--ink-600); }

.oc-brand-octacer .oc-auth-help {
    margin: 1.15rem 0 0; text-align: center; font-size: .78rem; line-height: 1.6; color: var(--ink-500);
}

/* A single entrance, not an animated page: the card arrives, the panel's copy follows it. Anyone who
   asked their system for less motion gets none of it. */
@media (prefers-reduced-motion: no-preference) {
    .oc-brand-octacer .oc-auth-card,
    .oc-brand-octacer .oc-auth-pitch {
        animation: oc-auth-rise .5s cubic-bezier(.22, 1, .36, 1) both;
    }
    .oc-brand-octacer .oc-auth-card { animation-delay: .06s; }
}
@keyframes oc-auth-rise {
    from { opacity: 0; transform: translateY(.75rem); }
}

/*
    LIGHT MODE. data-theme-mode="dark" is this page's default, not a lock — a client who has toggled to
    light keeps light, and localStorage wins. On a white card the lime chip and the lime clock glyph land
    at 1.6:1, so they step to the site's deep green (#3a7c21, 5.2:1 on white) — its own third shade, not
    a new one. The brand panel is dark in both themes, so its lime stays exactly as it is.
*/
/* This used to force the chip to the deep green in light mode by hand. It no longer has to: --oc-lime
   IS the deep green in light mode, so the base rule above already lands there. Kept as a no-op comment
   rather than a rule, because the rule now restates its own default. */

/* Only a genuinely short window drops the points now. The 700px threshold this used to carry was set
   while the panel could still overlap its own footer; with the block centring properly the three rows
   fit comfortably at 700, and hiding them there cost content for no reason. */
@media (max-height: 620px) {
    .oc-brand-octacer .oc-auth-points { display: none; }
}

@media (max-width: 640px) {
    .oc-brand-octacer .oc-auth-card { padding: 1.5rem 1.25rem; border-radius: .9rem; }
    /* Below the breakpoint the brand panel is gone, so the card carries the page: no elevation to cast
       against, and full-width padding is worth more than a rounded edge near the screen edge. */
    .oc-brand-octacer .oc-auth-card { box-shadow: none; }
}
.oc-brand-octacer .oc-brand-name small { letter-spacing: .11em; font-size: .6rem; }

/* The desktop collapse control is absent from every reference screen: the sidebar is a fixed part of the
   frame there, and four to six destinations do not need to be foldable away. The mobile drawer is
   untouched — that is a different control (data-oc-sidebar-open) and still the only way the nav works
   below 1024px. */
.oc-brand-octacer .oc-sidebar-collapse { display: none; }

/* ── Sidebar ground ──────────────────────────────────────────────────────────────────────────────────
   The one surface the page's own treatment never reached. .oc-app-bg's grid and constellation live on
   the shell BODY, and the sidebar is fixed on top of it, so the nav sat as a flat slab beside a textured
   page with a hard hairline down the join.

   NO grid here, and that was tried first: the page's own lime grid, even at half alpha and half cell,
   tiles a 248px-wide rail into a visible checkerboard behind the nav labels. A texture needs room to
   read as texture; in a narrow column it just becomes lines. What is left is the quiet half of the idea —
   a soft lime fall from the top corner, picking up the brand mark already sitting there, and a seam that
   belongs to the same palette. The rail reads as part of the page without competing with it.

   background-image only — the base rule's `background: var(--surface)` shorthand already painted the
   colour, and this layers over it rather than replacing it. */
.oc-brand-octacer .oc-sidebar {
    background-image:
        radial-gradient(20rem 15rem at 0% -8%, color-mix(in srgb, var(--oc-lime) 7%, transparent), transparent 64%);
    /* The seam joins the palette: a lime-tinted hairline instead of the neutral white-alpha --line, so the
       edge between rail and page reads as one design rather than two panels butted together. */
    border-right-color: color-mix(in srgb, var(--oc-lime) 10%, transparent);
}

/* The active destination. --brand-50 is a near-white lime tint, which reads as a white pill on a dark
   sidebar, so the active state is rebuilt as a wash of the accent plus the accent itself as text. */
.oc-brand-octacer .oc-nav-active,
.oc-brand-octacer .oc-nav-active:hover {
    background: var(--oc-lime-wash);
    color: var(--oc-lime);
    font-weight: 600;
}
.oc-brand-octacer .oc-nav-link { padding: .62rem .7rem; font-size: .9rem; }
.oc-brand-octacer .oc-nav { gap: .18rem; }
.oc-brand-octacer .oc-avatar { background: var(--oc-lime); color: var(--oc-on-lime); }

/* ── Topbar ──────────────────────────────────────────────────────────────────────────────────────────
   New chrome: the mockups put the theme control and the signed-in identity at the top right of the
   content pane, where the sidebar footer used to carry the theme toggle. Not position:fixed — it scrolls
   with the page as the mockups show, and a fixed strip would need every page's top padding rewritten. */
.oc-brand-octacer .oc-topbar {
    display: flex; align-items: center; justify-content: flex-end; gap: .35rem;
    margin: 0 0 .35rem; min-height: 2.5rem;
}
.oc-brand-octacer .oc-topbar-user {
    display: flex; align-items: center; gap: .6rem; padding: .3rem .6rem .3rem .3rem;
    border-radius: 999px; background: none; border: 0; cursor: pointer;
    font-size: .9rem; font-weight: 600; color: var(--ink-900);
    text-decoration: none; transition: background var(--duration-fast) var(--ease-out);
}
.oc-brand-octacer .oc-topbar-user:hover { background: var(--surface-muted); }
.oc-brand-octacer .oc-topbar-user .oc-avatar { width: 2rem; height: 2rem; font-size: .78rem; }
.oc-brand-octacer .oc-topbar-user .oc-caret { color: var(--ink-500); font-size: .7rem; }

/* ── Page header + tagline ───────────────────────────────────────────────────────────────────────────
   Every screen opens the same way: an uppercase eyebrow naming the surface, an oversized title, one
   plain-language line under it, and a bracketed brand line pushed to the right margin. The eyebrow is
   NOT a heading — it labels the h1 that follows, so it stays a <p> and never competes in the outline. */
.oc-brand-octacer .oc-page-head {
    display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 1.5rem 2.5rem;
    align-items: start; margin-bottom: 1.5rem;
}
.oc-brand-octacer .oc-kicker {
    margin: 0 0 .5rem; font-size: .7rem; font-weight: 700; text-transform: uppercase;
    letter-spacing: .13em; color: var(--ink-500);
}
.oc-brand-octacer .oc-page-title {
    margin: 0; font-size: clamp(1.9rem, 1.1rem + 2.6vw, 3rem); font-weight: 800;
    letter-spacing: -.035em; line-height: 1.04; color: var(--ink-900); text-wrap: balance;
}
.oc-brand-octacer .oc-page-sub {
    margin: .6rem 0 0; font-size: 1.02rem; line-height: 1.55; color: var(--ink-600); max-width: 60ch;
}
.oc-brand-octacer .oc-tagline {
    margin: .35rem 0 0; padding-left: .95rem; border-left: 2px solid var(--oc-lime);
    font-size: .95rem; line-height: 1.5; color: var(--ink-700); text-align: left;
}
@media (max-width: 900px) {
    /* The brand line is the first thing worth dropping on a narrow screen: it is atmosphere, and the
       title it sits beside is the message. */
    .oc-brand-octacer .oc-page-head { grid-template-columns: minmax(0, 1fr); }
    .oc-brand-octacer .oc-tagline { display: none; }
}

/* ── Two-column page body ────────────────────────────────────────────────────────────────────────────
   Main column plus a support rail. §27 forbids a permanent rail on mobile, so below the breakpoint the
   rail stops being a column and simply follows the content it was supporting. */
.oc-brand-octacer .oc-cols {
    display: grid; grid-template-columns: minmax(0, 1fr) 21.5rem; gap: 1.35rem; align-items: start;
}
.oc-brand-octacer .oc-rail { display: grid; gap: 1.35rem; align-content: start; }
@media (max-width: 1100px) {
    .oc-brand-octacer .oc-cols { grid-template-columns: minmax(0, 1fr); }
}

/* The two columns end on the same line.
   The row is as tall as whichever column has more in it, which is nearly always the rail (contact +
   accepted proposal), so a short lead card left a 100-150px hole under it. The lead card is NOT
   stretched to cover that: a bordered card grown over empty space reads as a void between its copy and
   its button, which is worse than the hole was. The lead column carries the journey band instead, and
   the two sides balance on content.

   The empty state is the one thing that does stretch: its copy is centred already, so filling the height
   it was given reads as a panel rather than as a gap. */
@media (min-width: 1101px) {
    .oc-brand-octacer .oc-cols > :first-child { display: flex; flex-direction: column; min-height: 0; }

    /* The last card in the column carries a bottom margin that would sit under it as dead space, since
       there is nothing after it to separate from. */
    .oc-brand-octacer .oc-cols > :first-child > :last-child { margin-bottom: 0; }

    .oc-brand-octacer .oc-cols > :first-child > .oc-blank { flex: 1 1 auto; justify-content: center; }
}

/* ── Hero card ───────────────────────────────────────────────────────────────────────────────────────
   The one place §2 permits a green glow: the hero/action area. It is a background-image on the card so
   it cannot intercept clicks, and it is anchored off the top-right corner so the illustration side of
   the card is where the light appears to come from. */
.oc-brand-octacer .oc-hero-card {
    position: relative; overflow: hidden; margin-bottom: 1.5rem;
    border: 1px solid var(--line); border-radius: var(--radius-lg); background: var(--surface);
}
/*
    The glow moved off the card's own background-image and onto a ::before, so it can BREATHE — the one
    piece of motion borrowed from octacer.com's hero, where the "operating core" pulses slowly behind the
    headline while labelled nodes drift around it. None of that diagram is reproduced here: a dashboard is
    read, not watched, so what carries over is only the slow ambient pulse, at an amplitude you notice
    about as much as you notice breathing.

    Why a pseudo-element rather than animating the card: `background-image` is not a compositable
    property, so pulsing it would repaint the whole card every frame. opacity/transform on a layer that
    paints nothing else is compositor-only — no layout, no repaint of the content above it.

    pointer-events: none for the same reason the gradient lived on the background before: this is light,
    and light must never sit between a client and a button. inset: 0 with the card's own overflow: hidden
    keeps it clipped to the rounded corners.
*/
.oc-brand-octacer .oc-hero-card::before {
    content: ""; position: absolute; inset: 0; pointer-events: none; z-index: 0;
    background-image: radial-gradient(38rem 24rem at 76% -25%, color-mix(in srgb, var(--oc-lime) 17%, transparent), transparent 62%);
}
/* The content sits above the light, not in it. */
.oc-brand-octacer .oc-hero-card-body { position: relative; z-index: 1; }

@media (prefers-reduced-motion: no-preference) {
    /* 9s, and the opacity floor is .82 — a swing small enough that it reads as depth rather than as
       something blinking for attention. Anyone who asked their system for less motion keeps the glow,
       simply held still: the gradient is part of the design, the movement is the enhancement. */
    .oc-brand-octacer .oc-hero-card::before { animation: oc-hero-breathe 9s ease-in-out infinite; }
}
@keyframes oc-hero-breathe {
    0%, 100% { opacity: .82; transform: scale(1); }
    50%      { opacity: 1;   transform: scale(1.06); }
}
/* Its OWN body class, not .oc-hero-body — that one belongs to the metrics hero and is a
   space-between flex row, which would lay this card's stacked content out sideways. */
.oc-brand-octacer .oc-hero-card-body { padding: 1.6rem 1.75rem; }
.oc-brand-octacer .oc-hero-kicker { display: flex; align-items: center; gap: .8rem; margin-bottom: 1.1rem; }
/*
    Lime, and only here. octacer.com's eyebrow ("AUTOMATION · AI · INTEGRATION …") is the one small
    letter-spaced label on its hero carrying the accent, and it is what makes that block read as Octacer's
    rather than as any dark dashboard. The page-head kicker above this card ("WELCOME BACK") deliberately
    stays --ink-500: §2/§26 spend the accent once per screen, and the lead card is where it earns it.

    Contrast holds in both themes without a second rule — --oc-lime is #a3dc2f on the dark surface (~12:1)
    and steps to the site's deep green #3a7c21 on white (5.2:1) in the light-mode block below.
*/
.oc-brand-octacer .oc-hero-kicker .oc-kicker { margin: 0; color: var(--oc-lime); }
.oc-brand-octacer .oc-hero-title {
    margin: 0; font-size: clamp(1.5rem, 1.1rem + 1.3vw, 2.05rem); font-weight: 800;
    letter-spacing: -.03em; line-height: 1.14; color: var(--ink-900); text-wrap: balance; max-width: 26ch;
}
.oc-brand-octacer .oc-hero-meta { margin: .9rem 0 0; }
.oc-brand-octacer .oc-hero-when {
    display: flex; align-items: center; gap: .55rem; margin: .9rem 0 0;
    font-size: .875rem; color: var(--ink-600);
}
.oc-brand-octacer .oc-hero-when i { color: var(--ink-500); }
.oc-brand-octacer .oc-hero-text { margin: .85rem 0 0; font-size: .95rem; line-height: 1.6; color: var(--ink-600); max-width: 46ch; }
.oc-brand-octacer .oc-hero-action { margin-top: 1.35rem; }
.oc-brand-octacer .oc-hero-note { margin: .9rem 0 0; font-size: .8rem; line-height: 1.5; color: var(--ink-500); }

/* ── Support-rail contents ───────────────────────────────────────────────────────────────────────────*/
.oc-brand-octacer .oc-avatar-lg { width: 2.9rem; height: 2.9rem; font-size: .95rem; }
.oc-brand-octacer .oc-contact-who { display: flex; align-items: center; gap: .85rem; }
.oc-brand-octacer .oc-contact-name { margin: 0; font-size: .95rem; font-weight: 700; color: var(--ink-900); }
.oc-brand-octacer .oc-contact-role { margin: .1rem 0 0; font-size: .82rem; color: var(--ink-500); }
.oc-brand-octacer .oc-contact-blurb {
    margin: 1rem 0; padding-top: 1rem; border-top: 1px solid var(--line-2);
    font-size: .875rem; line-height: 1.55; color: var(--ink-600);
}
.oc-brand-octacer .oc-rail-title { margin: 0; font-size: 1.05rem; font-weight: 700; letter-spacing: -.02em; color: var(--ink-900); }
.oc-brand-octacer .oc-rail-when { margin: .35rem 0 .9rem; font-size: .82rem; color: var(--ink-500); }

/* Rounded-square icon tile. Used as a section marker and as the leading glyph on a hero or rail card. */
.oc-brand-octacer .oc-chip-icon {
    width: 2.5rem; height: 2.5rem; flex: 0 0 auto; border-radius: .7rem; display: grid; place-items: center;
    background: var(--oc-lime-wash); color: var(--oc-lime); font-size: 1.05rem;
}
.oc-brand-octacer .oc-chip-icon-muted { background: var(--surface-muted); color: var(--ink-500); }
/* Same optical-centering fix as .oc-flow-num i — see that rule's own note. */
.oc-brand-octacer .oc-chip-icon i {
    line-height: 1; display: flex; align-items: center; justify-content: center;
}

/*
    Node variant of the tile above — a CIRCLE with a hairline lime ring, which is how octacer.com draws
    every icon in its hero ("verify", "signal", "execute" and the rest orbiting the core). Same box and
    same glyph size as .oc-chip-icon, so it is a skin on that tile, not a second component with its own
    metrics: only the radius, the fill and the ring change.

    Used on the hero card's leading glyph ONLY. The rounded square stays everywhere else on purpose —
    a motif repeated on every card stops being a motif, and the square tiles are what make the section
    markers read as chrome rather than as content.
*/
.oc-brand-octacer .oc-node-icon {
    width: 2.5rem; height: 2.5rem; flex: 0 0 auto; border-radius: 50%; display: grid; place-items: center;
    background: color-mix(in srgb, var(--oc-lime) 8%, transparent);
    border: 1px solid color-mix(in srgb, var(--oc-lime) 38%, transparent);
    color: var(--oc-lime); font-size: 1.05rem;
}
/* Same optical-centering fix as .oc-flow-num i — see that rule's own note. */
.oc-brand-octacer .oc-node-icon i {
    line-height: 1; display: flex; align-items: center; justify-content: center;
}

/* ── Status pills ────────────────────────────────────────────────────────────────────────────────────
   §26: colour carries meaning, and the brightest lime is reserved for CTAs and selected nav — so a
   "waiting on you" state is amber, never lime, however positive the wording. The dot is generated,
   so a pill is one element and one class at every call site. */
.oc-brand-octacer .oc-pill {
    /* The same box as every other status badge in the app (.badge: .18rem .5rem) — at .38rem .8rem the
       "Waiting for your decision" pill above Review proposal read as a button-sized tag (by request). */
    display: inline-flex; align-items: center; gap: .3rem; padding: .18rem .5rem;
    border-radius: 999px; border: 1px solid transparent;
    font-size: .75rem; font-weight: 600; line-height: 1.2; white-space: nowrap;
}
.oc-brand-octacer .oc-pill::before {
    content: ""; width: .375rem; height: .375rem; border-radius: 50%;
    background: currentColor; flex: 0 0 auto;
}
.oc-brand-octacer .oc-pill-icon::before { display: none; }
.oc-brand-octacer .oc-pill-wait    { background: rgba(251, 191, 36, .13); color: #fbbf24; border-color: rgba(251, 191, 36, .3); }
.oc-brand-octacer .oc-pill-good    { background: color-mix(in srgb, var(--oc-lime) 12%, transparent); color: var(--oc-lime); border-color: color-mix(in srgb, var(--oc-lime) 30%, transparent); }
.oc-brand-octacer .oc-pill-info    { background: rgba(96, 165, 250, .14);  color: #60a5fa; border-color: rgba(96, 165, 250, .3); }
.oc-brand-octacer .oc-pill-due     { background: rgba(248, 113, 113, .13); color: #f87171; border-color: rgba(248, 113, 113, .3); }
.oc-brand-octacer .oc-pill-quiet   { background: var(--surface-muted); color: var(--ink-500); border-color: var(--line); }

/* ── Primary call to action ──────────────────────────────────────────────────────────────────────────
   .btn-primary already inherits lime through --primary; what the mockups add is scale — one deliberately
   oversized action per screen (§24). */
/*
    FLAT, and that is the fix for "too strong, bright and distracting". The button was carrying three
    intensifiers at once: the brighter #bbef44 fill, the theme's white inner highlight, and a coloured
    glow beneath it — a lime object lit from inside on a near-black page. octacer.com's own CTA is a flat
    #a3dc2f rectangle with a black label and no shadow at all. Same colour, a third of the shouting.
*/
.oc-brand-octacer .oc-cta {
    padding: .9rem 1.5rem; font-size: .95rem; font-weight: 700; border-radius: .7rem;
    box-shadow: none;
}
/*
    The label colour, and this is a CONTRAST fix, not a preference. octacer-theme.css line 228 sets
    `.btn-primary { color: #fff }` as a literal, not through --primary-foreground — correct for the
    indigo brand it was written against (white on indigo is ~8:1) and a hard failure on lime: white on
    #a3dc2f measures 1.64:1, where WCAG AA asks 4.5:1 for a label. The site itself renders pure black on
    its lime buttons — 12.8:1. Overridden here because the theme file is octa-core-owned (CLAUDE.md §8).

    The gradient goes too: the theme paints -500 → -600 top-to-bottom, and the site's button is one flat
    fill. Two of the three things making this button shout were that gradient and the glow above.
*/
.oc-brand-octacer .btn-primary {
    background: var(--primary);
    color: var(--primary-foreground);
    box-shadow: none;
}
.oc-brand-octacer .btn-primary:hover {
    background: var(--primary-active);
    color: var(--primary-foreground);
}
/*
    .oc-hero and .oc-gradient-brand keep the theme's white label on purpose. They are filled with the
    DARK end of the ramp (#3a7c21 → #6b9e1e), where white measures 4.8:1 and black only 4.4:1 — the
    black-on-lime rule is about lime's luminance, not about the brand, and applying it to a dark green
    fill would make the same mistake in the other direction.
*/
.oc-brand-octacer .oc-cta-block { width: 100%; justify-content: center; }

/* ── Reassurance banner ──────────────────────────────────────────────────────────────────────────────
   "Nothing else is required from you right now" — the sentence §8 and §17 both mandate. Deliberately
   quiet: it is the absence of a demand, so it must not look like one. */
.oc-brand-octacer .oc-reassure {
    display: flex; align-items: flex-start; gap: .7rem; padding: .85rem 1rem;
    border: 1px solid var(--line); border-radius: .7rem; background: var(--surface-muted);
    font-size: .875rem; line-height: 1.5; color: var(--ink-600);
}
.oc-brand-octacer .oc-reassure i { color: var(--ink-500); margin-top: .1rem; }

/* ── Horizontal step row ─────────────────────────────────────────────────────────────────────────────
   "What happens next" (§8) and the post-acceptance journey (§13). The connector is a flex child rather
   than a border, so it absorbs the leftover width and the steps stay evenly spaced at any container
   size. Wrapping is allowed; the connectors vanish once wrapped, since a line to a step on the next row
   would point at nothing.

   NOTE ON NAMING: these are oc-flow-*, NOT oc-step-*. The app already owns .oc-steps / .oc-step /
   .oc-step-num as a VERTICAL timeline (flex-direction: column, with an absolutely-positioned 2px
   ::before connector and an indigo gradient on the number). Reusing those names inherited the column
   direction and the vertical rule, which stacked this row and drew a long line down the page. A distinct
   name is the fix; overriding half a dozen inherited properties would only have hidden the clash. */
/* One line, not two: with the per-step sentence gone (see _JourneyBand.cshtml's own note), a step is
   now just a circle and a short label, so flex-wrap changed from `wrap` to `nowrap` — three of these
   plus their connectors fit comfortably in the rail-width card that used to fold them after two. Content
   drives layout here, not the other way round: the wrap fallback still exists, just later (480px, where
   even the label alone is genuinely tight), not the 760px this needed while it carried a whole sentence. */
.oc-brand-octacer .oc-flow {
    display: flex; flex-direction: row; align-items: center; gap: .85rem; flex-wrap: nowrap;
    margin: 0; padding: 0; list-style: none;
}
.oc-brand-octacer .oc-flow-step {
    display: flex; align-items: center; gap: .55rem;
    flex: 0 1 auto; min-width: 0; padding: 0; position: static;
}
.oc-brand-octacer .oc-flow-step::before { content: none; }
.oc-brand-octacer .oc-flow-num {
    width: 2.1rem; height: 2.1rem; flex: 0 0 auto; border-radius: 50%; display: grid; place-items: center;
    font-size: .82rem; font-weight: 700; box-shadow: none;
    background: var(--surface-muted); color: var(--ink-500); border: 1px solid var(--line);
}
/* @Html.Icon's own w-N/h-N utility classes give the <i> an explicit CSS width/height (e.g. w-4 -> 16px)
   that is very often NOT the glyph's own advance width — grid's place-items:center on the PARENT
   correctly centers the <i>'s box, but the leftover space inside that box was never split evenly (a
   plain block, even with line-height:1, defaults that slack to one side). Making the <i> itself a flex
   container instead — display:flex, align-items/justify-content:center — centers the glyph content
   directly within whatever box the w-N/h-N classes gave it, on both axes at once. No transform/translate
   nudge, by request: a per-icon hardcoded offset is exactly what went stale and broke before
   (.oc-section-state used to carry a hand-measured .19rem/.15rem correction for fa-xmark alone that
   drifted wrong over time) — plain flex alignment here instead. */
.oc-brand-octacer .oc-flow-num i {
    line-height: 1; display: flex; align-items: center; justify-content: center;
}
.oc-brand-octacer .oc-flow-step.is-current .oc-flow-num,
.oc-brand-octacer .oc-flow-step.is-done .oc-flow-num {
    background: var(--oc-lime); color: var(--oc-on-lime); border-color: transparent;
}
/* No longer paired with .oc-flow-text below it, so no bottom margin to balance — just a single
   vertically-centred line next to its circle. overflow/text-overflow is the same safety valve
   .oc-col-text uses elsewhere: this row no longer wraps, so a step ever needing a longer label clips
   with an ellipsis (and its `title` attribute) instead of breaking the one-line layout. */
.oc-brand-octacer .oc-flow-name {
    margin: 0; font-size: .9rem; font-weight: 700; color: var(--ink-900);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.oc-brand-octacer .oc-flow-line {
    flex: 1 1 1.5rem; min-width: .75rem; height: 1px;
    background: var(--line); padding: 0; position: static;
}
.oc-brand-octacer .oc-flow-line::before { content: none; }
.oc-brand-octacer .oc-flow-line.is-done { background: var(--oc-lime); }
@media (max-width: 480px) {
    /* Wrapped onto separate rows, a connector would point at nothing. */
    .oc-brand-octacer .oc-flow { flex-wrap: wrap; row-gap: .6rem; }
    .oc-brand-octacer .oc-flow-line { display: none; }
}

/* ── Label/value rows ────────────────────────────────────────────────────────────────────────────────
   The rail's summary blocks: an icon, what the thing is, and its value. Values right-align and use
   tabular figures so money and dates line up down the column. */
.oc-brand-octacer .oc-kv { display: grid; gap: .1rem; }
.oc-brand-octacer .oc-kv-row {
    display: grid; grid-template-columns: 1.35rem minmax(0, 1fr) auto; gap: .7rem; align-items: baseline;
    padding: .7rem 0; border-bottom: 1px solid var(--line-2);
}
.oc-brand-octacer .oc-kv-row:last-child { border-bottom: 0; }
.oc-brand-octacer .oc-kv-row > i { color: var(--ink-500); justify-self: center; }
.oc-brand-octacer .oc-kv-key { font-size: .85rem; color: var(--ink-600); }
.oc-brand-octacer .oc-kv-val { font-size: .9rem; font-weight: 700; color: var(--ink-900); text-align: right; font-variant-numeric: tabular-nums; }
.oc-brand-octacer .oc-kv-note { grid-column: 3; font-size: .74rem; font-weight: 400; color: var(--ink-500); text-align: right; }

/* ── About Octacer ───────────────────────────────────────────────────────────────────────────────────
   §25 allows this only in early prospect states, and only as a compact strip — never a marketing band.
   The disciplines are separated by rules rather than repeated bullets, so the row reads as one line of
   capability instead of four competing items. */
.oc-brand-octacer .oc-about {
    display: flex; align-items: center; gap: 1.25rem 1.75rem; flex-wrap: wrap;
    padding: 1.1rem 1.35rem; border: 1px solid var(--line); border-radius: var(--radius-lg);
    background: var(--surface);
}
.oc-brand-octacer .oc-about-name { margin: 0; font-size: .95rem; font-weight: 700; color: var(--ink-900); }
.oc-brand-octacer .oc-about-text { margin: .15rem 0 0; font-size: .82rem; color: var(--ink-600); }
.oc-brand-octacer .oc-about-list { display: flex; align-items: center; gap: 1.1rem; flex-wrap: wrap; margin-left: auto; list-style: none; padding: 0; }
.oc-brand-octacer .oc-about-list li { display: flex; align-items: center; gap: .5rem; font-size: .82rem; font-weight: 500; color: var(--ink-700); }
.oc-brand-octacer .oc-about-list li + li { padding-left: 1.1rem; border-left: 1px solid var(--line); }
.oc-brand-octacer .oc-about-list i { color: var(--oc-lime); width: 1rem; text-align: center; flex: none; }
/* Below wide desktop (by request): the four labels as a tidy 2 × 2 grid — two per line, every icon and
   label on the same left edge — instead of a wrapping row whose dividers landed at the start of a line. */
@media (max-width: 80rem) {
    .oc-brand-octacer .oc-about-list {
        display: grid; grid-template-columns: repeat(2, max-content); gap: .6rem 1.75rem;
        margin-left: 0; flex-basis: 100%;
    }
    .oc-brand-octacer .oc-about-list li + li { padding-left: 0; border-left: 0; }
}
@media (max-width: 30rem) {
    .oc-brand-octacer .oc-about-list { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .6rem 1rem; }
}

/* A slow revolving lime ring around the strip's one call-to-action — .btn's own size/padding/radius are
   untouched, this only adds a rotating conic-gradient RING sitting just outside the button via a
   pseudo-element (inset: -2px keeps it off the button's own border), masked down to a ring with the
   `mask-composite: exclude` "border painted with a gradient" trick so the button's fill/text are never
   covered. @property --oc-cta-angle is what makes the conic-gradient's rotation itself animate smoothly
   (animating a plain custom property with no registered syntax only jumps between values); browsers
   without @property support just see a static ring, which still reads fine, not broken.

   Two things that went wrong the first pass, both fixed here: the radius was .btn's base 10px instead of
   THIS button's actual .btn-sm 8px, so the ring's corners didn't line up with the button's own rounded
   corners and read as a flat/straight edge where they diverged — outer radius is now the real button
   radius PLUS the inset, which is what keeps concentric rounded rects looking like one continuous curve
   instead of pinched or flattened at the corners. And the gradient used to drop to full transparency for
   a third of the loop (a comet with a genuinely invisible gap, which is what "disconnected" describes) —
   it now never goes below a dim, always-visible base ring, with one brighter point travelling around it,
   so the outline reads as continuous at every angle. */
@property --oc-cta-angle {
    syntax: "<angle>";
    inherits: false;
    initial-value: 0deg;
}
.oc-brand-octacer .oc-about-cta { position: relative; }
.oc-brand-octacer .oc-about-cta::before {
    content: ""; position: absolute; inset: -2px; border-radius: calc(.5rem + 2px); padding: 1.5px;
    background: conic-gradient(from var(--oc-cta-angle),
        color-mix(in srgb, var(--oc-lime) 20%, transparent) 0%,
        var(--oc-lime) 16%,
        color-mix(in srgb, var(--oc-lime) 20%, transparent) 42%,
        color-mix(in srgb, var(--oc-lime) 20%, transparent) 100%);
    -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    -webkit-mask-composite: xor; mask-composite: exclude;
    animation: oc-cta-spin 4.5s linear infinite;
    pointer-events: none;
}
@keyframes oc-cta-spin { to { --oc-cta-angle: 360deg; } }
@media (prefers-reduced-motion: reduce) {
    .oc-brand-octacer .oc-about-cta::before { animation: none; }
}

/* ── Nothing-yet state ───────────────────────────────────────────────────────────────────────────────
   Home for a client with no proposal, no engagement and nothing waiting. The page used to render the
   support rail against an empty main column, which left a tall blank gap where the lead card would be —
   an accident, not a design. This fills that column deliberately: it says what is (and is not) there,
   and offers the two things a person in that position can actually do.

   Centred and generously padded because it is the whole content of the column, not a small empty slot
   inside a card. §35 forbids the alternative — a zero-metric dashboard standing in for real content. */
.oc-brand-octacer .oc-blank {
    display: flex; flex-direction: column; align-items: center; text-align: center;
    padding: 3.25rem 2rem; border: 1px solid var(--line); border-radius: var(--radius-lg);
    background: var(--surface);
    background-image: radial-gradient(30rem 18rem at 50% -30%, color-mix(in srgb, var(--oc-lime) 10%, transparent), transparent 65%);
}
.oc-brand-octacer .oc-blank-icon {
    width: 4.5rem; height: 4.5rem; border-radius: 1.1rem; display: grid; place-items: center;
    margin-bottom: 1.4rem; font-size: 1.7rem;
    background: var(--oc-lime-wash); color: var(--oc-lime);
}
.oc-brand-octacer .oc-blank-title {
    margin: 0; font-size: clamp(1.25rem, 1rem + .9vw, 1.6rem); font-weight: 800;
    letter-spacing: -.025em; color: var(--ink-900); text-wrap: balance;
}
.oc-brand-octacer .oc-blank-text {
    margin: .8rem 0 0; max-width: 46ch; font-size: .92rem; line-height: 1.6; color: var(--ink-600);
}
.oc-brand-octacer .oc-blank-actions {
    display: flex; flex-wrap: wrap; justify-content: center; gap: .7rem; margin-top: 1.6rem;
}

/* ── "Other ways we can help" ────────────────────────────────────────────────────────────────────────
   Three routes out of a dead end, as cards rather than a list of links: each one is a destination with a
   reason attached, and the reason is what makes it clickable to someone who does not yet know what this
   portal is for. auto-fit rather than a fixed three columns, so they reflow instead of shrinking. */
.oc-brand-octacer .oc-help-grid {
    display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
}
.oc-brand-octacer .oc-help-card {
    display: flex; align-items: flex-start; gap: .85rem; padding: 1.1rem 1.2rem;
    border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface);
    text-decoration: none; color: inherit;
    transition: border-color var(--duration-fast) var(--ease-out),
                background var(--duration-fast) var(--ease-out);
}
.oc-brand-octacer .oc-help-card:hover { border-color: var(--oc-lime); background: var(--surface-muted); }
.oc-brand-octacer .oc-help-card:focus-visible { outline: 2px solid var(--oc-lime); outline-offset: 2px; }
.oc-brand-octacer .oc-help-card-name { margin: 0; font-size: .9rem; font-weight: 700; color: var(--ink-900); }
.oc-brand-octacer .oc-help-card-text { margin: .25rem 0 0; font-size: .82rem; line-height: 1.5; color: var(--ink-600); }
/* The chevron is decoration on an already-obvious link, so it never takes space from the words. */
.oc-brand-octacer .oc-help-card-go { margin-left: auto; align-self: center; color: var(--ink-400); flex: 0 0 auto; }
.oc-brand-octacer .oc-help-card:hover .oc-help-card-go { color: var(--oc-lime); }

/* ── Footer ──────────────────────────────────────────────────────────────────────────────────────────*/
.oc-brand-octacer .oc-portal-foot {
    display: flex; align-items: center; justify-content: center; gap: 1rem; flex-wrap: wrap;
    margin-top: 2.25rem;
    font-size: .78rem; color: var(--ink-500);
}
.oc-brand-octacer .oc-portal-foot p { margin: 0; }

/* ── Project readiness checklist (PJ-005) ────────────────────────────────────────────────────────────
   What has to be true before a signed contract becomes a project. Three states, not two: done, a real
   blocker, and "worth knowing but not stopping you" — collapsing the last two into one red list trains
   people to ignore it. */
.oc-readiness { list-style: none; margin: 0 0 .75rem; padding: 0; display: grid; gap: .3rem; }
.oc-readiness li { display: flex; align-items: flex-start; gap: .45rem; font-size: .82rem; line-height: 1.45; }
.oc-readiness li.is-done { color: var(--ink-600); }
.oc-readiness li.is-done i { color: var(--success); }
.oc-readiness li.is-blocking { color: var(--ink-900); font-weight: 600; }
.oc-readiness li.is-blocking i { color: var(--danger); }
.oc-readiness li.is-pending { color: var(--ink-500); }
.oc-readiness li.is-pending i { color: var(--ink-400); }

/* ── Project updates timeline (DL-008 / DL-011) ──────────────────────────────────────────────────────
   One feed carrying both what staff wrote and what the system recorded, because from the reader's side
   they are the same thing: what happened, and when. */
.oc-updates { list-style: none; margin: 0; padding: 0; display: grid; gap: .75rem; }
.oc-updates li { border-left: 3px solid var(--line); padding: .1rem 0 .1rem .85rem; }
.oc-updates li.is-shared { border-left-color: var(--brand-600); }
.oc-updates-head { display: flex; align-items: center; gap: .45rem; flex-wrap: wrap; margin-bottom: .3rem; }
.oc-updates li p { margin: 0; font-size: .88rem; line-height: 1.55; color: var(--ink-700); white-space: pre-wrap; }

/* ── Client to-do list (DL-015 to DL-019) ────────────────────────────────────────────────────────────
   What Octacer is waiting on the client for. Each row is one request with its own answer controls —
   never a bare list, because "approve" and "request changes" both need somewhere to happen. */
.oc-action-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 1rem; }
.oc-action-item { border: 1px solid var(--line); border-radius: var(--radius); padding: 1rem; }
.oc-action-item.is-closed { opacity: .8; }
.oc-action-main strong { font-size: .95rem; color: var(--ink-900); }
.oc-action-controls { display: flex; gap: .5rem; margin-top: .75rem; flex-wrap: wrap; }
.oc-action-panel { margin-top: .75rem; padding-top: .75rem; border-top: 1px solid var(--line-2); }
.oc-panel-actions { display: flex; align-items: center; justify-content: flex-end; gap: .5rem; margin-top: .5rem; }

/* ── Payment confirmation modal (IN-020 / IN-021) ────────────────────────────────────────────────────
   What Finance recorded against an invoice, as its own list — a partial payment shows the same way a
   full one does, each its own confirmed row. */
.oc-payments-list { list-style: none; margin: 0 0 .75rem; padding: 0; display: grid; gap: .6rem; }
.oc-payments-list li { border-bottom: 1px solid var(--line-2); padding-bottom: .5rem; }
.oc-payments-list li:last-child { border-bottom: 0; }
.oc-payments-row { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; }
.oc-payments-total { display: flex; justify-content: space-between; padding-top: .5rem; border-top: 1px solid var(--line); font-size: .9rem; }

/* A feedback thread whose dead space jumps to its section (data-oc-jump in app.js). The pointer appears
   on the card but reverts inside the reply form — the cursor should never promise a navigation that the
   handler is going to decline. */
.oc-comment-thread[data-oc-jump] { cursor: pointer; }
.oc-comment-thread[data-oc-jump] .oc-comment-reply-form,
.oc-comment-thread[data-oc-jump] .oc-comment-reply-form * { cursor: auto; }
.oc-comment-thread[data-oc-jump] .oc-comment-reply-form button { cursor: pointer; }
.oc-comment-thread[data-oc-jump]:hover { border-color: var(--line-strong); }
/* The heading is the real link; it keeps link affordances rather than inheriting the card's. */
a.oc-comment-thread-title { text-decoration: none; }
a.oc-comment-thread-title:hover { text-decoration: underline; }

/* ── Home: active-project overview ───────────────────────────────────────────────────────────────────
   What Home becomes once delivery is under way: the project, where it has got to, what is next, and
   what happened lately. Replaces a row of counts — "60%, Development phase, Testing & Review next"
   answers the question a client opens this page with; "1 active project" does not. */
.oc-brand-octacer .oc-proj-hero { padding: 1.5rem 1.65rem; }
.oc-brand-octacer .oc-proj-title {
    margin: .9rem 0 0; font-size: clamp(1.3rem, 1rem + 1.1vw, 1.75rem); font-weight: 800;
    letter-spacing: -.03em; line-height: 1.15; color: var(--ink-900); text-wrap: balance;
}
.oc-brand-octacer .oc-proj-sub { margin: .6rem 0 0; font-size: .92rem; line-height: 1.6; color: var(--ink-600); max-width: 52ch; }

/* Started / target / team. auto-fit so three facts on a wide card become one column on a narrow one
   without a breakpoint per arrangement. */
.oc-brand-octacer .oc-proj-facts {
    display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
    margin: 1.35rem 0 0;
}
.oc-brand-octacer .oc-proj-fact { display: flex; align-items: flex-start; gap: .65rem; }
.oc-brand-octacer .oc-proj-fact i { color: var(--oc-lime); margin-top: .15rem; }
.oc-brand-octacer .oc-proj-fact-k { margin: 0; font-size: .74rem; text-transform: uppercase; letter-spacing: .07em; color: var(--ink-500); }
.oc-brand-octacer .oc-proj-fact-v { margin: .2rem 0 0; font-size: .9rem; font-weight: 700; color: var(--ink-900); }

/* Progress. The number sits beside the bar rather than inside it: a figure printed on a partially
   filled bar changes contrast as the bar grows past it. */
.oc-brand-octacer .oc-proj-progress { display: flex; align-items: center; gap: 1rem; margin: 1.35rem 0 0; }
.oc-brand-octacer .oc-proj-bar {
    flex: 1 1 auto; height: .7rem; border-radius: 999px; overflow: hidden;
    background: var(--surface-muted); box-shadow: inset 0 0 0 1px var(--line);
}
.oc-brand-octacer .oc-proj-bar-fill { height: 100%; border-radius: 999px; background: var(--oc-lime); }
.oc-brand-octacer .oc-proj-pct { font-size: 1.1rem; font-weight: 800; color: var(--ink-900); font-variant-numeric: tabular-nums; }
.oc-brand-octacer .oc-proj-phase { margin: 0; font-size: .875rem; color: var(--ink-600); }

/* Where the project stands, and the way into it — one row, as the reference draws them: the sentence
   reads left, the action sits right. They wrap to their own lines on a narrow card, where a sentence
   squeezed beside a button is worse than either on its own. */
.oc-brand-octacer .oc-proj-foot {
    display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
    gap: .85rem 1.5rem; margin: 1.15rem 0 0;
}
.oc-brand-octacer .oc-proj-foot .oc-hero-action { margin: 0; }

/* ── Milestone timeline ──────────────────────────────────────────────────────────────────────────────
   A vertical rail with one marker per milestone. The connector is drawn by the marker's ::after rather
   than a wrapper border, so the last row simply has none and the line stops where the work does. */
.oc-brand-octacer .oc-tl { list-style: none; margin: 0; padding: 0; }
.oc-brand-octacer .oc-tl-item { display: grid; grid-template-columns: 1.4rem minmax(0, 1fr) auto; gap: .85rem;
  align-items: start; padding-bottom: 1.15rem; }
.oc-brand-octacer .oc-tl-item:last-child { padding-bottom: 0; }
.oc-brand-octacer .oc-tl-dot {
    position: relative; width: 1.4rem; height: 1.4rem; border-radius: 50%;
    display: grid; place-items: center; font-size: .6rem;
    background: var(--surface-muted); color: var(--ink-500); box-shadow: inset 0 0 0 1px var(--line);
}
.oc-brand-octacer .oc-tl-item:not(:last-child) .oc-tl-dot::after {
    content: ""; position: absolute; top: 1.55rem; left: 50%; transform: translateX(-50%);
    width: 2px; height: calc(100% + .5rem); background: var(--line);
}
.oc-brand-octacer .oc-tl-item.is-done .oc-tl-dot {
    background: var(--oc-lime); color: var(--oc-on-lime); box-shadow: none;
}
.oc-brand-octacer .oc-tl-item.is-done .oc-tl-dot::after { background: var(--oc-lime); }
/* The one in progress: ringed rather than filled, so "here" reads differently from "finished". */
.oc-brand-octacer .oc-tl-item.is-current .oc-tl-dot {
    background: transparent; color: var(--oc-lime);
    box-shadow: inset 0 0 0 2px var(--oc-lime);
}
.oc-brand-octacer .oc-tl-name { margin: .1rem 0 0; font-size: .9rem; font-weight: 700; color: var(--ink-900); }
.oc-brand-octacer .oc-tl-state { margin: .2rem 0 0; font-size: .78rem; color: var(--ink-500); }
.oc-brand-octacer .oc-tl-when { font-size: .8rem; color: var(--ink-500); white-space: nowrap; font-variant-numeric: tabular-nums; }
@media (max-width: 620px) {
    /* The date drops under the name rather than squeezing it to nothing. */
    .oc-brand-octacer .oc-tl-item { grid-template-columns: 1.4rem minmax(0, 1fr); }
    .oc-brand-octacer .oc-tl-when { grid-column: 2; }
}

/* ── Activity feed ───────────────────────────────────────────────────────────────────────────────────*/
.oc-brand-octacer .oc-feed { list-style: none; margin: 0; padding: 0; display: grid; gap: 1rem; }
.oc-brand-octacer .oc-feed-item { display: grid; grid-template-columns: 2rem minmax(0, 1fr) auto; gap: .85rem; align-items: start; }
.oc-brand-octacer .oc-feed-icon {
    width: 2rem; height: 2rem; border-radius: 50%; display: grid; place-items: center;
    background: var(--oc-lime-wash); color: var(--oc-lime); font-size: .8rem;
}
.oc-brand-octacer .oc-feed-name { margin: 0; font-size: .875rem; font-weight: 700; color: var(--ink-900); }
.oc-brand-octacer .oc-feed-body { margin: .2rem 0 0; font-size: .82rem; line-height: 1.5; color: var(--ink-600); overflow-wrap: anywhere; }
.oc-brand-octacer .oc-feed-when { font-size: .78rem; color: var(--ink-500); white-space: nowrap; }
@media (max-width: 620px) {
    .oc-brand-octacer .oc-feed-item { grid-template-columns: 2rem minmax(0, 1fr); }
    .oc-brand-octacer .oc-feed-when { grid-column: 2; }
}

/* File inputs ("Contract PDF", document uploads): the native "Choose file" button is a button, but with
   no cursor rule it inherits the text input's default I-beam, reading as disabled or non-interactive. */
input[type="file"]::file-selector-button { cursor: pointer; }

/* A <select> is a picker, not a text field — Chromium/Windows leaves it the default arrow cursor with no
   rule of its own, which reads as "nothing happens here" on a control whose entire job is being clicked.
   Every dropdown in the app (.field-select), not just one: the same control, the same expectation. */
.field-select:not(:disabled) { cursor: pointer; }

/* ── Themed <select> popup ────────────────────────────────────────────────────────────────────────────
   The CLOSED control has always been ours — border, radius, chevron. The OPEN list was not: a native
   select's popup is drawn by the operating system, outside the page entirely, so no ordinary CSS reaches
   it. That is why a dropdown sitting in a themed filter bar opened as a grey list with a system-blue
   highlight, matching nothing around it.

   `appearance: base-select` is the standards-track opt-in that moves the popup INTO the page as a real
   ::picker(select) element we can style. Wrapped in @supports so this is pure enhancement: a browser
   without it keeps the native popup, which is perfectly usable — and, importantly, this is still a real
   <select>. Keyboard behaviour, screen-reader semantics, form submission and the native picker phones
   show are all unchanged, which is the thing a hand-built JS dropdown has to reimplement and usually
   gets wrong. Nothing here is load-bearing: delete the block and the app still works. */
@supports (appearance: base-select) {
    .field-select,
    .field-select::picker(select) { appearance: base-select; }

    /* The UA adds its own arrow once base-select is on; ours is already painted as a background image. */
    .field-select::picker-icon { display: none; }

    .field-select::picker(select) {
        border: 1px solid var(--line);
        border-radius: var(--radius);
        background: var(--surface);
        box-shadow: var(--shadow-lg);
        padding: .25rem;
        /* A long list (every country, every client) scrolls inside the popup rather than running off
           the screen. */
        max-height: 18rem;
        overflow-y: auto;
        /* Exactly the width of the field it belongs to. Left to itself the popup sizes to its widest
           option, so one outlier ("AED — United Arab Emirates Dirham" in a currency list) pushed the
           whole list out past the field's right edge and over the column beside it — the popup stopped
           looking like it belonged to the control that opened it. anchor-size(width) is the picker's own
           anchor, i.e. this select, so it tracks whatever width the field has in whichever grid it sits.
           Nothing is truncated by this: with a width to work within, a long option wraps instead, which
           is why the open list stays the place the whole label is readable (see the closed-control rule
           at the end of this block). */
        box-sizing: border-box;
        width: anchor-size(width);
    }

    .field-select option {
        display: flex; align-items: center; gap: .5rem;
        padding: .45rem .6rem;
        border-radius: .4rem;
        font-size: .8125rem;
        color: var(--ink-900);
        background: transparent;
    }
    /* Hover and keyboard focus get the SAME treatment: the arrow keys and the mouse are two ways of
       pointing at one thing, and a list that highlights them differently reads as two lists. */
    .field-select option:hover,
    .field-select option:focus { background: var(--surface-muted); }
    .field-select option:checked { background: var(--brand-50); color: var(--brand-700); font-weight: 600; }
    /* The UA's tick for the selected row: removed, because the highlight above already says which it is
       and the mark shifts every label a few pixels right. */
    .field-select option::checkmark { display: none; }

    /* With appearance: base-select, the CLOSED control shows the checked <option>'s own real markup —
       so a long combination (a contact's name, its parenthesised email, " · primary") wraps across two
       or three lines inside the fixed-width closed box, which a plain OS-drawn select never did. Scoped
       to :not(:open): the OPEN popup keeps display:flex above and stays fully readable, since that is
       the one place there is room for the whole thing — only the collapsed button truncates. */
    .field-select:not(:open) option:checked {
        display: block;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        max-width: 100%;
    }
}

/* The per-client proposal number, riding top-right of the title in the same cell. Small, muted and
   tabular so it reads as a marker on the name rather than as a second piece of content competing with it.
   The cell keeps truncating its title: the label is a flex item of fixed width, so a long name ellipsises
   against it instead of pushing it out of the row. */
/* A wrapper INSIDE the cell, never the cell itself: a <td> made display:flex leaves the table's layout
   algorithm, and under table-layout:fixed the column then resolved to no usable width — the title
   vanished completely. The cell stays a cell; this span is what lays its two pieces out. */
.oc-title-cell { display: flex; align-items: baseline; justify-content: space-between; gap: .5rem; width: 100%; }
/* Beats .oc-table-dense td.oc-col-text > a { display: inline }, which would stop the link being a flex
   item and take its truncation with it. */
.oc-table-dense td.oc-col-text .oc-title-cell > a {
    display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
}
.oc-title-version {
    flex: 0 0 auto;
    font-size: .65rem; line-height: 1.1;
    color: var(--ink-400);
    letter-spacing: .01em;
}

/* A tab that LEAVES the page. It sits in the same strip as the real tabs, so it has to look like one —
   but it never becomes "is-active", and pretending otherwise would promise a panel that never appears.
   The trailing arrow (in the markup) is what says it navigates; this keeps it in the strip's rhythm and
   gives it the link colour on hover so the difference is felt before it is clicked. */
.oc-tab-link { text-decoration: none; }
.oc-tab-link:hover { color: var(--primary); }

/* Proposal Builder — Revisions list: one row per send, opened read-only in a new tab. */
.oc-revision-list { gap: .5rem; }
.oc-revision-row {
  display: flex; align-items: center; gap: .75rem; flex-wrap: wrap;
  padding: .5rem .75rem; border: 1px solid var(--line); border-radius: var(--radius-md);
}
.oc-revision-row .btn { margin-left: auto; }

/* .oc-tooltip-end (a manual "anchor this tooltip to its right edge" opt-out) no longer does anything —
   see the oc-admin-tooltip note near the top of this file for why, and where tooltip positioning now
   actually lives for the Admin side. The class is harmless left on an element; nothing reads it any more. */

/* A field that posts but is not for typing into — the contract's Pricing while a proposal is linked
   (_ContractModelField). Muted rather than greyed-out like [disabled]: the number is real and is saved,
   it is just not this form's to change. */
.field-input[readonly] { color: var(--ink-500); background: color-mix(in srgb, var(--line-2) 60%, transparent); cursor: default; }
.field-input[readonly]:focus { box-shadow: none; border-color: var(--line); }

/* ── Signature field (_SignaturePad) ─────────────────────────────────────────────────────────────────
   The stored signature sits above the capture controls; it is the one thing on the document today, so
   it gets the same preview treatment as a fresh capture but marked as current. */
.oc-editor-subhead { margin-bottom: .5rem; }
.oc-signature-stored img { background: #fff; padding: .25rem; }
.oc-signature-field .oc-signature-pad { touch-action: none; }

/* The Upload signature / Sign on the pad choice — octacer-theme.css's plain .oc-view-toggle is a tight,
   auto-width control meant for a handful of icon-only view switches elsewhere; stretched across a whole
   field with text labels it read as two loose, cramped links rather than one clear either/or choice. This
   makes it read as a single control: full width, equal-width halves, a visible border on the whole group,
   and a stronger active state (brand-tinted background, not just white-on-grey) so which mode is on is
   obvious at a glance. */
.oc-signature-field .oc-view-toggle {
    display: flex;
    width: 100%;
    padding: .25rem;
    gap: .25rem;
    background: var(--surface-muted);
    border: 1px solid var(--line);
    border-radius: .75rem;
}
.oc-signature-field .oc-view-toggle-button {
    flex: 1 1 0;
    min-width: 0;
    min-height: 2.5rem;
    font-size: .8125rem;
    white-space: nowrap; /* "Upload signature" / "Sign on the pad" on one line each, by request */
    font-weight: 600;
    border-radius: .55rem;
    transition: background-color .15s ease, color .15s ease, box-shadow .15s ease;
}
@media (max-width: 30rem) {
    /* A narrow phone: smaller type and tighter padding so both labels still fit on their one line. */
    .oc-signature-field .oc-view-toggle-button { font-size: .75rem; padding: 0 .4rem; gap: .3rem; }
}
.oc-signature-field .oc-view-toggle-button:hover { background: color-mix(in srgb, var(--surface) 60%, transparent); }
.oc-signature-field .oc-view-toggle-button.is-active {
    background: var(--surface);
    color: var(--brand-700);
    box-shadow: var(--shadow-sm), inset 0 0 0 1px color-mix(in srgb, var(--brand-700) 25%, transparent);
}

/* ============================================================
   Optional detail, folded away (.oc-more)
   ------------------------------------------------------------
   A <details> block for the part of a form nobody fills in most of the time — company details on the
   client form, the extras on a new proposal. Native <details>, not an Alpine toggle: it opens with no
   script, it is keyboard- and screen-reader-operable for free, and `open` renders server-side when the
   block already has something in it (an edit, or a submission sent back with errors).
   ============================================================ */
.oc-more { margin-top: 1.1rem; border-top: 1px solid var(--line-2); padding-top: .9rem; }
.oc-more-summary {
    display: flex; align-items: center; gap: .5rem; cursor: pointer;
    font-size: .85rem; font-weight: 600; color: var(--ink-700);
    list-style: none; min-height: 2.75rem;
}
.oc-more-summary::-webkit-details-marker { display: none; }
/* The disclosure triangle, drawn rather than inherited so it can rotate and match the theme's chevrons. */
.oc-more-summary::before {
    content: ""; width: .5rem; height: .5rem; flex: 0 0 auto;
    border-right: 2px solid var(--ink-400); border-bottom: 2px solid var(--ink-400);
    transform: rotate(-45deg); transition: transform .15s ease; margin-right: .1rem;
}
.oc-more[open] > .oc-more-summary::before { transform: rotate(45deg); }
.oc-more-summary:hover { color: var(--ink-900); }
.oc-more-summary:focus-visible { outline: 2px solid var(--brand-500); outline-offset: 3px; border-radius: .4rem; }

/* An info icon beside a header action, explaining what the action is for. Focusable so the tooltip is
   reachable from the keyboard, and carrying the same sentence as an aria-label so it is not
   hover-only. */
.oc-info-dot {
    display: inline-flex; align-items: center; justify-content: center;
    width: 1.75rem; height: 1.75rem; border-radius: 999px;
    color: var(--ink-400); cursor: help;
}
.oc-info-dot:hover, .oc-info-dot:focus-visible { color: var(--ink-700); background: var(--surface-muted); }
.oc-info-dot:focus-visible { outline: 2px solid var(--brand-500); outline-offset: 2px; }

/* The autosave line that replaced the Save button. Quiet by default — a save that worked is not news —
   and only loud when something is genuinely unsaved. */
.oc-autosave { margin-left: auto; font-size: .78rem; color: var(--ink-400); display: inline-flex; align-items: center; gap: .35rem; }
.oc-autosave[data-state="dirty"] { color: var(--warning-foreground, #92400e); }
.oc-autosave[data-state="busy"] { color: var(--ink-500); }
.oc-autosave[data-state="error"] { color: var(--danger, #b91c1c); font-weight: 600; }
.oc-autosave::before { content: ""; width: .4rem; height: .4rem; border-radius: 999px; background: currentColor; opacity: .55; }

/* ---- Tables on a phone (portal-ux §95–§97) ---------------------------------------------------------
   THE BUG THIS FIXES, and it is one missing property. Every column here is sized as a PERCENTAGE, and
   .oc-table is width:100% — so the table resolves against its container and can never be wider than the
   viewport. A table that always fits can never scroll, which is why .oc-table-scroll (overflow-x:auto,
   already correct, already wrapping all 29 of these) had nothing to do: at 390px the nine proposal
   columns got about 43px each, the header cells had no room to truncate, and PROPOSAL/CLIENT/BILLING
   TYPE printed on top of each other.

   A min-width in ABSOLUTE units is what gives the percentages something other than the viewport to
   divide up, and the wrapper the overflow it was always waiting for.

   NOT CARDS, deliberately (§96): "horizontal scroll preserves column relationships; responsive-hide only
   genuinely secondary columns; never hide critical info". A table exists to be compared DOWN a column —
   stack each row into its own block and "which of these is Draft" stops being one glance. Cards remain
   available the sanctioned way, as a deliberate list view behind _ViewToggle, which is a different thing
   from the layout doing it to you below a breakpoint. */
@media (max-width: 60rem) {
    /* Wide enough that the columns left below are readable, narrow enough that the scroll is short. */
    .oc-table-scroll > .oc-table { min-width: 44rem; }

    /* A header that has been given room should still refuse to wrap into two lines mid-word. */
    .oc-table-scroll > .oc-table thead th { white-space: nowrap; }

    /* SECONDARY COLUMNS ONLY. What stays on every table: what the row IS, who it belongs to, what state
       it is in, and what you can do to it. What goes: counts, dates and classifications — the things you
       come to a desktop to compare, never the thing you came to this row to find. Nothing hidden here is
       the only copy of anything; each is on the record's own page. */
    .oc-table-clients .oc-col-country,
    .oc-table-clients .oc-col-count,
    .oc-table-clients .oc-col-since,
    .oc-table-proposals .oc-col-pricing,
    .oc-table-proposals .oc-col-notes,
    .oc-table-proposals .oc-col-feedback,
    .oc-table-proposals .oc-col-date,
    .oc-table-contracts .oc-col-type,
    .oc-table-contracts .oc-col-pricing,
    .oc-table-contracts .oc-col-portal,
    .oc-table-invoices .oc-col-date,
    .oc-table-invoices .oc-col-methods,
    .oc-table-invoices .oc-col-pricing,
    .oc-table-invoices .oc-col-portal,
    .oc-table-invoices .oc-col-out,
    .oc-table-receipts .oc-col-sent,
    .oc-table-receipts .oc-col-invoice { display: none; }
}

/* Receipts on a phone: two columns are hidden, so the desktop percentages add up to ~77% and the table
   stopped short, leaving the heading background ending before the right edge. Let the table fill its
   box and size the columns by content instead; the scroll wrapper still handles any overflow. */
@media (max-width: 60rem) {
    .oc-table-receipts { table-layout: auto; width: 100%; }
    .oc-table-receipts :is(thead th, tbody td) { width: auto; }
}

/* ==== Mobile flow — Option A "Minimal & Calm" ========================================================
   Chosen from Agreement-Mobile-Full-Flow-Design-Options.pdf: today's structure, the known phone bugs
   fixed, nothing reinvented. Replaces the earlier Option C treatment (bottom sheets, underlined fields,
   icon badges) — plain boxed fields in plain cards, as on desktop. Everything here is phone-only; the
   desktop layout is untouched. */
@media (max-width: 60rem) {
    /* (The phone-only "soft dot" that replaced the red asterisk was removed by request — mandatory
       fields show the same red * as on desktop, .oc-required above.) */

    /* ---- 1. New agreement ---------------------------------------------------------------------------
       The thumbnails were a phone-screen each; three cards took four screens to scroll past. The name,
       "When to use" and the section count are what decide which boxes to tick. */
    .oc-template-card .oc-thumb { display: none; }
    /* Agreement cards (the ones carrying the "when to use" description), by request: the name, the
       "When to use" callout (desktop's own tinted style, kept), then the checkbox — no section count,
       no "All N sections" disclosure. */
    .oc-template-card:has(.is-when) { padding: .9rem 1rem; }
    .oc-template-card:has(.is-when) .oc-template-body { gap: .2rem; padding-right: 0; }
    .oc-template-card:has(.is-when) .oc-template-name { font-size: 1rem; font-weight: 700; }
    .oc-template-card:has(.is-when) .oc-template-meta { display: none; }
    .oc-template-card:has(.is-when) > label { padding: .6rem 0 0; font-size: .8125rem; color: var(--ink-600); }
    .oc-template-card:has(.is-when) .oc-template-preview { display: none; }
    /* A hover tooltip nobody can hover — the button's own label already says it. */
    .oc-head-actions:has([data-testid="proposal__add-client"]) .oc-info-dot { display: none; }
    .oc-head-actions:has([data-testid="proposal__add-client"]) { flex: 1 1 100%; }
    .oc-head-actions:has([data-testid="proposal__add-client"]) .btn { flex: 1 1 auto; justify-content: center; }
    /* Contact/Currency's slot before a client is picked: an empty grid with a top margin was a blank
       band inside the Client card. */
    #agreement_client_extras:empty { display: none; }

    /* Sticky bottom button: "Start drafting" sat between the heading and the first card, and Back/Start
       on the shared step at the very bottom of a long form — either way a scroll back to submit. Pinned to
       the bottom edge instead. Each lives in its own tab panel, so only the showing screen's bar is on. */
    [data-oc-tab-panel="picker"] .oc-section > .oc-head-actions:has([data-oc-batch-start]),
    .oc-shared-panel-actions {
        /* Above the phone tab bar, not at the window's edge: at bottom: 0 the tab bar covered it and
           "Start drafting" / "Start" could not be seen or pressed (by request). --oc-bottom-chrome is the
           tab bar's height (0 when there is none, or while the keyboard is open). */
        position: fixed; left: 0; right: 0; bottom: var(--oc-bottom-chrome, 0px); z-index: 30; margin: 0;
        display: flex; gap: .6rem;
        padding: .75rem 1rem max(.75rem, env(safe-area-inset-bottom));
        background: linear-gradient(to top, var(--bg) 72%, transparent);
    }
    [data-oc-tab-panel="picker"] .oc-section > .oc-head-actions [data-oc-batch-start],
    .oc-shared-panel-actions [data-oc-batch-start] {
        flex: 1 1 0; min-width: 0; justify-content: center; min-height: 2.9rem;
        font-size: .8125rem; /* smaller "Start drafting" text on a phone, by request */
    }
    /* Phone, by request: the button reads just "Start Drafting" — the "N agreement(s)" count is desktop's.
       The count span stays in the DOM (agreement-batch-start.js keeps it current). */
    [data-oc-batch-start] .oc-batch-extra { display: none; }
    .oc-batch-start-foot { display: none; } /* desktop's bottom copy — a phone has its own bottom bar */
    [data-oc-batch-start] .oc-batch-label { text-transform: capitalize; }
    /* The arrow's glyph sat at the top of its 16px icon box, so it read higher than the text beside it. */
    [data-oc-batch-start] > i { display: inline-flex; align-items: center; justify-content: center; line-height: 1; }
    /* .oc-form-actions stacks its buttons full-width on a phone; this bar is Back + one wide button. */
    .oc-form-actions.oc-shared-panel-actions { flex-direction: row; }
    .oc-form-actions.oc-shared-panel-actions > .btn { width: auto; }
    .oc-shared-panel-actions [data-oc-tab="picker"] { flex: 0 0 auto; min-height: 2.9rem; }
    /* Room under the last card for the pinned bar, so it never covers it. */
    form:has([data-oc-batch-start]) { padding-bottom: 5rem; }

    /* ---- 2. Shared fields -----------------------------------------------------------------------
       Its own screen: the client card and "New client" were still showing above it, a screen of fields
       already answered. The header's icon and sub-line repeated the banner inside the card; the banner
       stays, as the one short explanation. */
    form:has([data-oc-tab-panel="shared"].is-active) [data-oc-builder-tabs] > .oc-card,
    body:has([data-oc-tab-panel="shared"].is-active) .oc-page-head .oc-head-actions { display: none; }
    .oc-shared-header .oc-section-ic,
    .oc-shared-info-card:has(#shared_agreement_fields.is-filled) .oc-shared-s-filled { display: none; }
    .oc-shared-header { margin-top: .5rem; }
    /* Group rows: name, count and chevron fought for one line and every name wrapped a word a line. The
       count drops under the name instead. */
    .oc-sharedgroup-summary {
        display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
        column-gap: .75rem; row-gap: .1rem; padding: .75rem .85rem;
    }
    .oc-sharedgroup-num { grid-row: span 2; width: 1.9rem; height: 1.9rem; font-size: .82rem; }
    .oc-sharedgroup-titles { grid-column: 2; }
    .oc-sharedgroup-count { grid-column: 2; grid-row: 2; font-size: .75rem; }
    .oc-sharedgroup-chev { grid-column: 3; grid-row: 1 / span 2; }
    .oc-sharedgroup-body { padding: .85rem; }

    /* ---- 3 & 4. Basic info and Sections ---------------------------------------------------------
       One plain card around the fields, boxed inputs inside — the flat "one surface" layout left the
       fields floating on the page background with nothing saying where the form ends. */
    .oc-editor-pane > .oc-form { padding: 1rem; }
    #proposal-details-form {
        border: 1px solid var(--line-2); border-radius: .9rem; background: var(--surface);
        padding: 1rem;
    }
}

/* Solid band behind the fixed menu button (Option A: "no more overlap"). The button itself was always
   opaque, but page text scrolled up around and under its edges, so headings read as clipped by it. The
   band is the page background, full width and as tall as the button's clearance (.oc-main's 3.75rem
   padding-top), so content scrolls away beneath it instead. z-index 35: above page content and the
   sticky bars (20/30), below the drawer overlay (40) and the sidebar (50), so an open drawer still dims it. */
@media (max-width: 1023px) {
    body:has(.oc-sidebar-mobile-button)::before {
        content: ""; position: fixed; top: 0; left: 0; right: 0; z-index: 35;
        height: 3.6rem; background: var(--bg);
        box-shadow: 0 1px 0 var(--line-2);
        pointer-events: none;
    }
}
@media (max-width: 60rem) {
    /* The sticky Previous/Next: Next is the button that gets pressed, so it takes the width. */
    .oc-sectionstep-nav .btn { flex: 1 1 0; }
}
/* A straight line between the two Send-dialog paragraphs, by request — the explanation ("You're about to
   send...") and the impact line ("They'll receive an email...") used to run together with only a gap.
   Scoped to send_modal alone (id selector), not the shared _ConfirmModal primitive, which is octa-core-
   owned and never hand-edited — every other confirm dialog built from it is untouched. */
#send_modal .text-sm.font-medium.text-mono {
    border-top: 1px solid var(--line);
    padding-top: .75rem;
}

@media (max-width: 60rem) {
    /* ---- 5. Send confirm (Option A) --------------------------------------------------------------
       A sheet along the bottom edge, where the thumb already is after pressing Send, with "Not yet" and
       "Send" as two equal halves rather than two small buttons pushed to the right. */
    #send_modal.oc-modal { align-items: flex-end; padding: 0; }
    #send_modal .oc-modal-panel {
        max-width: none; border-radius: 1.25rem 1.25rem 0 0;
        padding-bottom: env(safe-area-inset-bottom);
    }
    #send_modal .oc-modal-panel > div:last-child > .btn { flex: 1 1 0; justify-content: center; }
}

/* ---- Mobile buttons: one primary per screen --------------------------------------------------------
   On a phone every screen had two or three solid buttons of equal weight (Start drafting + Save,
   Next agreement + Next, orange testing buttons as big as Send), so nothing said which one moves the
   work forward. Now each screen keeps ONE solid primary — Send in the header, "Start drafting" in the
   bottom bar, "Next" on a section — and everything else steps down a level:
     secondary  = outline in the brand colour, full width (Save, Previous/Next agreement);
     testing    = small dashed chips, right-aligned, clearly not part of the real flow. */
@media (max-width: 60rem) {
    /* Testing-only tools: the shared card's own dummy button, and the Builder header's pair (now plain
       children of .oc-head-actions, after Send — see the markup comment there for why AFTER). */
    div:has(> [data-oc-shared-dummy]) {
        display: flex; justify-content: flex-end; gap: .4rem; margin-bottom: .75rem;
    }
    .oc-head-actions > .btn.oc-icon-warning,
    [data-oc-shared-dummy].btn {
        min-height: 0; height: 2rem; padding: 0 .65rem; margin: 0; gap: .35rem;
        font-size: .75rem; font-weight: 600; box-shadow: none;
        background: transparent; border: 1px dashed color-mix(in srgb, var(--warning) 55%, transparent);
    }

    /* Shared card: the note, then the autosave status under it (no Save button any more). */
    .oc-sharedgroup-foot { flex-direction: column; align-items: stretch; gap: .6rem; }
    .oc-sharedgroup-note { margin-right: 0; font-size: .78rem; }
    .oc-sharedgroup-foot .oc-autosave { margin-left: 0; }

    /* Previous / Next agreement: moving to another document of the batch, a secondary step next to the
       sticky "Next" section button. Stacked full width so a long template name never wraps inside it. */
    .oc-editor-batch-bar { display: flex; flex-direction: column; gap: .5rem; }
    .oc-editor-batch-bar > .oc-inline-form { display: block; width: 100%; }
    .oc-editor-batch-bar .btn {
        width: 100%; justify-content: center; min-height: 2.6rem;
        background: var(--surface); color: var(--brand-600); border: 1.5px solid var(--brand-500); box-shadow: none;
    }

    /* A section's footer: the requirement note on the left, "Saved" on the right, on one line —
       not an indented, stacked pair. */
    .oc-form-actions.oc-editor-actions {
        flex-direction: row; justify-content: space-between; align-items: center; gap: .75rem;
        padding-inline: 0;
    }
    .oc-form-actions.oc-editor-actions .oc-autosave { margin-left: auto; white-space: nowrap; }
}
@media (max-width: 60rem) {
    /* The shared step's sections were cards inside a card — two sets of walls and padding on a 375px
       screen. The outer one comes down; each section card sits on the page gutter. */
    .oc-shared-body { border: 0; background: transparent; box-shadow: none; }
    .oc-shared-body > .oc-card-body { padding: 0; }
    .oc-shared-body #shared_agreement_fields { margin-top: 0; }
    /* Back in the bottom bar already leaves this step; a second, bare "Cancel" link floated above it. */
    form:has([data-oc-tab-panel="shared"].is-active) > .oc-form-actions:not(.oc-shared-panel-actions) { display: none; }
}
/* Builder header's status · client · valid-until line: each piece stays on ONE line and the row wraps
   between pieces instead — squeezed flex items were breaking "Asadullah Asghar" and "valid until 29 Sep
   2026" across two lines each on a phone. A piece longer than the row (a long proposal title) clips. */
.oc-head-meta { flex-wrap: wrap; row-gap: .2rem; min-width: 0; }
.oc-head-meta > * { white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
@media (max-width: 60rem) { .oc-head-meta > .text-sm { font-size: .8125rem; } }

/* Portal Agreements: "Sign several agreements at once" (Contracts/Index.cshtml, portal-sign-all.js). */
.oc-sign-all-bar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 1rem; }
.oc-sign-all-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 1rem; }
.oc-sign-all-form { display: grid; gap: 1rem; margin-top: 1.25rem; padding-top: 1.25rem; border-top: 1px solid var(--line-2); }
.oc-sign-all-form[hidden] { display: none; }
.oc-sign-all-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
.oc-sign-all-form-actions { display: flex; justify-content: flex-end; gap: .5rem; }
/* "Select all" sat above the text's centre line (the theme's inline-check is baseline-aligned). */
.oc-sign-all-actions .oc-check-inline { display: inline-flex; align-items: center; gap: .5rem; line-height: 1; }
.oc-sign-all-actions .oc-check-inline .oc-check { margin: 0; }
/* Rows have no box of their own — "Select all" marks each agreement it will sign with this highlight. */
.oc-proposal-card.is-sign-selected {
    border-color: var(--brand-500);
    box-shadow: 0 0 0 1px var(--brand-500), 0 0 1.25rem -0.25rem color-mix(in srgb, var(--brand-500) 45%, transparent);
    background: color-mix(in srgb, var(--brand-500) 6%, transparent);
}
@media (max-width: 560px) {
    .oc-sign-all-grid { grid-template-columns: 1fr; }
    .oc-sign-all-actions { width: 100%; justify-content: space-between; }
}

/* The app's date calendar (app.js), in place of the browser's own picker on every screen size: above
   the field on a phone, below it (flipping when needed) on desktop. Day cells are the theme's .oc-cal-day. */
.oc-dp-panel {
    position: fixed; z-index: 80; width: min(19rem, calc(100vw - 2rem)); padding: .75rem;
    background: var(--surface); border: 1px solid var(--line); border-radius: .9rem;
    box-shadow: 0 12px 32px rgba(15, 23, 42, .18);
}
.oc-dp-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: .35rem;
    font-size: .9rem; color: var(--ink-900); }
.oc-dp-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: .15rem; }
.oc-dp-grid .oc-cal-day { min-height: 2.1rem; }
.oc-dp-foot { display: flex; justify-content: space-between; margin-top: .35rem; }
/* A smaller calendar on every screen, by request (phone first, then desktop too) — narrower panel,
   short (not square) day cells, smaller type — so it no longer covers the fields around the date. */
.oc-dp-panel { width: min(16rem, calc(100vw - 2rem)); padding: .55rem .6rem; border-radius: .8rem; }
.oc-dp-head { font-size: .82rem; margin-bottom: .2rem; }
.oc-dp-head .oc-icon-btn { width: 1.9rem; height: 1.9rem; }
.oc-dp-grid { gap: .1rem; }
.oc-dp-grid .oc-cal-weekday { height: 1.4rem; font-size: .625rem; }
.oc-dp-grid .oc-cal-day { aspect-ratio: auto; min-height: 0; height: 1.9rem; font-size: .75rem; }
.oc-dp-foot { margin-top: .2rem; }
.oc-dp-foot .btn { min-height: 0; height: 1.9rem; font-size: .75rem; padding: 0 .5rem; }
/* Empty lead-in cells before the 1st (app.js) — space only, never a hover or a target. */
.oc-dp-grid .oc-cal-day--blank, .oc-dp-grid .oc-cal-day--blank:hover { background: transparent; box-shadow: none; cursor: default; }

/* Shared-prefill banner (Builder): desktop shows the full section lists; the short headline and the
   per-agreement counts are phone-only (swapped below). */
.oc-prefill-short, .oc-prefill-count { display: none; }
@media (max-width: 60rem) {
    /* The "Waiting on…" guidance is not shown on a phone, by request — Send in the header already says
       what the next step is. */
    [data-testid="proposal__workflow-guidance"] { display: none; }

    /* Shared-prefill banner: "Pre-filled from shared sections" plus one line per agreement with a count,
       not every section name. */
    [data-testid="builder__shared-prefill"] { padding: .6rem .75rem; font-size: .85rem; gap: .55rem; }
    [data-testid="builder__shared-prefill"] .oc-prefill-long,
    [data-testid="builder__shared-prefill"] .oc-prefill-names { display: none; }
    [data-testid="builder__shared-prefill"] .oc-prefill-short,
    [data-testid="builder__shared-prefill"] .oc-prefill-count { display: inline; }
    [data-testid="builder__shared-prefill"] .oc-mt-xs { margin-top: .15rem; }

    /* A stored signature's Remove: trash icon only, pushed to the row's end. */
    [data-oc-pad-stored-remove] .oc-btn-label,
    [data-oc-pad-remove] .oc-btn-label { display: none; }
    [data-oc-pad-stored-remove],
    [data-oc-pad-remove] { margin-left: auto; flex: 0 0 auto; padding-inline: .6rem; }
}

/* ---- Agreements list (Contracts/Index.cshtml), mobile ----------------------------------------------
   Four stat tiles used to stack full-width, one per row, each burning a whole screen's height before the
   toolbar or the table itself ever appeared. Scoped to .oc-agreements-* (set on this page only, alongside
   the shared .oc-stats-grid/.oc-toolbar classes) rather than those shared classes themselves — Clients,
   Invoices, Projects, Dashboard and Documents also render .oc-stats-grid and keep the theme's own mobile
   behaviour untouched. */
@media (max-width: 60rem) {
    .oc-agreements-stats {
        display: flex; flex-wrap: nowrap; gap: .5rem;
        overflow-x: auto; -webkit-overflow-scrolling: touch; padding-bottom: .15rem;
    }
    .oc-agreements-stats > .oc-stat { flex: 0 0 8rem; padding: .6rem .7rem; gap: .5rem; }
    .oc-agreements-stats .oc-stat-icon { width: 1.9rem; height: 1.9rem; border-radius: .6rem; }
    .oc-agreements-stats .oc-stat-icon i { font-size: .8rem; }
    .oc-agreements-stats .oc-stat-icon svg { width: .8rem; height: .8rem; }
    .oc-agreements-stats .oc-stat-value { font-size: 1rem; margin-top: 0; }
    .oc-agreements-stats .oc-stat-label { font-size: .68rem; line-height: 1.2; }
    /* The per-currency breakdown under "Value on this page" — secondary at this size; the headline
       figure is still the one number the tile promises. */
    .oc-agreements-stats .oc-stat-sub { display: none; }

    /* The search box already narrows the one thing staff scan for (title/client); the status dropdown
       was a whole row of its own above a table that was already three rows below the fold. Desktop keeps
       it — there's room there. */
    .oc-agreements-toolbar select[name="status"] { display: none; }

    /* Pager only, by request: the count line and rows-per-page came off, the page links + prev/next
       arrows stay — centred, as the one control in the bar. */
    .oc-agreements-pagination .oc-pagination-count,
    .oc-agreements-pagination .oc-pagination-size { display: none; }
    .oc-agreements-pagination .oc-pagination { justify-content: center; }
    .oc-agreements-pagination .oc-pagination-pages { margin-left: 0; gap: .35rem; }
    .oc-agreements-pagination .oc-page-num,
    .oc-agreements-pagination .oc-page-arrow { min-width: 2.25rem; height: 2.25rem; font-size: .85rem; }
}

/* ---- Builder header, mobile: Preview/Send/testing tools ---------------------------------------------
   Preview and Send used to be two equal-weight bars (.oc-page-head .oc-head-actions > .btn gets
   flex:1 1 auto for touch-target width — right for a two-button row like Edit/Delete elsewhere, wrong
   once one of the two is a lesser action). Preview drops to icon-only, matching the row's own Edit/
   Delete icon buttons elsewhere in the app; Send stops stretching to match it. Scoped with :has() to the
   row that actually carries Send, so every other .oc-page-head (Clients, Invoices, Contracts…) keeps its
   own stretch-to-fill behaviour untouched. */
@media (max-width: 60rem) {
    /* Two right-aligned lines, by request:
         line 1 — View · PDF · Resend email (icons) · Send / Send update (the one full button, far right);
         line 2 — the two testing buttons.
       Reordered with `order` only — the markup keeps its desktop order. The ::before is a zero-height,
       full-width flex item that forces the line break between the two groups. Scoped by Preview, which
       every Builder header has. */
    .oc-page-head .oc-head-actions:has([data-testid="proposal__preview"]) {
        justify-content: flex-end; gap: .3rem .5rem; /* row gap counts twice around the break */
    }
    .oc-page-head .oc-head-actions:has([data-testid="proposal__preview"])::before {
        content: ""; order: 5; flex: 0 0 100%; height: 0;
    }
    .oc-page-head .oc-head-actions:has([data-testid="proposal__preview"]) > .btn,
    .oc-page-head .oc-head-actions:has([data-testid="proposal__preview"]) > a.btn,
    .oc-page-head .oc-head-actions:has([data-testid="proposal__preview"]) > form { flex: 0 0 auto; }
    .oc-page-head .oc-head-actions > [data-testid="proposal__preview"] { order: 1; }
    .oc-page-head .oc-head-actions > [data-testid="proposal__pdf"] { order: 2; }
    .oc-page-head .oc-head-actions > form:has([data-testid="proposal__resend-email"]) { order: 3; }
    .oc-page-head .oc-head-actions > [data-testid="proposal__send"] { order: 4; }
    .oc-page-head .oc-head-actions [data-oc-confirm="#dummy_data_modal"] { order: 6; }
    .oc-page-head .oc-head-actions [data-oc-confirm="#delete_dummy_data_modal"] { order: 7; }
    /* Agreement Builder, by request — line 1: View · Save Agreement · Send (far right); line 2: the two
       testing buttons, Delete far right. Once sent: View · Download · Save Agreement (far right), and the
       testing buttons are not rendered at all (Builder.cshtml). */
    .oc-page-head .oc-head-actions > [data-testid="agreement__save"] { order: 3; height: 2.5rem; min-height: 0; }

    /* Line 1: one height; View, PDF and Resend are square icon buttons (their names stay for screen
       readers); Send keeps its label. */
    .oc-page-head .oc-head-actions > [data-testid="proposal__preview"],
    .oc-page-head .oc-head-actions > [data-testid="proposal__pdf"],
    .oc-page-head .oc-head-actions > form > [data-testid="proposal__resend-email"],
    .oc-page-head .oc-head-actions > [data-testid="proposal__send"] {
        height: 2.5rem; min-height: 0; justify-content: center;
    }
    .oc-page-head .oc-head-actions > [data-testid="proposal__preview"],
    .oc-page-head .oc-head-actions > [data-testid="proposal__pdf"],
    .oc-page-head .oc-head-actions > form > [data-testid="proposal__resend-email"] {
        width: 2.5rem; padding: 0; gap: 0;
    }
    /* Feedback, by request: icon only on a phone, the same square as View / PDF / Resend. */
    .oc-page-head .oc-head-actions > .oc-feedback-btn {
        height: 2.5rem; min-height: 0; width: 2.5rem; padding: 0; gap: 0; justify-content: center; flex: 0 0 auto;
    }
    .oc-feedback-btn .oc-btn-label,
    [data-testid="proposal__preview"] .oc-btn-label,
    [data-testid="proposal__pdf"] .oc-btn-label,
    [data-testid="proposal__resend-email"] .oc-btn-label { display: none; }
    /* Duplicate (an Accepted/Declined proposal) joins the square icon buttons so Send update stays on the
       same line, right of PDF. */
    .oc-page-head .oc-head-actions > [data-testid="proposal__duplicate"] { order: 0; height: 2.5rem; min-height: 0; width: 2.5rem; padding: 0; gap: 0; justify-content: center; }
    [data-testid="proposal__duplicate"] .oc-btn-label { display: none; }
    [data-testid="proposal__send"] { font-size: .8125rem; padding: 0 .75rem; }
    /* A sent agreement has no Send / Send update (no versioning), so Resend email is line 1's one full
       button at the far right, label and all. */
    .oc-page-head .oc-head-actions:not(:has([data-testid="proposal__send"])) > form > [data-testid="proposal__resend-email"] {
        width: auto; padding: 0 1rem; gap: .4rem; font-size: .8125rem;
    }
    .oc-page-head .oc-head-actions:not(:has([data-testid="proposal__send"])) [data-testid="proposal__resend-email"] .oc-btn-label { display: inline; }

    /* Dummy Data for Testing: icon-only, matching the Delete-dummy-data icon button beside it — the full
       label was the widest thing in either row. */
    [data-oc-confirm="#dummy_data_modal"] .oc-btn-label { display: none; }

    /* Previous/Next agreement (.oc-editor-batch-bar): "Previous agreement: Automation Pilot Agreement"
       wrapped to two lines at any font small enough to call "small" — single-line with an ellipsis reads
       as a compact pill instead, same information (direction + which template) at a glance. */
    .oc-editor-batch-bar .btn {
        min-height: 0; font-size: .78rem; padding: .5rem .75rem;
        overflow: hidden;
    }
    .oc-editor-batch-bar .btn .oc-btn-label {
        min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    }

    /* Previous/Next SECTION (.oc-sectionstep-nav): same pill size and colour as the agreement-level bar
       above made the two look like the same control — one moves between documents, the other between
       sections of the one open now. Smaller than the agreement bar (a lower-stakes, far more frequent
       tap) and relabelled ("Previous section"/"Next section" in the markup) rather than relying on size
       alone to tell them apart. */
    .oc-sectionstep-nav .btn { min-height: 2.2rem; font-size: .78rem; padding: .4rem .65rem; }
}

/* ==== One hover language — Admin, desktop =============================================================
   By request: 84 hover rules had grown up page by page — tints in hard-coded greys and indigos, borders in
   three shades, shadows, and cards that jumped 1–3px. Every Admin hover is now one of five kinds, all in
   the theme's own tokens, and nothing moves:
     1. cards / tiles      — brand border + soft brand ring
     2. rows / list items  — the muted surface tint
     3. text links         — darker brand + underline
     4. icon / ghost btns  — the muted surface tint, ink text
     5. destructive icons  — a light red tint
   Placed last and scoped to html[data-theme-scope="admin"], so it wins over the older per-page rules
   without editing each one, and never reaches the Client Portal. (hover: hover) keeps touch screens out:
   a phone has no hover, only "sticky" tap states. Current / active / selected items keep their own look. */
@media (hover: hover) and (min-width: 60.0625rem) {
    html[data-theme-scope="admin"] {
        --oc-hover-tint: var(--surface-muted);
        --oc-hover-border: var(--brand-500);
        --oc-hover-ring: 0 0 0 3px color-mix(in srgb, var(--brand-500) 12%, transparent);
        --oc-hover-danger: color-mix(in srgb, var(--danger) 10%, transparent);
    }

    /* 1. Cards and tiles */
    html[data-theme-scope="admin"] :is(.oc-template-card, .oc-kpi, .oc-kcard, .oc-ktile, .oc-tile, .oc-start,
        .oc-method-card, .oc-choice, .oc-file-picker, .oc-proposal-card.is-closed):hover {
        border-color: var(--oc-hover-border); box-shadow: var(--oc-hover-ring); transform: none;
    }
    html[data-theme-scope="admin"] .oc-template-card:hover .oc-thumb { border-color: var(--brand-100); }

    /* 2. Rows and list items */
    html[data-theme-scope="admin"] :is(.oc-section-item:not(.is-active), .oc-sheet-section:not(.is-current),
        .oc-nav-link:not([aria-current="page"]), .oc-queue-item, .oc-attention-row, .oc-create-list a,
        .oc-disclosure > summary, .oc-more-summary, .oc-kv > div, .oc-comment-thread[data-oc-jump],
        .oc-section-add-row, .oc-tab-link:not([aria-current]), .oc-sort, .oc-chip,
        .oc-page-num:not(.is-current), .oc-page-arrow):hover {
        background: var(--oc-hover-tint); transform: none;
    }
    html[data-theme-scope="admin"] .oc-table tbody tr:hover { transform: none; }
    html[data-theme-scope="admin"] .oc-table tbody tr:hover > td { background: var(--oc-hover-tint); }
    html[data-theme-scope="admin"] .oc-section-item:hover .oc-section-state { transform: none; }

    /* 3. Text links */
    html[data-theme-scope="admin"] :is(.oc-link, .oc-queue-title, .oc-tile-name, a.oc-tile-sub,
        .oc-kv-linked dt a, a.oc-comment-thread-title):hover {
        color: var(--brand-700); text-decoration: underline; text-underline-offset: 2px;
    }

    /* 4. Icon and ghost buttons (the destructive ones are overridden just below) */
    html[data-theme-scope="admin"] :is(.oc-icon-btn, .btn-ghost, .oc-drag-handle, .oc-sidebar-collapse,
        .oc-flash-close, .oc-alert > .oc-dismiss, .oc-tile-action, .oc-comment-edit-toggle):hover:not(:disabled) {
        background: var(--oc-hover-tint); color: var(--ink-900); box-shadow: none;
    }

    /* 5. Destructive icons */
    html[data-theme-scope="admin"] :is(.oc-icon-danger, .oc-tile-action.is-danger, .oc-row-remove,
        .oc-section-toggle.is-on):hover:not(:disabled) {
        background: var(--oc-hover-danger); color: var(--danger); box-shadow: none;
    }
}

/* ===================================================================================================
   Invoice builder (_InvoiceBuilder.cshtml). Form on the left, a sticky Invoice summary on the right from
   64rem; below that the summary follows the form, so on a phone it sits under the last section with the
   Save button — never floating over fields being typed into. Theme tokens only, so dark mode follows.
   =================================================================================================== */
.oc-invoice-layout { border: 0; margin: 0; padding: 0; min-width: 0; display: grid; grid-template-columns: minmax(0, 1fr); gap: 1rem; }
@media (min-width: 64rem) {
    .oc-invoice-layout { grid-template-columns: minmax(0, 1fr) 20rem; align-items: start; }
    .oc-invoice-summary { position: sticky; top: 1rem; }
}
/* ONE card for the whole form (by request): each part is a section of the same card, split by a rule
   rather than wrapped in a card of its own. */
.oc-invoice-main { display: flex; flex-direction: column; min-width: 0; }
.oc-invoice-part { padding: 1.25rem 1.5rem; }
.oc-invoice-part + .oc-invoice-part { border-top: 1px solid var(--line); }
.oc-invoice-part-title { margin: 0 0 1rem; display: flex; align-items: center; gap: .375rem;
    font-size: 1rem; font-weight: 700; color: var(--ink-900); }
/* The Client & invoice details part wraps its fields in .oc-form, whose own 1.5rem padding sat them 24px
   further in (both sides) than the headings and than every part below (Pricing, Payment methods, notes).
   The part already has the padding — drop the second one so one left and right edge runs down the form. */
.oc-invoice-part .oc-form { padding: 0; }
/* The ⓘ beside a part heading (Pricing): smaller than the shared page-header one. */
.oc-invoice-part-title .oc-info-dot { width: 1.125rem; height: 1.125rem; }
.oc-invoice-part-title .oc-info-dot > i { width: .75rem; height: .75rem; font-size: .75rem; line-height: 1; }
@media (max-width: 40rem) { .oc-invoice-part { padding: 1rem; } }
.oc-invoice-readonly { padding: .5rem 0; font-weight: 600; color: var(--ink-900); }
.oc-invoice-client-fields { display: flex; flex-direction: column; gap: 1rem; }
.oc-invoice-billto { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; padding: .625rem .75rem;
    border: 1px solid var(--line); border-radius: 10px; background: var(--line-2); font-size: .875rem; color: var(--ink-700); }
/* Bill-to summary. */
.oc-invoice-client-fields { margin-bottom: 1.25rem; }
.oc-invoice-billto { padding: .3rem .625rem; font-size: .8125rem; border-radius: 8px; }
.oc-invoice-billto-label { display: inline-flex; align-items: center; gap: .375rem; font-weight: 600; color: var(--ink-500); }
/* The row spans the full width (so "+ Project" can reach the far right of the card, by request), but the
   banner itself still hugs only its own content — it is a flex item inside this row, not stretched, so its
   visible box stays small; the row around it is invisible (no border/background of its own). */
.oc-invoice-billto-row { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.oc-invoice-billto-row .oc-invoice-add { margin-left: auto; }
[data-oc-invoice-link][hidden] { display: none; }

/* Payment due · Invoice due date on one row, Payment terms under them — the three read as one decision. */
.oc-invoice-due-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; }
.oc-invoice-due-grid .field { margin: 0; }
.oc-invoice-due-terms { grid-column: 1 / -1; }
@media (max-width: 40rem) { .oc-invoice-due-grid { grid-template-columns: 1fr; } }

/* Optional links: the + buttons sit beside the heading; a field appears only once asked for. */
.oc-invoice-links { display: flex; flex-direction: column; gap: .75rem; padding-top: 1rem; border-top: 1px dashed var(--line); }
.oc-invoice-links-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: .5rem; }
.oc-invoice-links-head .field-label { margin: 0; }
.oc-invoice-links-add { display: flex; gap: .5rem; }
.oc-invoice-links [data-oc-invoice-link][hidden] { display: none; }
.oc-invoice-link-label { display: flex; align-items: center; justify-content: space-between; gap: .5rem; }
.oc-invoice-link-label .field-label { margin-bottom: 0; }

/* Every "+ …" button on the invoice builder (Proposal, Agreement, Add billing address, Discount & tax,
   Add pricing): one small size, and the + centred on the text. The icon's own line box sat it high of
   the label; a fixed-size, line-height-1 icon in an inline-flex row lines the two up. */
.btn.oc-invoice-add { display: inline-flex; align-items: center; justify-content: center; gap: .375rem;
    height: 1.875rem; min-height: 0; padding: 0 .625rem; font-size: .8125rem; line-height: 1; }
:is(.btn.oc-invoice-add, .oc-pricing-add-item) > :first-child { flex: none; width: .75rem; height: .75rem;
    font-size: .75rem; line-height: 1; display: inline-grid; place-items: center; }

/* A part heading with an action on its far right (Payment methods' "+ Discount & tax", Discount & tax's ×). */
.oc-invoice-part-head { display: flex; align-items: center; justify-content: space-between; gap: .75rem; margin-bottom: 1rem; }
.oc-invoice-part-head .oc-invoice-part-title { margin: 0; }
.oc-invoice-part-head .btn[hidden] { display: none; }
.oc-invoice-part[hidden] { display: none; }

/* Tax (inside Pricing): one small box — heading, Tax name, Tax rate. */
.oc-invoice-tax-box { margin-top: .75rem; padding: .75rem; border: 1px solid var(--line); border-radius: 10px; }
.oc-invoice-tax-box .oc-invoice-part-head { margin-bottom: .5rem; }
.oc-invoice-tax-box .oc-invoice-part-head .field-label { margin: 0; }
.oc-invoice-tax-box .oc-form-grid { margin-bottom: 0; }
.oc-invoice-tax-box .field { margin: 0; }
.oc-invoice-tax-box[hidden] { display: none; }

/* Pricing: a flat list, not boxes. Each group is a header row — type · its subtotal · delete — over a
   plain table of its items (column labels once, then aligned inputs, "+ Add item" under the first
   column). Groups are split by a hairline and air, nothing else. Every item carries all the fields any
   type uses; the group's data-basis decides which show (invoice-builder.js disables the rest so they
   never post). */
.oc-pricing-groups { display: flex; flex-direction: column; }
.oc-pricing-group { padding: .875rem 0; }
.oc-pricing-group:first-child { padding-top: 0; }
.oc-pricing-group + .oc-pricing-group { border-top: 1px solid var(--line-2); }
.oc-pricing-group-head { display: flex; align-items: flex-end; gap: .5rem; min-height: 2.25rem; }
/* The label reuses .field-label as-is (same size, weight and label-to-input gap as every other field in the
   form, e.g. "Currency" or "Payment due" above it) — a custom smaller label here was what actually read as
   misaligned, not the position. */
.oc-pricing-type-field { margin: 0; }
/* Width only — height, padding, weight and text position stay the theme's .field-select, like every
   other dropdown on the form (a fixed height with zeroed padding sat the text high, and bold read heavy). */
.oc-pricing-type { width: 13rem; max-width: 100%; font-weight: 400; }
.oc-pricing-subtotal { margin-left: auto; font-size: .875rem; font-weight: 600; font-variant-numeric: tabular-nums;
    color: var(--ink-900); white-space: nowrap; }
.oc-pricing-subtotal[hidden] { display: none; }
/* The group's one delete: red (by request), no background, a plain hover tint. Pushed right when there
   is no subtotal yet (a group still on "Select"). */
.oc-pricing-remove-group > i { width: .75rem; height: .75rem; font-size: .75rem; line-height: 1; }
.oc-pricing-remove-group { display: inline-grid; place-items: center; width: 1.5rem; height: 1.5rem; padding: 0;
    border: 0; border-radius: 6px; background: none; color: var(--danger); cursor: pointer; }
.oc-pricing-subtotal[hidden] + .oc-pricing-remove-group { margin-left: auto; }
.oc-pricing-remove-group:not(:disabled):hover, .oc-pricing-remove-group:focus-visible { background: var(--oc-hover-danger, rgb(220 38 38 / .1)); }
.oc-pricing-remove-group:disabled { opacity: .35; cursor: not-allowed; }

/* An item's own small delete, on every pricing type (by request). Grey, only red on hover, deliberately
   quieter than the group's own delete. Hidden only while the group is still on "Select". */
.oc-pricing-item-remove { display: none; }
.oc-pricing-group:not([data-basis=""]) .oc-pricing-item-remove {
    display: inline-grid; place-items: center; align-self: center; width: 1.375rem; height: 1.375rem; padding: 0;
    border: 0; border-radius: 6px; background: none; color: var(--ink-400, var(--ink-500)); cursor: pointer; }
.oc-pricing-item-remove:hover, .oc-pricing-item-remove:focus-visible { color: var(--danger); }
.oc-pricing-item-remove > i { width: .6875rem; height: .6875rem; font-size: .6875rem; line-height: 1; }
.oc-pricing-body { margin-top: .625rem; }
.oc-pricing-body[hidden] { display: none; }

/* One grid per type so every column lines up item to item; the delete column is gone (group delete only),
   the right edge is kept clear to line up with the subtotal above. */
.oc-pricing-cols, .oc-pricing-item { display: grid; gap: .5rem .75rem; align-items: start;
    grid-template-columns: minmax(0, 1fr) 9rem 1.5rem; }                              /* Fixed price (default) */
.oc-pricing-group[data-basis="Milestone"] :is(.oc-pricing-cols, .oc-pricing-item) {
    grid-template-columns: 8rem minmax(0, 1fr) 9rem 1.5rem; }
.oc-pricing-group[data-basis="Monthly"] :is(.oc-pricing-cols, .oc-pricing-item) {
    grid-template-columns: minmax(0, 1fr) 9rem 9rem 9rem 1.5rem; }
.oc-pricing-group:is([data-basis="Hourly"], [data-basis="Standard"]) :is(.oc-pricing-cols, .oc-pricing-item) {
    grid-template-columns: minmax(0, 1fr) 5.5rem 7.5rem 7rem 1.5rem; }
/* Only the columns this type uses take a cell. */
.oc-pricing-group:not([data-basis="Monthly"]) :is([data-col="from"], [data-col="to"]),
.oc-pricing-group:not([data-basis="Hourly"]):not([data-basis="Standard"]) :is([data-col="qty"], [data-col="total"]),
.oc-pricing-group:not([data-basis="Milestone"]) [data-col="num"] { display: none; }
/* Project field with nothing to pick: the hint and the New project button on one line. */
.oc-invoice-no-project { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin-top: .5rem; }
.oc-invoice-no-project .field-hint { margin: 0; }
/* A milestone's number: "M" then the number box ("M 3"), the M fixed so only the number is typed. */
.oc-milestone-no { display: flex; align-items: center; gap: .375rem; }
.oc-milestone-prefix { font-weight: 600; color: var(--ink-700); }
.oc-milestone-no .field-input { min-width: 0; }
/* "Continue existing milestones?" — an info alert (flex in the theme), so [hidden] needs saying again. */
.oc-milestone-decision[hidden] { display: none; }
.oc-milestone-decision-body { flex: 1 1 auto; min-width: 0; }
.oc-milestone-decision-list { display: flex; flex-direction: column; gap: .375rem; margin: .625rem 0 0; padding: 0; border: 0; }
.oc-milestone-decision-option { display: flex; align-items: center; gap: .5rem; cursor: pointer; }
.oc-milestone-decision-actions { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: .75rem; }
.oc-milestone-decision .field-error:empty { display: none; }
/* The project's last milestone billed, under a Milestone group's items. */
.oc-milestone-last { margin: .5rem 0 0; }
.oc-milestone-last:empty, .oc-pricing-group:not([data-basis="Milestone"]) .oc-milestone-last { display: none; }
.oc-pricing-cols { margin-bottom: .375rem; font-size: .6875rem; font-weight: 600; text-transform: uppercase;
    letter-spacing: .05em; color: var(--ink-500); }
.oc-pricing-items { display: flex; flex-direction: column; gap: .5rem; }
.oc-pricing-item .field { margin: 0; min-width: 0; }
.oc-pricing-item .field-input { width: 100%; }
.oc-pricing-item .field-error:empty { display: none; }
.oc-pricing-cell-label { display: none; }
.oc-pricing-total { text-align: right; }
.oc-pricing-total output { display: block; line-height: 2.5rem; font-size: .875rem; font-weight: 600;
    font-variant-numeric: tabular-nums; color: var(--ink-700); white-space: nowrap; }
/* "+ Add item": a quiet text action under the first column, not another button. */
.oc-pricing-add-item { display: inline-flex; align-items: center; gap: .375rem; margin-top: .5rem; padding: .25rem 0;
    border: 0; background: none; font-size: .8125rem; font-weight: 600; color: var(--brand-600, var(--ink-700)); cursor: pointer; }
.oc-pricing-add-item:hover { text-decoration: underline; }
/* "+ Tax" and "+ Add pricing" together on the far right, like the other "+" actions. */
.oc-pricing-actions { display: flex; justify-content: flex-end; gap: .5rem; margin-top: .75rem; }
.oc-pricing-actions .btn[hidden] { display: none; }
@media (max-width: 40rem) {
    .oc-pricing-cols { display: none; }
    .oc-pricing-item, .oc-pricing-group[data-basis] .oc-pricing-item { grid-template-columns: 1fr 1fr;
        padding-bottom: .5rem; border-bottom: 1px dashed var(--line-2); }
    .oc-pricing-item [data-col="desc"] { grid-column: 1 / -1; }
    .oc-pricing-cell-label { display: block; margin-bottom: .25rem; font-size: .75rem; font-weight: 600; color: var(--ink-500); }
    .oc-pricing-total { text-align: left; }
    .oc-pricing-type { flex: 0 1 auto; }
    .oc-pricing-item-remove { justify-self: end; }
    /* Milestone, by request — line 1: Milestone · Amount (and the small delete); line 2: Description. */
    .oc-pricing-group[data-basis="Milestone"] .oc-pricing-item { grid-template-columns: 1fr 1fr auto; }
    .oc-pricing-group[data-basis="Milestone"] .oc-pricing-item [data-col="num"]    { order: 1; }
    .oc-pricing-group[data-basis="Milestone"] .oc-pricing-item [data-col="rate"]   { order: 2; }
    .oc-pricing-group[data-basis="Milestone"] .oc-pricing-item [data-col="remove"] { order: 3; align-self: end; margin-bottom: .55rem; }
    .oc-pricing-group[data-basis="Milestone"] .oc-pricing-item [data-col="desc"]   { order: 4; }
}

/* Keyboard highlight in a searchable dropdown (oc-dropdown-keys.js, ↑ / ↓): the same tint as hover, so
   the option Enter will pick is always visible. octacer-searchable.js itself is octa-core-owned. */
.oc-searchable-option.is-active { background: var(--surface-muted, #f1f5f9); }
.oc-invoice-method:focus-within { background: var(--line-2); }

/* Payment methods: a multi-select dropdown — a <details> whose face lists what is ticked. Methods with no
   details in Settings are shown but cannot be ticked. */
.oc-invoice-methods-dd { position: relative; max-width: 28rem; }
.oc-invoice-methods-summary { display: flex; align-items: center; cursor: pointer; list-style: none; }
.oc-invoice-methods-summary::-webkit-details-marker { display: none; }
.oc-invoice-methods-summary span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.oc-invoice-methods-menu { position: absolute; z-index: 30; left: 0; right: 0; margin-top: .25rem; padding: .375rem;
    display: flex; flex-direction: column; background: var(--surface, #fff); border: 1px solid var(--line);
    border-radius: 10px; box-shadow: 0 8px 24px rgb(15 23 42 / .12); }
.oc-invoice-method { display: flex; align-items: center; gap: .5rem; padding: .5rem .625rem; border-radius: 8px; cursor: pointer; }
.oc-invoice-method:hover { background: var(--line-2); }
.oc-invoice-method.is-off { cursor: not-allowed; opacity: .6; }

/* Summary */
.oc-invoice-totals { margin: 0; display: flex; flex-direction: column; gap: .375rem; font-variant-numeric: tabular-nums; }
.oc-invoice-totals > div { display: flex; justify-content: space-between; gap: .75rem; font-size: .875rem; }
.oc-invoice-totals > div[hidden] { display: none; }
.oc-invoice-totals dt { color: var(--ink-500); }
.oc-invoice-totals dd { margin: 0; color: var(--ink-900); white-space: nowrap; }
.oc-invoice-totals .oc-invoice-total { padding-top: .5rem; border-top: 1px solid var(--line); font-weight: 700; }
.oc-invoice-totals .oc-invoice-balance { padding: .5rem .625rem; border-radius: 8px; background: var(--line-2); font-weight: 700; font-size: .9375rem; }
.oc-invoice-actions { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: 1rem; }
/* The invoice page's one message line (no toasts there, by request): why Save / Send are locked, or what
   failed. */
.oc-invoice-msg { margin: .5rem 0 0; font-size: .75rem; line-height: 1.4; color: var(--warning, #b45309); }
.oc-invoice-msg[data-tone="error"] { color: var(--danger); }
.oc-invoice-msg[hidden] { display: none; }
/* Autosave's state line: quiet grey, green once saved, amber while something still blocks it, red on failure. */
.oc-invoice-autosave { margin: .5rem 0 0; font-size: .75rem; line-height: 1.4; color: var(--ink-500); }
.oc-invoice-autosave:empty { display: none; }
.oc-invoice-autosave[data-state="saved"] { color: var(--success, #15803d); }
.oc-invoice-autosave[data-state="pending"] { color: var(--warning, #b45309); }
.oc-invoice-autosave[data-state="error"] { color: var(--danger); }
.oc-invoice-actions .btn { flex: 1 1 auto; justify-content: center; }
/* Invoice builder on a phone (by request, like the quotation form's — its own copy, quotations and invoices share no
   styles): the buttons pinned to the bottom edge above the tab bar, Save and Send wide, Preview and Download PDF
   icon-only; the notes under the summary hidden. Why Save / Send are locked (.oc-invoice-msg) floats just above the
   bar, since the summary card it belongs to may be far up the page. Desktop is untouched. */
@media (max-width: 60rem) {
    #invoice_builder_form .oc-inv-sumnote { display: none; }
    #invoice_builder_form .oc-invoice-actions {
        position: fixed; left: 0; right: 0; bottom: var(--oc-bottom-chrome, 0px); z-index: 30; margin: 0;
        display: flex; flex-wrap: nowrap; gap: .5rem;
        padding: .75rem 1rem max(.75rem, env(safe-area-inset-bottom));
        background: linear-gradient(to top, var(--bg) 72%, transparent);
    }
    #invoice_builder_form .oc-invoice-actions .btn { min-height: 2.75rem; }
    #invoice_builder_form .oc-invoice-actions :is([data-oc-invoice-preview], [data-oc-invoice-download]) { flex: 0 0 auto; padding-inline: .75rem; }
    #invoice_builder_form .oc-invoice-actions :is([data-oc-invoice-preview], [data-oc-invoice-download]) .oc-btn-label { display: none; }
    #invoice_builder_form .oc-invoice-actions :is([data-oc-invoice-save], [data-oc-invoice-send]) { flex: 1 1 0; min-width: 0; }
    #invoice_builder_form .oc-invoice-msg:not([hidden]) {
        position: fixed; left: 1rem; right: 1rem; bottom: calc(var(--oc-bottom-chrome, 0px) + 4.25rem); z-index: 30; margin: 0;
        padding: .5rem .75rem; border: 1px solid var(--line); border-radius: 10px; background: var(--surface); box-shadow: var(--shadow-lg);
    }
    #invoice_builder_form { padding-bottom: 5rem; }
}

/* Preview dialog: the invoice's own print page in an iframe — wide, and scrolling inside the card. */
.oc-modal-card-invoice { max-width: 60rem; }
.oc-invoice-preview-body { padding-bottom: 0; }
.oc-invoice-preview-frame { display: block; width: 100%; height: min(72vh, 58rem); border: 1px solid var(--line); border-radius: 8px;
    background: #eef0f3; }

/* A client's invoice row / card opens the invoice itself in a new tab (data-oc-jump-new-tab, app.js). */
.oc-row-link[data-oc-jump] { cursor: pointer; }
tr.oc-row-link[data-oc-jump]:hover td { background: var(--line-2); }

/* ---- Client portal: Pay invoice (Areas/Portal/Views/Invoices/Pay.cshtml) ---------------------------- */
.oc-pay-summary { display: flex; flex-wrap: wrap; align-items: flex-start; justify-content: space-between; gap: 1.25rem; }
.oc-pay-due { display: flex; flex-direction: column; gap: .35rem; }
.oc-pay-due-label { font-size: .75rem; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--ink-500); }
.oc-pay-due-amount { font-size: 2rem; font-weight: 700; line-height: 1.1; color: var(--ink-900); font-variant-numeric: tabular-nums; }
.oc-pay-due .badge { align-self: flex-start; }
.oc-pay-facts { margin: 0; display: grid; grid-template-columns: auto auto; column-gap: 1.25rem; row-gap: .35rem; font-size: .875rem; }
.oc-pay-facts > div { display: contents; }
.oc-pay-facts dt { color: var(--ink-500); }
.oc-pay-facts dd { margin: 0; font-weight: 600; color: var(--ink-900); font-variant-numeric: tabular-nums; }
.oc-pay-doc-actions { display: flex; flex-wrap: wrap; gap: .5rem; align-self: center; }
/* "How to pay": a "Select payment method" dropdown (by request) — one method's own details showing at a
   time (pay-method-picker.js), instead of a boxed card per method or a list of every one at once. A
   pill-tab version tried here first read as decoration rather than a control; a plain <select> (.field/
   .field-select, the same as every other dropdown in the app) is unambiguous. Detail lines reuse
   .oc-pay-facts' label/value grid (the "Amount due" card just above) rather than a new one. */
.oc-pay-method-picker { display: flex; flex-direction: column; gap: 1rem; }
.oc-pay-method-select-field { max-width: 18rem; }
.oc-pay-method-facts dd { overflow-wrap: anywhere; }
.oc-pay-method-note { grid-column: 1 / -1; }
.oc-pay-method-note dd { font-weight: 400; color: var(--ink-600); }
/* The legacy fallback (an older invoice's own typed instructions, no methods picked) — plain text, open,
   not boxed. */
.oc-pay-method-note-text { white-space: pre-line; overflow-wrap: anywhere; }
@media (max-width: 40rem) {
    .oc-pay-summary { flex-direction: column; }
    .oc-pay-due-amount { font-size: 1.6rem; }
}

/* Settings → Invoice → Payment methods (_PaymentMethodsEditor.cshtml, Admin only). */
.oc-payment-methods-chips { display: flex; flex-wrap: wrap; gap: .5rem; }
.oc-payment-method-picker-field { max-width: 22rem; }
.oc-payment-fields { margin-top: .75rem; display: flex; flex-direction: column; gap: .75rem; }
.oc-payment-fields-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
@media (max-width: 40rem) {
    .oc-payment-fields-grid { grid-template-columns: 1fr; }
}



/* ==== Invoices list, grouped Client → Project → invoices (Areas/Admin/Views/Invoices/Index) ==============
   One container per client (a native <details>, collapsible with no script), a quiet sub-heading per
   project, compact rows under it. Client and project are the groups, so the rows do not repeat them. */
.oc-inv-tabs { overflow-x: auto; flex-wrap: nowrap; }
.oc-inv-toolbar { flex-wrap: wrap; }
.oc-inv-toolbar .field-select, .oc-inv-toolbar .oc-searchable { width: auto; min-width: 10rem; max-width: 16rem; }
.oc-inv-groups { display: flex; flex-direction: column; gap: 1rem; }
.oc-inv-client { padding: 0; overflow: hidden; }
.oc-inv-client-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: .375rem .75rem; padding: .875rem 1.25rem;
    cursor: pointer; list-style: none; border-bottom: 1px solid var(--line); }
.oc-inv-client-head::-webkit-details-marker { display: none; }
.oc-inv-client:not([open]) .oc-inv-client-head { border-bottom: 0; }
.oc-inv-client-head:hover { background: var(--surface-muted, transparent); }
.oc-inv-chevron { display: inline-grid; place-items: center; align-self: center; color: var(--ink-500); transition: transform .15s; }
.oc-inv-client:not([open]) .oc-inv-chevron { transform: rotate(-90deg); }
.oc-inv-client-name { font-size: 1rem; font-weight: 700; color: var(--ink-900); }
.oc-inv-client-meta { font-size: .8125rem; color: var(--ink-500); font-variant-numeric: tabular-nums; }
/* The project's own sub-heading row: a label across the table, a hairline above, nothing heavier. */
.oc-table-inv-grouped .oc-inv-project-row th { padding: .625rem 1.25rem .375rem; text-align: left; font-size: .75rem;
    font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--ink-700);
    background: var(--surface-muted, transparent); border-top: 1px solid var(--line-2); }
.oc-inv-project-name { display: inline-flex; align-items: center; gap: .375rem; }
.oc-inv-project-name.is-none { color: var(--ink-500); }
.oc-inv-project-code { margin-left: .5rem; font-weight: 500; color: var(--ink-500); text-transform: none; letter-spacing: 0; }
.oc-table-inv-grouped { table-layout: fixed; }
/* No Milestone name column (by request): its 11% went 30% to Invoice, 40% to Billing, 15% each to Created and Due. */
.oc-table-inv-grouped .oc-col-number    { width: 13.3%; }
.oc-table-inv-grouped .oc-col-title     { width: 17.16%; }   /* Invoice title, by request; +15% of Billing's width (by request) */
.oc-table-inv-grouped .oc-col-billing   { width: 12.24%; }   /* 15% narrower, given to Title (by request) */
.oc-table-inv-grouped .oc-col-amount    { width: 10%; text-align: center; font-variant-numeric: tabular-nums; }
/* Amount: the heading left-aligned, the figures centred (by request). */
.oc-table-inv-grouped thead th.oc-col-amount { text-align: left; }
.oc-table-inv-grouped tbody td.oc-col-amount { text-align: center; }
.oc-table-inv-grouped .oc-col-status    { width: 10%; }
.oc-table-inv-grouped .oc-col-methods   { width: 9%; text-align: center; }
.oc-table-inv-grouped .oc-col-created   { width: 9.65%; }
.oc-table-inv-grouped .oc-col-date      { width: 9.65%; }
.oc-table-inv-grouped .oc-col-actions   { width: 9%; text-align: center; }
.oc-inv-project-row .oc-info-dot { margin-left: .375rem; vertical-align: middle; text-transform: none; letter-spacing: 0; }
/* Projects list: fixed widths, so the row actions can never push the table wider than the page (it
   scrolled sideways on a laptop screen — the actions column is nowrap and sized to its content). */
.oc-card .oc-table-scroll > .oc-table-projects { table-layout: fixed; min-width: 0; }
.oc-table-projects .oc-col-client  { width: 34%; }
.oc-table-projects .oc-col-project { width: 32%; }
.oc-table-projects .oc-col-date    { width: 18%; }
.oc-table-projects .oc-col-actions { width: 7.5rem; }
/* Phone: fixed layout with min-width 0 clipped the cells instead of scrolling; keep a floor so the wrapper scrolls. */
@media (max-width: 48rem) { .oc-card .oc-table-scroll > .oc-table-projects { min-width: 36rem; } }
.oc-table-projects td { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Headings clip like the cells — a heading wider than its column ran into the next one. */
.oc-table-inv-grouped :is(td, thead th) { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.oc-table-inv-grouped .oc-row-actions { justify-content: center; }
.oc-table-inv-grouped .oc-col-date.is-danger { color: var(--danger); font-weight: 600; }
/* Phones and tablets (below 64rem, where nine columns no longer fit beside the sidebar without scrolling
   sideways): the table gives way to compact cards (the .oc-inv-cards markup beside it) —
   never the table itself stacked cell by cell, which made every invoice a screen tall. */
.oc-inv-cards { display: none; }
.oc-inv-filters-toggle { display: none; }
.oc-inv-filters { display: contents; }
@media (max-width: 64rem) {
    .oc-inv-client > .oc-table-scroll { display: none; }
    .oc-inv-cards { display: block; padding: 0 .75rem .75rem; }
    .oc-inv-cards-project { display: flex; align-items: center; gap: .375rem; flex-wrap: wrap; padding: .75rem .25rem .375rem;
        font-size: .6875rem; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--ink-700); }
    .oc-inv-cards-project .oc-inv-project-name { min-width: 0; overflow-wrap: anywhere; }
    .oc-inv-card { display: flex; flex-direction: column; gap: .25rem; padding: .625rem .75rem; margin-bottom: .5rem;
        border: 1px solid var(--line); border-radius: 10px; background: var(--surface); cursor: pointer; }
    .oc-inv-card:hover { border-color: var(--line-strong, var(--line)); }
    .oc-inv-card-top { display: flex; align-items: center; justify-content: space-between; gap: .5rem; }
    .oc-inv-card-top .oc-link { font-size: .875rem; font-weight: 700; }
    .oc-inv-card-billing { font-size: .75rem; font-weight: 600; color: var(--ink-500); }
    .oc-inv-card-line { font-size: .875rem; color: var(--ink-900); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .oc-inv-card-meta { display: flex; align-items: baseline; justify-content: space-between; gap: .5rem; font-size: .8125rem;
        color: var(--ink-500); }
    .oc-inv-card-meta .is-danger { color: var(--danger); font-weight: 600; }
    .oc-inv-card-amount { font-size: 1rem; font-weight: 700; color: var(--ink-900); }
    .oc-inv-card-foot { display: flex; align-items: center; justify-content: space-between; gap: .5rem;
        margin-top: .125rem; padding-top: .375rem; border-top: 1px solid var(--line-2); }
    .oc-inv-card-method { min-width: 0; font-size: .75rem; color: var(--ink-500); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .oc-inv-card-actions { display: flex; align-items: center; gap: .125rem; flex-shrink: 0; }
    .oc-inv-card-actions .btn { padding: .25rem .5rem; min-height: 0; font-size: .8125rem; }
    .oc-inv-more { position: relative; }
    .oc-inv-more-list { position: absolute; right: 0; bottom: calc(100% + .25rem); z-index: 30; min-width: 9rem; padding: .25rem;
        border: 1px solid var(--line); border-radius: 10px; background: var(--surface); box-shadow: var(--shadow-md, 0 8px 24px rgb(0 0 0 / .3)); }
    .oc-inv-more-list > :is(a, button) { display: flex; align-items: center; gap: .5rem; width: 100%; padding: .5rem .625rem;
        border: 0; border-radius: 6px; background: none; font-size: .875rem; color: var(--ink-700); text-align: left; cursor: pointer; }
    .oc-inv-more-list > :is(a, button):hover, .oc-inv-more-list > :is(a, button):focus-visible { background: var(--surface-muted, transparent); }
    .oc-inv-more-list > .is-danger { color: var(--danger); }
    .oc-inv-client-head { padding: .75rem 1rem; }
    /* Corners (by request): the client card clips to its own rounded shape — overflow:clip also holds the
       header's square hover fill inside it — and the invoice cards inside share the theme's radius instead
       of a fixed 10px that did not match the card around them. */
    .oc-inv-client { overflow: clip; border-radius: var(--radius-lg); }
    .oc-inv-client-head { border-radius: var(--radius-lg) var(--radius-lg) 0 0; }
    .oc-inv-client:not([open]) .oc-inv-client-head { border-radius: var(--radius-lg); }
    .oc-inv-card { border-radius: var(--radius); }   /* not overflow:hidden — its More menu opens outside it */
    .oc-inv-card:last-child { margin-bottom: 0; }
    /* Status tabs as scrollable filter chips — the usual mobile pattern for status filters (Gmail, Stripe,
       Shopify): rounded pills in one swipeable row, the chosen one filled. Each sized to its own label —
       a phone-wide .oc-tab rule (flex: 1 1 0, written for the Builder's tabstrip) squeezed them to equal
       widths, so the labels ran into each other. */
    .oc-inv-tabs { flex-wrap: nowrap; gap: .5rem; overflow-x: auto; scrollbar-width: none; border-bottom: 0;
        padding-bottom: .25rem; scroll-snap-type: x proximity; }
    .oc-inv-tabs::-webkit-scrollbar { display: none; }
    .oc-inv-tabs .oc-tab { flex: 0 0 auto; min-width: auto; margin: 0; padding: .375rem .875rem; border: 1px solid var(--line);
        border-radius: 999px; font-size: .8125rem; font-weight: 600; color: var(--ink-700); scroll-snap-align: start; }
    .oc-inv-tabs .oc-tab.is-active { color: var(--brand-700); border-color: var(--brand-600);
        background: color-mix(in srgb, var(--brand-600) 16%, transparent); }
    /* Summary: a small 2 × 2 grid instead of four full-width cards. */
    .oc-inv-stats { display: grid; grid-template-columns: 1fr 1fr; gap: .5rem; }
    .oc-inv-stats .oc-stat { padding: .625rem .75rem; gap: .5rem; }
    .oc-inv-stats .oc-stat-icon { display: none; }
    .oc-inv-stats .oc-stat-value { font-size: 1rem; }
    .oc-inv-stats .oc-stat-label { font-size: .75rem; }
    /* Pager: in the flow at the end of the list, never pinned — a sticky bar would cover the last card and
       hold a strip of a small screen for good. One compact row: the count left, the pages right; rows per
       page is a desktop choice (the default 15 suits a phone), and the rule above it is the cards' own gap. */
    .oc-inv-pager .oc-pagination { flex-wrap: nowrap; padding: .75rem .25rem 0; border-top: 0; }
    .oc-inv-pager .oc-pagination-count { flex: 0 1 auto; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .oc-inv-pager .oc-pagination-size { display: none; }
    .oc-inv-pager .oc-pagination-pages { margin-left: auto; flex: 0 0 auto; }
    /* Filters: search stays; the dropdowns fold behind "Filters" and stack when opened. */
    /* Search and the Filters button share one row (the shared phone toolbar stacks as a column, which
       turned the search box's flex size into a height); opened filters drop below, full width. */
    .oc-inv-toolbar { flex-direction: row; flex-wrap: wrap; align-items: center; gap: .5rem; }
    .oc-inv-toolbar > .field-group { flex: 1 1 0; min-width: 0; max-width: none; height: auto; }
    .oc-inv-filters-toggle { display: inline-flex; flex: 0 0 auto; width: auto; }
    .oc-inv-filters { display: none; }
    .oc-inv-filters.is-open { display: flex; flex-direction: column; align-items: stretch; gap: .5rem; flex: 1 1 100%; }
    .oc-inv-filters.is-open > .field-select, .oc-inv-filters.is-open .oc-searchable { width: 100%; max-width: none; min-width: 0; }
}

/* ==== Invoice, read-only (Areas/Admin/Views/Invoices/Details) ==========================================
   Main column of sections, a side column with the summary and the Status card; one column on a phone. */
.oc-inv-view { display: grid; grid-template-columns: minmax(0, 1fr) 22rem; gap: 1.25rem; align-items: start; }
.oc-inv-view-side { position: sticky; top: 1rem; }
@media (max-width: 60rem) {
    .oc-inv-view { grid-template-columns: minmax(0, 1fr); }
    .oc-inv-view-side { position: static; order: -1; }
}
.oc-inv-view-title { margin-top: .25rem; font-size: 1rem; color: var(--ink-700); }
.oc-inv-view-meta { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem 1rem; margin-top: .625rem;
    font-size: .875rem; color: var(--ink-500); }
.oc-inv-view-meta > span { display: inline-flex; align-items: center; gap: .375rem; }
.oc-inv-view-dl { grid-template-columns: minmax(8rem, auto) 1fr; }
.oc-inv-view-items .oc-inv-view-milestone { display: inline-block; margin-right: .5rem; padding: .05rem .4rem;
    border-radius: 6px; font-size: .75rem; font-weight: 700; background: var(--surface-muted, transparent);
    border: 1px solid var(--line); color: var(--ink-700); }
.oc-inv-view-methods { display: flex; flex-direction: column; gap: .875rem; margin: 0; padding: 0; list-style: none; }
.oc-inv-view-methods li { display: flex; flex-direction: column; gap: .25rem; }
.oc-inv-view-heading { margin: 0 0 .75rem; font-size: 1rem; font-weight: 700; color: var(--ink-900); }
.oc-inv-view-activity { display: flex; flex-direction: column; gap: .75rem; margin: 0; padding: 0; list-style: none; }
.oc-inv-view-activity li { display: flex; flex-direction: column; gap: .125rem; padding-left: .875rem;
    border-left: 2px solid var(--line); }
/* The Status card's actions in the read-only page's narrow side column: stacked, full width — in one
   unwrapping row (the builder page's layout) they spilled out of the card to the left. */
.oc-inv-view-side .oc-form-actions { flex-direction: column; align-items: stretch; gap: .5rem; }
.oc-inv-view-side .oc-form-actions > form { margin: 0; }
.oc-inv-view-side .oc-form-actions :is(.btn, form > .btn) { width: 100%; justify-content: center; }

/* Agreements status filter: sized to its longest option ("Awaiting signature", "Changes requested") instead
   of the toolbar's generic 9rem floor, which clipped them. The open list is anchor-size(width) wide (the
   base-select rule above), so it grows with the field. Local to this toolbar. */
.oc-agreements-toolbar > select[name="status"] { min-width: 14rem; }

/* Invoice page header on a phone (by request): ONE line — Duplicate, Another invoice, Preview, Download PDF
   (and Record payment) shrink to icon-only on the left, Send invoice keeps its full text on the far right.
   Local to .oc-inv-actions (Invoices/Details). The labels stay in the markup for desktop and screen readers
   (each button also has an aria-label). */
@media (max-width: 640px) {
    .oc-page-head .oc-inv-actions { flex-wrap: nowrap; gap: .4rem; }
    .oc-page-head .oc-inv-actions .oc-btn-label { display: none; }
    .oc-page-head .oc-inv-actions > .btn,
    .oc-page-head .oc-inv-actions > a.btn,
    .oc-page-head .oc-inv-actions > form > .btn { flex: 0 0 auto; padding-inline: .6rem; }
    .oc-page-head .oc-inv-actions > form:not(.hidden) { flex: 0 0 auto; }
    .oc-page-head .oc-inv-actions > [data-testid="invoice__send"] { margin-left: auto; padding-inline: .85rem; }
}

/* ---------- Contained scrollbars (project-wide rule; see .claude/rules/conventions.md → CSS) -------------
   A scrollbar inside a rounded card, modal, popup or list must stop where the rounded corners BEGIN — never
   run into the curve or past the container's edge. The track is inset by the corner radius at both ends
   (horizontal bars left/right, vertical bars top/bottom), so the thumb can never reach the curve.

   Why a rule list and not one global selector: the theme styles its scrollers with the standard
   scrollbar-width / scrollbar-color properties, and while those are set Chrome ignores every
   ::-webkit-scrollbar rule — so each scroller is reset to auto here, which is also what lets the track
   margin apply. Firefox has no track-margin equivalent, so the whole block is skipped there and it keeps
   its own thin bar (@supports selector()).

   A NEW scroll area inside a rounded container joins by adding .oc-scroll-inset to the element (or its
   class to this list if it comes from a partial that cannot take one). --oc-scroll-inset overrides the
   inset for a container whose radius is not --radius-lg. Scrollers with scrollbar-width: none (the tab
   strips, the stat rails) show no bar and need nothing. */
@supports selector(::-webkit-scrollbar) {
    :is(.oc-table-scroll, .oc-datatable-scroll, .oc-kanban-scroll, .oc-kanban-col-body, .oc-kanban-move-menu,
        .oc-section-list, .oc-editor-nav, .oc-modal-body, .oc-modal-panel, .oc-drawer-body, .oc-preview-body,
        .oc-command-list, .oc-combobox-menu, .oc-richtext pre, .oc-scroll-inset) {
        scrollbar-width: auto;
        scrollbar-color: auto;
    }
    :is(.oc-table-scroll, .oc-datatable-scroll, .oc-kanban-scroll, .oc-kanban-col-body, .oc-kanban-move-menu,
        .oc-section-list, .oc-editor-nav, .oc-modal-body, .oc-modal-panel, .oc-drawer-body, .oc-preview-body,
        .oc-command-list, .oc-combobox-menu, .oc-richtext pre, .oc-scroll-inset)::-webkit-scrollbar {
        width: .375rem;
        height: .375rem;
    }
    :is(.oc-table-scroll, .oc-datatable-scroll, .oc-kanban-scroll, .oc-kanban-col-body, .oc-kanban-move-menu,
        .oc-section-list, .oc-editor-nav, .oc-modal-body, .oc-modal-panel, .oc-drawer-body, .oc-preview-body,
        .oc-command-list, .oc-combobox-menu, .oc-richtext pre, .oc-scroll-inset)::-webkit-scrollbar-track {
        background: transparent;
    }
    :is(.oc-table-scroll, .oc-datatable-scroll, .oc-kanban-scroll, .oc-kanban-col-body, .oc-kanban-move-menu,
        .oc-section-list, .oc-editor-nav, .oc-modal-body, .oc-modal-panel, .oc-drawer-body, .oc-preview-body,
        .oc-command-list, .oc-combobox-menu, .oc-richtext pre, .oc-scroll-inset)::-webkit-scrollbar-track:horizontal {
        margin-inline: var(--oc-scroll-inset, var(--radius-lg));
    }
    :is(.oc-table-scroll, .oc-datatable-scroll, .oc-kanban-scroll, .oc-kanban-col-body, .oc-kanban-move-menu,
        .oc-section-list, .oc-editor-nav, .oc-modal-body, .oc-modal-panel, .oc-drawer-body, .oc-preview-body,
        .oc-command-list, .oc-combobox-menu, .oc-richtext pre, .oc-scroll-inset)::-webkit-scrollbar-track:vertical {
        margin-block: var(--oc-scroll-inset, var(--radius-lg));
    }
    :is(.oc-table-scroll, .oc-datatable-scroll, .oc-kanban-scroll, .oc-kanban-col-body, .oc-kanban-move-menu,
        .oc-section-list, .oc-editor-nav, .oc-modal-body, .oc-modal-panel, .oc-drawer-body, .oc-preview-body,
        .oc-command-list, .oc-combobox-menu, .oc-richtext pre, .oc-scroll-inset)::-webkit-scrollbar-thumb {
        /* Subtle: translucent, only 4px of it visible (the 1px transparent border is just grab room), and it
           brightens when pointed at so it is still easy to find and drag. */
        background: color-mix(in srgb, var(--ink-500) 32%, transparent);
        background-clip: padding-box;
        border: 1px solid transparent;
        border-radius: 999px;
    }
    :is(.oc-table-scroll, .oc-datatable-scroll, .oc-kanban-scroll, .oc-kanban-col-body, .oc-kanban-move-menu,
        .oc-section-list, .oc-editor-nav, .oc-modal-body, .oc-modal-panel, .oc-drawer-body, .oc-preview-body,
        .oc-command-list, .oc-combobox-menu, .oc-richtext pre, .oc-scroll-inset)::-webkit-scrollbar-thumb:hover {
        background: color-mix(in srgb, var(--ink-500) 58%, transparent);
        background-clip: padding-box;
    }
}

/* Queue rows (portal "Needs attention", "Active projects"): the severity accent is a left BORDER on each row,
   and the last row sits flush in the card's bottom corner — so its straight edge poked out past the curve.
   Clipping the list to the card's own rounded shape makes the accent (and the row's hover tint) follow the
   corner instead. Only the bottom corners are rounded here when a card head sits above the list. */
.oc-card > .oc-queue { overflow: hidden; border-radius: 0 0 var(--radius-lg) var(--radius-lg); }
.oc-card > .oc-queue:first-child { border-radius: var(--radius-lg); }

/* The wordmark on the sign-in pages is a link back to the sign-in page (by request). Sized to the logo, not
   the row, so only the logo itself is clickable; the focus ring is for keyboard users. Local to the Portal
   auth layout. */
.oc-auth-logo-link { display: block; width: fit-content; border-radius: .5rem; }
.oc-auth-logo-link:focus-visible { outline: 2px solid var(--oc-lime, #a3e635); outline-offset: 4px; }

/* ---- Sign-in pages, background video (by request; oc-auth-video.js) -----------------------------
   The form panel is the whole screen on a phone and the right-hand (black) half on desktop; the video lives
   INSIDE it, so it can never touch the green brand panel or anything else. It fills the panel edge to edge with object-fit: cover —
   scaled, never stretched, the overflow cropped evenly from the centre — behind a dark overlay that keeps
   the form and logo readable. isolation: isolate keeps the video and the logo's lighten blend in one
   stacking group so the logo sits cleanly on it. It fades in only once it is actually playing; until then
   (or when it never loads) the page is the same dark ground as before. */
.oc-auth-form { position: relative; isolation: isolate; overflow: hidden; }
.oc-auth-bg { position: absolute; inset: 0; z-index: -1; background: #03060a; }
.oc-auth-bg-video { display: block; width: 100%; height: 100%; object-fit: cover; object-position: center; opacity: 0; transition: opacity .8s ease; }
.oc-auth-bg-video.is-playing { opacity: 1; }
.oc-auth-bg::after { content: ""; position: absolute; inset: 0; background: linear-gradient(180deg, rgba(3,6,10,.55) 0%, rgba(3,6,10,.72) 100%); }

@media (prefers-reduced-motion: reduce) { .oc-auth-bg-video { transition: none; } }

/* "N new" tag on a Needs attention row: something in that group was touched in the last 48 hours. Quiet and
   small so the row's label stays the loudest thing on it. Local to the Dashboard's attention card. */
.oc-attention-new { display: inline-block; margin-left: .45rem; padding: .05rem .45rem; border-radius: 999px; vertical-align: middle;
    font-size: .6875rem; font-weight: 700; letter-spacing: .02em; line-height: 1.5;
    color: var(--brand-700); background: var(--brand-50); }

/* The Octacer contact card's avatar is wwwroot/octa_logo.jpg (by request): the ring mark on dark grey, no wordmark.
   The ring sits a little below and right of the image's own centre, so the picture is nudged back (translate) and
   enlarged a touch (scale) to put the RING dead centre in the round avatar; the avatar's own dark-grey ground
   matches the image edge, so the nudge never shows a seam. Local to .oc-avatar-logo (the contact card). */
.oc-avatar.oc-avatar-logo { padding: 0; overflow: hidden; background: linear-gradient(180deg, #4a4a4a 0%, #2a2a2a 100%); }
/* Measured from the rendered avatar: at scale 1.2 the ring filled ~95% of the circle (almost touching the edge) and
   sat ~3px right of centre. Scale 0.88 leaves an even margin of grey all round; translate (re-measured on the rendered
   avatar: ring was 5px left and 2px low of centre, so shifted right 0.2% / up 5.2%) puts the ring's centre on
   the avatar's centre. The avatar's own gradient matches the image's grey, so the area the smaller picture no
   longer covers does not show as a seam. */
.oc-avatar-logo-img { display: block; width: 100%; height: 100%; object-fit: cover; translate: -0.2% -5.2%; scale: .88; }

/* Portal Your team: the search box and the Search button are the same height (by request) — the input was
   taller than the button beside it, which they sized by two unrelated rules. One fixed height for both.
   Local to .oc-team-toolbar. */
.oc-team-toolbar { align-items: center; }
.oc-team-toolbar .oc-search-input,
.oc-team-toolbar > .btn { box-sizing: border-box; height: 2.75rem; padding-block: 0; }

/* Portal Proposals cards: the decision row (Accept / Request changes / Decline) is three equal-width, equal-height
   buttons with centred content, and Download PDF is an icon-only square (by request) so the footer is not a
   mixed row of wide and narrow buttons. */
.oc-proposal-decide { align-items: stretch; }
.oc-proposal-decide .btn { flex: 1 1 0; min-width: 0; justify-content: center; text-align: center; min-height: 2.5rem; gap: .4rem; }
.oc-icon-only { width: 2.5rem; min-height: 2.5rem; padding: 0; justify-content: center; flex: none; }
.oc-proposal-card-foot .btn { min-height: 2.5rem; }

/* Agreements list: the per-row signing form that Add Signature opens in place (same look as the sign-all form). */
.oc-sign-inline { display: grid; gap: 1rem; margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--line-2); }
.oc-sign-inline[hidden] { display: none; }

/* Proposals and Agreements lists: on desktop the download is the full "Download PDF" button (by request); on a
   phone it stays the icon-only square so the row of buttons fits on one line. Scoped to the link that carries
   the label, so every other icon-only button is untouched. */
.oc-dl-label { display: none; }
@media (min-width: 40.01rem) {
    :is(.oc-icon-only, .oc-icon-btn):has(> .oc-dl-label) { width: auto; padding-inline: 1rem; gap: .5rem; }
    .oc-dl-label { display: inline; }
}

/* The proposal an agreement is drawn up under is a plain label in the Admin Agreements list (by request), not a
   link: it keeps the link's weight and colour so the "under …" line still reads the same, minus the click. */
.oc-link-plain { color: var(--primary-700, var(--primary)); font-weight: 600; cursor: default; }

/* Admin header account menu (octa-core account-menu-2026-10): the theme's _UserMenu at the top right of the
   content pane, where the sidebar foot's avatar / theme / sign-out used to be. Same strip as the portal's. */
.oc-admin-topbar {
    display: flex; align-items: center; justify-content: flex-end; gap: .35rem;
    margin: 0 0 .35rem; min-height: 2.5rem;
}

/* Admin Invoices status tabs (octa-core 0265c0e sync): the theme's compact tab height left this strip 1px
   taller than its box, and overflow:auto answered with a vertical scrollbar (stacked arrows at the right).
   Scoped to this one strip; it still scrolls sideways on a narrow screen. */
.oc-inv-tabs { overflow-y: hidden; }

/* NOTE (octa-core 0265c0e sync): the builder / New-proposal tab hook is data-oc-builder-tabs, NOT data-oc-tabs.
   octa-core's octacer-nav.js now owns [data-oc-tabs] (role="tablist" roving tabs) and hid the "Proposal
   details" panel on load. See proposal-builder-tabs.js. */

/* Primary contact marker (Admin client > Contacts). Desktop shows the "Primary" tag; phone and tablet (< 64rem,
   the width where the app shell switches to the bottom tab bar) show a brand-blue star after the name instead,
   because the tag wraps onto its own line in the narrow name column. Both are in the markup, CSS picks one.
   Local to that one list; the same ★ marks the primary contact in the proposal form's contact picker. */
.oc-primary-tag { margin-inline-start: .4rem; }
.oc-primary-star { display: none; margin-inline-start: .3rem; color: var(--primary); font-size: .95em; line-height: 1; white-space: nowrap; }
@media (max-width: 63.9375rem) {
    .oc-primary-tag { display: none; }
    .oc-primary-star { display: inline; }
}

/* ==========================================================================================
   Microsoft Fluent 2 typography — all Admin pages (html[data-theme-scope=admin] main). Trial started on:
     • Admin > Clients list           (main has .oc-table-clients)
     • Admin > Proposals list         (main has .oc-table-proposals)
     • Admin > Proposal builder       (main has the Send button and no agreement Save button, so the
                                       agreement builder, which shares the view, is left alone)
   Every other page keeps its current styling. Inter stays the font family.

   Roles are the official Fluent 2 web type ramp, each as one `font` shorthand
   (weight size/line-height family). Sizes are rem so browser zoom still works.
     Caption 2  10/14   Caption 1  12/16   Body 1  14/20   Subtitle 2  16/22
     Subtitle 1 20/28   Title 3    24/32   Title 2 28/36
   Weights are Fluent's: Regular 400, Semibold 600 ("Strong"), Bold 700 ("Stronger").
   ========================================================================================== */
:root {
    --fl-caption2:         400 .625rem/.875rem var(--font-sans);
    --fl-caption2-strong:  600 .625rem/.875rem var(--font-sans);
    --fl-caption1:         400 .75rem/1rem var(--font-sans);
    --fl-caption1-strong:  600 .75rem/1rem var(--font-sans);
    --fl-body1:            400 .875rem/1.25rem var(--font-sans);
    --fl-body1-strong:     600 .875rem/1.25rem var(--font-sans);
    --fl-subtitle2:        600 1rem/1.375rem var(--font-sans);
    --fl-subtitle1:        600 1.25rem/1.75rem var(--font-sans);
    --fl-title3:           600 1.5rem/2rem var(--font-sans);
    --fl-title1:           600 2rem/2.5rem var(--font-sans);
    --fl-title2:           600 1.75rem/2.25rem var(--font-sans);
}

:is(html[data-theme-scope="admin"], html[data-theme-scope="portal"]) main {
    /* ---- Page header ---- */
    & .oc-page-title { font: var(--fl-title2); }                          /* "Clients", "Proposals", proposal name */
    & .oc-page-sub, & .oc-page-sub .oc-link { font: var(--fl-body1); }    /* subtitle and the "Proposals" crumb */
    & .oc-head-meta > span, & .oc-head-meta .badge ~ span { font: var(--fl-body1); }
    & .oc-head-meta .badge, & .oc-editor-title-row .badge, & .oc-revision-row .badge { font: var(--fl-caption1-strong); }

    /* ---- Buttons: compact (btn-sm) = Fluent small, default = Fluent medium ---- */
    & .btn { font: var(--fl-body1-strong); }
    & .btn.btn-sm, & .btn-sm .oc-btn-label, & .oc-head-actions .btn, & .oc-head-actions .btn .oc-btn-label { font: var(--fl-caption1-strong); }

    /* ---- Tabs ---- */
    & .oc-tab { font: var(--fl-body1); }
    & .oc-tab.is-active, & .oc-tab[aria-selected="true"] { font: var(--fl-body1-strong); }

    /* ---- Summary cards: the number is a Title 3 figure, the label a Caption 1 ---- */
    & .oc-stat-value, & .oc-stats-grid > .oc-stat .oc-stat-value, & .oc-stats-grid.oc-stats-compact > .oc-stat .oc-stat-value { font-size: 1.5rem; line-height: 2rem; font-weight: 600; }
    & .oc-stat-label, & .oc-stats-grid > .oc-stat .oc-stat-label, & .oc-stats-grid.oc-stats-compact > .oc-stat .oc-stat-label { font: var(--fl-caption1); }

    /* ---- Filters ---- */
    & .oc-filterfield-label { font: var(--fl-caption1-strong); }

    /* ---- Tables ---- */
    & .oc-table thead th { font: var(--fl-caption1-strong); }
    & .oc-table tbody td, & .oc-table tfoot :is(td, th) { font: var(--fl-body1); }
    & .oc-table tbody td :is(.oc-link, a, strong) { font: var(--fl-body1-strong); }
    & .oc-table .badge { font: var(--fl-caption1-strong); }
    & .oc-title-version { font: var(--fl-caption2-strong); }
    & .oc-feedback-cell .oc-muted { font: var(--fl-body1); }
    /* Narrower than 1280px (small laptops, tablets, phones) the fixed table columns cannot hold Body 1 (14px)
       rows, so they step down to Caption 1 (12px), the next Fluent role down, rather than clipping. */
    @media (max-width: 1280px) {
        & .oc-table tbody td, & .oc-table tfoot :is(td, th), & .oc-feedback-cell .oc-muted { font: var(--fl-caption1); }
        & .oc-table tbody td :is(.oc-link, a, strong) { font: var(--fl-caption1-strong); }
    }

    /* ---- Cards and sections ---- */
    & .oc-card-title { font: var(--fl-subtitle2); }
    & .oc-row-head > span, & .oc-editor-nav-head, & .oc-section-add-head { font: var(--fl-caption1-strong); }
    & .oc-autosave, & .oc-row-head > .oc-autosave { font: var(--fl-caption1); }

    /* ---- Forms ---- */
    & .field-label { font: var(--fl-body1); }
    & .field-hint, & .field-error, & .field-validation-error { font: var(--fl-caption1); }
    & .oc-form-actions .oc-muted { font: var(--fl-caption1); }
    & .oc-section-guide { font: var(--fl-body1); }

    /* ---- Alerts and banners ---- */
    & .oc-alert, & .oc-alert span, & .oc-alert .oc-block { font: var(--fl-body1); }
    & .oc-alert strong, & .oc-alert .oc-link, & span > strong { font: var(--fl-body1-strong); }

    /* ---- Builder: section list and revision history ---- */
    & .oc-section-item .oc-section-name, & .oc-section-add-row .oc-section-name { font: var(--fl-body1); }
    & .oc-section-item.is-active .oc-section-name, & .oc-section-item[aria-current] .oc-section-name { font: var(--fl-body1-strong); }
    & .oc-card-body .oc-t-sm, & .oc-revision-row .oc-t-muted { font: var(--fl-body1); }
    & .oc-revision-row .oc-number { font-size: .875rem; line-height: 1.25rem; font-weight: 400; }
    & .oc-revision-row .oc-number:last-of-type { font-weight: 600; }
    & .oc-sheet-section-n { font: var(--fl-caption1); }
    & .oc-checkline span { font: var(--fl-body1); }       /* checkbox label, same as a field label */
    & .oc-row-num { font: var(--fl-caption1); }

    /* ---- Confirmation dialogs opened from these pages ---- */
    & .oc-modal-warn p, & .oc-modal-warn p strong { font: var(--fl-body1); }
    & .oc-modal-warn p strong { font-weight: 600; }
}

/* Fluent Body 1 (14px) table rows are one step bigger than the old 12px rows, so a few fixed column widths on
   these two tables no longer fit their content (phone numbers, client names, the Contacts heading). Same
   total width, shifted a little from the roomiest columns. Scoped to the two Fluent screens. */
main:has(.oc-table-clients) .oc-table-clients .oc-col-phone    { width: 11.8%; }
main:has(.oc-table-clients) .oc-table-clients .oc-col-contacts { width: 7%; }
main:has(.oc-table-clients) .oc-table-clients .oc-col-country  { width: 7.4%; }
main:has(.oc-table-clients) .oc-table-clients .oc-col-actions  { width: 9.6%; }
/* Proposals: the original percentages add up to 109 and the browser scales them down, so set all nine
   explicitly and make them add up to 100. */
main:has(.oc-table-proposals) .oc-table-proposals :is(th, td).oc-col-title    { width: 16%; }
main:has(.oc-table-proposals) .oc-table-proposals :is(th, td).oc-col-client   { width: 11.6%; }
main:has(.oc-table-proposals) .oc-table-proposals :is(th, td).oc-col-pricing  { width: 10.2%; }
main:has(.oc-table-proposals) .oc-table-proposals :is(th, td).oc-col-status   { width: 9.2%; }
main:has(.oc-table-proposals) .oc-table-proposals :is(th, td).oc-col-notes    { width: 13.4%; }
main:has(.oc-table-proposals) .oc-table-proposals :is(th, td).oc-col-feedback { width: 11%; }
main:has(.oc-table-proposals) .oc-table-proposals :is(th, td).oc-col-date     { width: 8.8%; }
main:has(.oc-table-proposals) .oc-table-proposals :is(th, td).oc-col-actions  { width: 11%; }

/* ---- Fluent 2 typography, second pass: the shell and the remaining Admin page parts ----
   `:not(#fl)` lifts specificity above the older per-component font rules; it matches everything. */
:is(html[data-theme-scope="admin"], html[data-theme-scope="portal"]):not(#fl) {
    /* Shell: sidebar, user menu, phone tab bar */
    & .oc-brand-name { font: var(--fl-subtitle2); }
    & .oc-brand-name small, & .oc-nav-label { font: var(--fl-caption2-strong); }
    & .oc-sidebar-collapse span { font: var(--fl-caption1); }
    & .oc-user-avatar, & .oc-user-head-name { font: var(--fl-body1-strong); }
    & .oc-user-head-sub { font: var(--fl-caption1); }
    & .oc-popmenu-label { font: var(--fl-caption1-strong); }
    & .oc-tabbar-label { font: var(--fl-caption1); }
    & .oc-more-group-title { font: var(--fl-caption1-strong); }

    /* Page parts */
    & main {
        & .oc-create-list a, & .oc-empty-text, & .oc-more-summary span, & .oc-more-summary .oc-t-faint,
        & .oc-card-body .oc-muted, & .oc-inv-view-methods li .oc-muted, & .oc-check-inline .oc-muted,
        & .oc-template-desc, & .oc-primary-star { font: var(--fl-body1); }
        & .oc-card-head .oc-link, & .oc-card-head .oc-card-sub, & .oc-kv dt, & .oc-kv dt .oc-link, & .oc-dl dt,
        & .oc-attention-detail, & .oc-section-s, & .oc-template-secs li, & .oc-pagination-count,
        & .oc-inv-client-meta, & .oc-invoice-billto-label, & .oc-invoice-billto span { font: var(--fl-caption1); }
        & .badge, & .oc-attention-new, & .oc-template-preview-h, & .oc-template-when-label,
        & .oc-pricing-cols span, & .oc-pricing-cols .oc-pricing-total, & .oc-pricing-add-item { font: var(--fl-caption1-strong); }
        & .oc-empty-title, & .oc-section-t, & .oc-invoice-balance dt, & .oc-invoice-balance dd { font: var(--fl-subtitle2); }
    }
    & .oc-center-card .oc-page-title { font: var(--fl-subtitle1); }
    & .oc-center-card .oc-page-sub { font: var(--fl-body1); }
}

/* Agreements list: Fluent Caption 1 Strong badges are 1px wider; give "Ready to send" 2% from Client (still 100%). */
.oc-table-contracts .oc-col-client { width: 15.28%; }
.oc-table-contracts .oc-col-status { width: 14%; }

/* Fluent 2: figures and codes take the size of the role around them. */
:is(html[data-theme-scope="admin"], html[data-theme-scope="portal"]):not(#fl) main {
    & .oc-mono { font-size: 1em; }
    & td.oc-mono, & th.oc-mono { font-size: .875rem; }
    & .oc-inv-card-top .oc-link { font: var(--fl-body1-strong); }
    & .oc-attention-count, & .oc-price-summary .oc-number { font-size: 1rem; line-height: 1.375rem; }
    & .oc-kv .oc-number, & .field-textarea { font-size: .875rem; }
}

:is(html[data-theme-scope="admin"], html[data-theme-scope="portal"]):not(#fl) .oc-toast-message { font: var(--fl-body1); }
:is(html[data-theme-scope="admin"], html[data-theme-scope="portal"]):not(#fl) main {
    & .oc-price-item-total, & .oc-price-item-total .oc-number span { font-size: .875rem; }
    & .oc-method-card-b { font: var(--fl-caption1); }
}

/* ---- Fluent 2 typography, Portal-only parts (the client's own pages; the proposal / contract / invoice documents are .pp-* / invoice.css and keep their print design) ---- */
html[data-theme-scope="portal"]:not(#fl) {
    & .oc-page-title, & .oc-hero-title, & .oc-pay-due-amount { font: var(--fl-title1); }
    & .oc-page-sub, & .oc-page-sub .oc-link, & .oc-tagline, & .oc-hero-text, & .oc-skip-link,
    & .oc-nav-link span, & .oc-card-body .oc-t-muted, & .oc-t-muted time, & .oc-record-facts dd time,
    & .field-label .oc-required { font: var(--fl-body1); }
    & .btn, & .oc-modal-actions .btn span, & .oc-form-actions .btn .oc-btn-label, & .oc-form-actions .btn .oc-btn-busy,
    & .oc-hero-card .btn, & .oc-contact-name, & .oc-about-name { font: var(--fl-body1-strong); }
    & .btn.btn-sm, & .oc-proposal-card-foot .btn { font: var(--fl-caption1-strong); }
    & .oc-record-facts dt, & .oc-card-head .oc-card-sub { font: var(--fl-caption1); }
    & .oc-record-facts .oc-number { font-size: .875rem; line-height: 1.25rem; }
    & .oc-proposal-card-title, & .oc-rail-title { font: var(--fl-subtitle2); }
    & .oc-table thead th, & .oc-table thead .oc-col-actions, & .oc-table th .oc-sort span { font: var(--fl-caption1-strong); }
}
html[data-theme-scope="portal"]:not(#fl) {
    & .oc-record-facts dd, & .oc-record-wide dd .oc-t-faint, & .oc-about-text, & .oc-about-list li { font: var(--fl-body1); }
    & .oc-flow-name { font: var(--fl-body1-strong); }
    & .oc-view-toggle-button, & .oc-record-link, & .oc-pill, & .oc-kicker { font: var(--fl-caption1-strong); }
    & .oc-signature-preview span, & .oc-file-picker-name, & .oc-file-picker-hint, & .oc-contact-role,
    & .oc-side-user div, & .oc-rail-when, & .oc-sign-all-actions .oc-check-inline, & .oc-queue-reason { font: var(--fl-caption1); }
}

/* Fluent 2 writes column headings in sentence case, so the theme's all-caps table headings are switched off here
   (the theme file itself is core-owned and stays as is). Eyebrow labels such as the sidebar's "WORKSPACE" are not table headings and keep their caps. */
:is(html[data-theme-scope="admin"], html[data-theme-scope="portal"]):not(#fl) {
    & .oc-table thead th, & .oc-table-inv-grouped .oc-inv-project-row th, & .oc-pricing-cols, & .oc-pricing-cols span { text-transform: none; letter-spacing: normal; }
}

/* The "not found" card sits outside the Admin and Portal layouts, so it is scoped to itself. */
html .oc-center-card .oc-page-title:not(#fl) { font: var(--fl-subtitle1); }
html .oc-center-card .oc-page-sub:not(#fl) { font: var(--fl-body1); }

/* ---- Quotations (Areas/Admin/Views/Quotations — the sidebar list, the form, the quotation's page) ---- */
/* Shown-not-typed values (the number, a sent quotation's client) sit in a field-shaped box so the grid
   lines up, but read as text, not as an input waiting for something. */
.oc-quo-readonly { display: flex; align-items: center; background: var(--surface-muted, transparent); color: var(--ink-700); }
/* The client portal's quotation document: the standalone document in a frame, tall enough to read in one go. */
.oc-quo-frame { display: block; width: 100%; height: 1150px; border: 0; border-radius: inherit; background: #fff; }
@media (max-width: 48rem) { .oc-quo-frame { height: 1500px; } }
/* The "sent to" banner sits a line below the Client field (by request). */
.oc-quo-billto:not(:empty) { margin-top: .75rem; }
/* ...and hugs its own content: it ends where the email ends, never stretching across the column (quotation form only). */
.oc-quo-billto .oc-quo-billbox { width: fit-content; max-width: 100%; }
/* Terms & conditions: one lead line, then the box at full width. */
/* "(Optional)" after a label or section title (by request) — quiet, regular weight, never a lead word. */
.oc-quo-optional { font-weight: 400; color: var(--ink-500); }
.oc-quo-part-lead { margin: -.5rem 0 1rem; font: var(--fl-caption1); color: var(--ink-500); }
/* The rich-text toolbar on desktop, every editor (_RichTextEditor), by request: the theme's small buttons with
   generous padding made a tall, sparse bar. Compact, evenly spaced 1.75rem buttons with .8125rem icons — still a
   comfortable mouse target. Phones have their own one-row rule (the 60rem block, ui-patterns.md). */
@media (min-width: 60.0625rem) {
    .oc-richtext-toolbar { gap: .125rem; padding: .25rem .375rem; }
    .oc-richtext-toolbar .btn.oc-icon-btn { width: 1.75rem; height: 1.75rem; min-height: 0; min-width: 0; padding: 0; justify-content: center; }
    .oc-richtext-toolbar .btn.oc-icon-btn i { width: .8125rem; height: .8125rem; font-size: .8125rem; }
    .oc-richtext-toolbar > span[aria-hidden] { height: 1rem; margin-inline: .25rem; }
}
/* Project details: one section, its parts stacked with even spacing (by request — they were five separate parts).
   Objectives and What we need from you, both short lists, sit side by side when there is room. */
.oc-quo-subs { display: flex; flex-direction: column; gap: 1.5rem; }
/* Project details folds (by request): the heading is the toggle, with a chevron that turns when open. */
.oc-quo-fold-summary { display: flex; align-items: center; justify-content: space-between; gap: .75rem; cursor: pointer;
    list-style: none; border-radius: 6px; }
.oc-quo-fold-summary::-webkit-details-marker { display: none; }
.oc-quo-fold-summary .oc-quo-part-title { margin: 0; }
.oc-quo-fold-summary:focus-visible { outline: 2px solid var(--brand-500, currentColor); outline-offset: 4px; }
.oc-quo-fold-chev { display: inline-grid; place-items: center; color: var(--ink-500); transition: transform .15s ease; }
.oc-quo-fold:not([open]) .oc-quo-fold-chev { transform: rotate(-90deg); }
.oc-quo-fold[open] > .oc-quo-fold-summary { margin-bottom: 1rem; }
/* The lead stays inside the toggle, so a closed fold still says what it holds. */
.oc-quo-fold-text { display: flex; flex-direction: column; gap: .25rem; min-width: 0; }
.oc-quo-fold-text .oc-quo-part-lead { margin: 0; }
/* A short field alone on its row (Total duration) takes half the width, not a full-width box for "4-5 weeks". */
@media (min-width: 48rem) { .oc-quo-half { max-width: calc(50% - .625rem); } }
@media (prefers-reduced-motion: reduce) { .oc-quo-fold-chev { transition: none; } }
.oc-quo-sub { margin: 0; min-width: 0; }
.oc-quo-sub > .field-label { display: block; margin: 0 0 .375rem; }
.oc-quo-sub-hint { display: block; margin: -.125rem 0 .625rem; }
.oc-quo-sub-pair { display: grid; grid-template-columns: minmax(0, 1fr); gap: 1.5rem; }
.oc-quo-sub-pair > .field { margin: 0; }
/* Objectives: hidden behind "+ Add objectives" (the Add tax button, on the right) until opened (by request); the ×
   that clears and hides it again sits on its label line, the list rows' delete look. */
.oc-quo-reveal-row { margin-top: 0; }
.oc-quo-reveal-row[hidden], .oc-quo-revealed[hidden] { display: none; }
.oc-quo-revealed { position: relative; margin: 0; }
.oc-quo-revealed > .oc-quo-rremove { top: 0; right: 0; }
@media (min-width: 48rem) { .oc-quo-sub-pair { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1.25rem; } }
.oc-quo-notes { display: grid; grid-template-columns: minmax(0, 1fr); gap: 1rem; }
.oc-quo-notes .field { margin: 0; }
.oc-quo-notes .field-textarea { min-height: 8rem; resize: vertical; }
/* The quotation page's Terms card: the text in a soft panel, or a quiet "none" line. */
.oc-quo-view-notes { display: grid; grid-template-columns: minmax(0, 1fr); gap: 1rem; }
.oc-quo-view-note { padding: .75rem 1rem; border-radius: 10px; background: var(--line-2); }
.oc-quo-view-note h3 { margin: 0 0 .375rem; font: var(--fl-caption1-strong); color: var(--ink-500); }
.oc-quo-view-note p { margin: 0; font: var(--fl-body1); color: var(--ink-900, inherit); white-space: pre-line; overflow-wrap: anywhere; }
.oc-quo-view-note p.is-empty { color: var(--ink-500); }
.oc-quo-view-note h4 { margin: .5rem 0 .125rem; font: var(--fl-body1-strong); }
.oc-quo-view-list { margin: 0; padding-left: 1.125rem; list-style: disc; font: var(--fl-body1); overflow-wrap: anywhere; }
/* Formatted text on the quotation page (html from the rich-text editor, sanitised): the reset strips list bullets and
   paragraph gaps, so they are put back here. */
.oc-quo-rich { font: var(--fl-body1); color: var(--ink-900, inherit); overflow-wrap: anywhere; }
.oc-quo-rich p { margin: 0 0 .375rem; }
.oc-quo-rich ul { list-style: disc; padding-left: 1.25rem; margin: .25rem 0 .375rem; }
.oc-quo-rich ol { list-style: decimal; padding-left: 1.25rem; margin: .25rem 0 .375rem; }
.oc-quo-rich > :last-child { margin-bottom: 0; }
.oc-quo-rich a { color: var(--brand-600, inherit); text-decoration: underline; }
/* "+ Tax": .btn sets display, which would beat the bare [hidden] while the tax box is open. */
.oc-quo-adjust .btn[hidden] { display: none; }
.oc-quo-actions { display: flex; align-items: center; gap: .25rem; flex-wrap: wrap; }
.oc-quo-shortcuts { display: flex; flex-wrap: wrap; gap: .5rem; }
.oc-quo-title { max-width: 22rem; overflow-wrap: anywhere; }
@media (max-width: 48rem) {
    .oc-quo-notes, .oc-quo-view-notes { grid-template-columns: minmax(0, 1fr); }
}

/* The Quotations list: the table on desktop, compact cards below 64rem (the invoice list's shape). */
.oc-quo-cards { display: none; }
@media (max-width: 64rem) {
    .oc-quo-list > .oc-table-scroll { display: none; }
    .oc-quo-cards { display: block; padding: .75rem; }
    .oc-quo-card { display: flex; flex-direction: column; gap: .25rem; padding: .625rem .75rem; margin-bottom: .5rem;
        border: 1px solid var(--line); border-radius: 10px; background: var(--surface); }
    .oc-quo-card:last-child { margin-bottom: 0; }
    .oc-quo-card-top { display: flex; align-items: center; justify-content: space-between; gap: .5rem; }
    .oc-quo-card-top .oc-link { font: var(--fl-body1-strong); }
    .oc-quo-card-title { font: var(--fl-body1); color: var(--ink-700); overflow-wrap: anywhere; }
    .oc-quo-card-meta { display: flex; align-items: baseline; justify-content: space-between; gap: .5rem; font: var(--fl-caption1);
        color: var(--ink-500); }
    .oc-quo-card-meta .is-danger { color: var(--danger, #b91c1c); }
    /* Body 1 Strong by size only. */
    .oc-quo-card-amount { font-size: .875rem; line-height: 1.25rem; font-weight: 600; color: var(--ink-900, inherit); }
    .oc-quo-card-foot { display: flex; align-items: center; justify-content: space-between; gap: .5rem; font: var(--fl-caption1); }}

/* A required searchable dropdown showed its message TWICE: oc-live-validate.js writes it into the field's own
   .field-error line, and octa-core's octacer-searchable.js adds a second one ([data-oc-client-error]) below it.
   When the field has its own line, that one is the message; core's copy is hidden. A field with no line of its
   own still gets core's (this rule needs the app's .field-error to exist). Shared by Admin and the Portal. */
.field:has(.field-error:not([data-oc-client-error])) [data-oc-client-error] { display: none; }

/* ===== Quotation form: its OWN copy of the invoice builder's layout and pricing rules (by request: quotations and invoices never share
   styles, so changing one cannot change the other). Generated by renaming oc-invoice-* / oc-pricing-* to oc-quo-*.
   Edit THESE for the quotation; the invoice's own rules above are untouched. ===== */
.oc-card.oc-quo-view-billing:has(.oc-table-scroll) { overflow: hidden; }
.oc-quo-view-billing > .oc-table-scroll { border-radius: 0 0 var(--radius-lg) var(--radius-lg); }
/* The quotation page's Pricing items: widths add up to 100% (fixed table layout). The item text gets the room and
   wraps instead of being cut off; number, due date and amount stay on one line. */
.oc-quo-items :is(th, td).oc-quo-items-n { width: 5%; color: var(--ink-500); font-variant-numeric: tabular-nums; }
.oc-quo-items :is(th, td).oc-quo-items-item { width: 75%; white-space: normal; overflow: visible; text-overflow: clip;
    overflow-wrap: anywhere; }
.oc-quo-items :is(th, td).oc-quo-items-amount { width: 20%; text-align: right; white-space: nowrap; }
.oc-quo-items.has-due :is(th, td).oc-quo-items-item { width: 57%; }
.oc-quo-items.has-due :is(th, td).oc-quo-items-due { width: 18%; white-space: nowrap; }
/* A milestone's number before its name (M1, M2 …). */
.oc-quo-ms { display: inline-block; margin-right: .375rem; padding: 0 .375rem; border-radius: 4px; background: var(--ink-900);
    color: #fff; font: var(--fl-caption1-strong); vertical-align: 1px; }
.oc-quo-pricesum { display: flex; flex-direction: column; gap: .45rem; }
.oc-quo-pricesum > div { display: flex; justify-content: space-between; align-items: baseline; gap: 1rem; }
.oc-quo-pricesum dt { color: var(--ink-500); }
.oc-quo-pricesum dd { color: var(--ink-900); font-weight: 600; }
.oc-quo-pricesum .is-total {
    border-top: 1px solid var(--line); padding-top: .55rem; margin-top: .15rem;
}
.oc-quo-pricesum .is-total dt { color: var(--ink-900); font-weight: 600; }
.oc-quo-pricesum .is-total dd { font-weight: 600; }
.oc-quo-layout { border: 0; margin: 0; padding: 0; min-width: 0; display: grid; grid-template-columns: minmax(0, 1fr); gap: 1rem; }
@media (min-width: 64rem) {
.oc-quo-layout { grid-template-columns: minmax(0, 1fr) 20rem; align-items: start; }
.oc-quo-summary { position: sticky; top: 1rem; }
}
.oc-quo-main { display: flex; flex-direction: column; min-width: 0; }
.oc-quo-part { padding: 1.25rem 1.5rem; }
.oc-quo-part + .oc-quo-part { border-top: 1px solid var(--line); }
/* No divider between Timeline and Pricing (by request); Timeline keeps the line above it. */
.oc-quo-part:has(#quo_s_timeline) + .oc-quo-part { border-top: 0; padding-top: .5rem; }
.oc-quo-part-title { margin: 0 0 1rem; display: flex; align-items: center; gap: .375rem; color: var(--ink-900); }
.oc-quo-part .oc-form { padding: 0; }
/* A field inside a grid row already gets the grid's gap; its own bottom margin plus the row's stacked a ~50px empty
   band under the last row of Client & details. Scoped to the quotation form. */
.oc-quo-part .oc-form-grid > .field { margin-bottom: 0; }
.oc-quo-part .oc-form > .oc-form-grid:last-child { margin-bottom: 0; }
.oc-quo-part-title .oc-info-dot { width: 1.125rem; height: 1.125rem; }
.oc-quo-part-title .oc-info-dot > i { width: .75rem; height: .75rem; font-size: .75rem; line-height: 1; }
@media (max-width: 40rem) {
.oc-quo-part { padding: 1rem; }
}
.oc-quo-billbox { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; padding: .625rem .75rem;
    border: 1px solid var(--line); border-radius: 10px; background: var(--line-2); color: var(--ink-700); }
.oc-quo-billbox { padding: .3rem .625rem; border-radius: 8px; }
.oc-quo-billbox-label { display: inline-flex; align-items: center; gap: .375rem; font-weight: 600; color: var(--ink-500); }
.btn.oc-quo-addbtn { display: inline-flex; align-items: center; justify-content: center; gap: .375rem;
    height: 1.875rem; min-height: 0; padding: 0 .625rem; }
:is(.btn.oc-quo-addbtn, .oc-quo-padd-item) > :first-child { flex: none; width: .75rem; height: .75rem;
    font-size: .75rem; line-height: 1; display: inline-grid; place-items: center; }
.oc-quo-part-head { display: flex; align-items: center; justify-content: space-between; gap: .75rem; margin-bottom: 1rem; }
.oc-quo-part-head .oc-quo-part-title { margin: 0; }
.oc-quo-part-head .btn[hidden] { display: none; }
.oc-quo-part[hidden] { display: none; }
.oc-quo-tax-box { margin-top: .75rem; padding: .75rem; border: 1px solid var(--line); border-radius: 10px; }
.oc-quo-tax-box .oc-quo-part-head { margin-bottom: .5rem; }
.oc-quo-tax-box .oc-quo-part-head .field-label { margin: 0; }
.oc-quo-tax-box .oc-form-grid { margin-bottom: 0; }
.oc-quo-tax-box .field { margin: 0; }
.oc-quo-tax-box[hidden] { display: none; }
/* Pricing groups (by request, the invoice's shape): a Billing type dropdown, the group's own total and a delete on
   the head line; then the column labels and the items. A group's data-type picks its columns: Fixed price and
   Milestone are description | due date | amount; Monthly adds Months before the amount (which is then per month). */
.oc-quo-pgroups { display: flex; flex-direction: column; }
.oc-quo-pgroup { padding: 1rem 0; }
.oc-quo-pgroup:first-child { padding-top: 0; }
.oc-quo-pgroup + .oc-quo-pgroup { border-top: 1px solid var(--line-2); }
.oc-quo-pgroup-head { display: flex; align-items: flex-end; gap: .75rem; }
.oc-quo-ptype { width: 14rem; max-width: 100%; margin: 0; }
/* The closed base-select box is inline-flex and stretches its chosen option to the top; on a touch phone the box is
   taller than the text, so "Fixed price" sat high (by request). Centred vertically. */
.oc-quo-ptype .field-select { align-items: center; }
.oc-quo-psubtotal { margin-left: auto; padding-bottom: .5rem; font-variant-numeric: tabular-nums;
    color: var(--ink-900); white-space: nowrap; }
.oc-quo-premove, .oc-quo-pitem-remove {
    display: inline-grid; place-items: center; width: 1.375rem; height: 1.375rem; padding: 0;
    border: 0; border-radius: 6px; background: none; color: var(--ink-400, var(--ink-500)); cursor: pointer; }
.oc-quo-premove { margin-bottom: .5rem; color: var(--danger); }
.oc-quo-pitem-remove { align-self: center; }
:is(.oc-quo-premove, .oc-quo-pitem-remove):hover, :is(.oc-quo-premove, .oc-quo-pitem-remove):focus-visible { color: var(--danger); }
.oc-quo-pitem-remove > i { width: .6875rem; height: .6875rem; font-size: .6875rem; line-height: 1; }
.oc-quo-premove > i { width: .875rem; height: .875rem; font-size: .875rem; line-height: 1; }
.oc-quo-pbody { margin-top: .875rem; }
.oc-quo-pcols, .oc-quo-pitem { display: grid; gap: .5rem .75rem; align-items: start;
    grid-template-columns: minmax(0, 1fr) 10rem 9rem 1.5rem; }   /* description | due date | amount | delete */
.oc-quo-pgroup[data-type="Monthly"] :is(.oc-quo-pcols, .oc-quo-pitem) {
    grid-template-columns: minmax(0, 1fr) 10rem 6rem 9rem 1.5rem; }   /* ... | months | per month | delete */
.oc-quo-pgroup:not([data-type="Monthly"]) .oc-quo-pcols [data-col="qty"] { display: none; }
/* Milestone groups (the invoice's shape): "M" and the milestone number in a field box first. */
.oc-quo-pgroup[data-type="Milestone"] :is(.oc-quo-pcols, .oc-quo-pitem) {
    grid-template-columns: 8rem minmax(0, 1fr) 10rem 9rem 1.5rem; }   /* milestone | name | due | amount | delete */
.oc-quo-pgroup:not([data-type="Milestone"]) [data-col="num"] { display: none; }
/* A Fixed price group is one item (by request): no "+ Add item" — "+ Add pricing" adds another. */
.oc-quo-pgroup[data-type="Fixed"] [data-oc-quo-add] { display: none; }
.oc-quo-pnum-box { display: flex; align-items: center; gap: .5rem; }
.oc-quo-pnum-m { font: var(--fl-body1-strong); color: var(--ink-900); }
.oc-quo-pnum-box .field-input { min-width: 0; cursor: default; background: var(--surface); color: var(--ink-900); }   /* white like the invoice, though never typed */
.oc-quo-pitem [data-col="qty"][hidden] { display: none; }
.oc-quo-pcols { margin-bottom: .375rem; color: var(--ink-500); }
.oc-quo-pitems { display: flex; flex-direction: column; gap: .5rem; }
.oc-quo-pitem .field { margin: 0; min-width: 0; }
.oc-quo-pitem .field-input { width: 100%; }
.oc-quo-pitem .field-error:empty { display: none; }
.oc-quo-pcell-label { display: none; }
.oc-quo-padd-item { display: inline-flex; align-items: center; gap: .375rem; margin-top: .5rem; padding: .25rem 0;
    border: 0; background: none; color: var(--brand-600, var(--ink-700)); cursor: pointer; }
.oc-quo-padd-item:hover { text-decoration: underline; }
.oc-quo-pactions { display: flex; justify-content: flex-end; gap: .5rem; margin-top: .75rem; }
.oc-quo-pactions .btn[hidden] { display: none; }
/* The optional project lists (Scope of work groups, Timeline phases, Payment schedule): each row a light box with
   its fields side by side and the small grey delete in its corner (the items' delete, same look). */
.oc-quo-rlist { display: flex; flex-direction: column; gap: .75rem; }
.oc-quo-rrow { position: relative; display: grid; gap: .75rem; padding: .75rem 2.25rem .75rem .75rem;
    border: 1px solid var(--line); border-radius: 10px; }
.oc-quo-rrow[data-list="Phases"] { grid-template-columns: minmax(0, 1fr) 10rem; }
.oc-quo-rrow .field { margin: 0; min-width: 0; }
.oc-quo-rrow .field-error:empty { display: none; }
.oc-quo-rremove { position: absolute; top: .5rem; right: .5rem; display: inline-grid; place-items: center;
    width: 1.375rem; height: 1.375rem; padding: 0; border: 0; border-radius: 6px; background: none;
    color: var(--ink-400, var(--ink-500)); cursor: pointer; }
.oc-quo-rremove:hover, .oc-quo-rremove:focus-visible { color: var(--danger); }
.oc-quo-rremove > i { width: .6875rem; height: .6875rem; font-size: .6875rem; line-height: 1; }
@media (max-width: 40rem) {
    .oc-quo-rrow[data-list] { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 40rem) {
    /* Phones: the column labels move onto each field; description full width, the rest two by two. */
    .oc-quo-pcols { display: none; }
    .oc-quo-pitem, .oc-quo-pgroup[data-type] .oc-quo-pitem { grid-template-columns: 1fr 1fr;
        padding-bottom: .5rem; border-bottom: 1px dashed var(--line-2); }
    .oc-quo-pitem [data-col="desc"] { grid-column: 1 / -1; }
    .oc-quo-pcell-label { display: block; margin-bottom: .25rem; color: var(--ink-500); }
    /* The item's grey delete sits on its first label's line, on the right ("Milestone" / "Description"), by request —
       not alone on a row of its own under the fields. */
    .oc-quo-pitem { position: relative; }
    .oc-quo-pitem-remove { position: absolute; top: -.125rem; right: 0; }
    /* Milestone items (by request): a narrow Milestone number with Amount beside it, then the name, then the due date. */
    .oc-quo-pgroup[data-type="Milestone"] .oc-quo-pitem { grid-template-columns: 6.5rem minmax(0, 1fr);
        grid-template-areas: "num rate" "desc desc" "due due"; }
    .oc-quo-pgroup[data-type="Milestone"] .oc-quo-pitem > [data-col="num"] { grid-area: num; }
    .oc-quo-pgroup[data-type="Milestone"] .oc-quo-pitem > [data-col="rate"] { grid-area: rate; }
    .oc-quo-pgroup[data-type="Milestone"] .oc-quo-pitem > [data-col="desc"] { grid-area: desc; }
    .oc-quo-pgroup[data-type="Milestone"] .oc-quo-pitem > [data-col="due"] { grid-area: due; }
    /* Billing type, the group's total and its delete stay on one line (by request): the dropdown takes what is left. */
    .oc-quo-pgroup-head { flex-wrap: nowrap; }
    .oc-quo-ptype { flex: 1 1 auto; width: auto; min-width: 0; }
}
.oc-quo-sumtotals { margin: 0; display: flex; flex-direction: column; gap: .375rem; font-variant-numeric: tabular-nums; }
.oc-quo-sumtotals > div { display: flex; justify-content: space-between; gap: .75rem; }
.oc-quo-sumtotals > div[hidden] { display: none; }
.oc-quo-sumtotals dt { color: var(--ink-500); }
.oc-quo-sumtotals dd { margin: 0; color: var(--ink-900); white-space: nowrap; }
.oc-quo-sumtotals .oc-quo-sumtotal { padding-top: .5rem; border-top: 1px solid var(--line); }
.oc-quo-sumactions { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: 1rem; }
.oc-quo-sumactions .btn { flex: 1 1 auto; justify-content: center; }
/* Quotation form on a phone — the Agreements/Proposals phone layout (Option A "Minimal & Calm", the 60rem block
   above; ui-patterns.md "Phone layout for create/edit forms"), by request: one plain card of boxed fields, the
   buttons pinned to the bottom edge above the tab bar with Send as the one wide main button (Save beside it,
   Preview and Download PDF icon-only), and the summary cut to its totals. Desktop is untouched. */
@media (max-width: 60rem) {
    #quotation_form .oc-quo-part { padding: 1rem; }
    #quotation_form .oc-quo-sumnote { display: none; }
    #quotation_form .oc-quo-sumactions {
        position: fixed; left: 0; right: 0; bottom: var(--oc-bottom-chrome, 0px); z-index: 30; margin: 0;
        display: flex; flex-wrap: nowrap; gap: .5rem;
        padding: .75rem 1rem max(.75rem, env(safe-area-inset-bottom));
        background: linear-gradient(to top, var(--bg) 72%, transparent);
    }
    #quotation_form .oc-quo-sumactions .btn { min-height: 2.75rem; }
    #quotation_form .oc-quo-sumactions :is([data-testid="quotation__preview"], [data-oc-quo-download]) { flex: 0 0 auto; padding-inline: .75rem; }
    #quotation_form .oc-quo-sumactions :is([data-testid="quotation__preview"], [data-oc-quo-download]) .oc-btn-label { display: none; }
    #quotation_form .oc-quo-sumactions :is([data-oc-quo-gated="save"], [data-oc-quo-gated="send"]) { flex: 1 1 0; min-width: 0; }
    /* Room under the summary for the pinned bar, so it never covers the last thing on the page. */
    #quotation_form { padding-bottom: 5rem; }
}.oc-quo-view { display: grid; grid-template-columns: minmax(0, 1fr) 22rem; gap: 1.25rem; align-items: start; }
.oc-quo-view-side { position: sticky; top: 1rem; }
@media (max-width: 60rem) {
.oc-quo-view { grid-template-columns: minmax(0, 1fr); }
.oc-quo-view-side { position: static; order: -1; }
}
.oc-quo-view-title { margin-top: .25rem; color: var(--ink-700); }
.oc-quo-view-meta { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem 1rem; margin-top: .625rem;
    color: var(--ink-500); }
.oc-quo-view-meta > span { display: inline-flex; align-items: center; gap: .375rem; }
.oc-quo-view-dl { grid-template-columns: minmax(8rem, auto) 1fr; }
.oc-quo-view-side .oc-form-actions { flex-direction: column; align-items: stretch; gap: .5rem; }
.oc-quo-view-side .oc-form-actions > form { margin: 0; }
.oc-quo-view-side .oc-form-actions :is(.btn, form > .btn) { width: 100%; justify-content: center; }
@media (max-width: 640px) {
.oc-page-head .oc-quo-pageactions { flex-wrap: nowrap; gap: .4rem; }
.oc-page-head .oc-quo-pageactions .oc-btn-label { display: none; }
.oc-page-head .oc-quo-pageactions > .btn, .oc-page-head .oc-quo-pageactions > a.btn, .oc-page-head .oc-quo-pageactions > form > .btn { flex: 0 0 auto; padding-inline: .6rem; }
.oc-page-head .oc-quo-pageactions > form:not(.hidden) { flex: 0 0 auto; }
.oc-page-head .oc-quo-pageactions > [data-testid="invoice__send"] { margin-left: auto; padding-inline: .85rem; }
}
:is(html[data-theme-scope="admin"], html[data-theme-scope="portal"]):not(#fl) {
& main {
& .oc-quo-billbox, & .oc-quo-billbox-label, & .oc-quo-billbox span { font: var(--fl-caption1); }
& .oc-quo-billbox-label { font-weight: 600; }
& .oc-quo-pcols, & .oc-quo-pcols span, & .oc-quo-pcols .oc-quo-ptotal, & .oc-quo-padd-item, & .oc-quo-pcell-label,
& .btn.oc-quo-addbtn { font: var(--fl-caption1-strong); }
/* Section titles inside the form and the page cards -> Subtitle 2. */
& .oc-quo-part-title { font: var(--fl-subtitle2); }
/* Headings inside a part (Project overview, Objectives, Scope of work, Steps) -> Body 1 Strong, one look for all
   (by request: some read bold and some did not). Objectives' heading is its editor's own label. */
& .oc-quo-sub-title, & .oc-quo-revealed > [data-oc-richtext] > .field-label { font: var(--fl-body1-strong); color: var(--ink-900); }
/* The form's summary and the page's price summary: labels and figures Body 1, the total Body 1 Strong. */
& .oc-quo-sumtotals > div, & .oc-quo-pricesum dt, & .oc-quo-view-title, & .oc-quo-view-meta { font: var(--fl-body1); }
& .oc-quo-sumtotals .oc-quo-sumtotal, & .oc-quo-pricesum .is-total dt, & .oc-quo-psubtotal,
& .oc-quo-ptotal output { font: var(--fl-body1-strong); }
/* Hints and notes under the form's buttons. */
& .oc-quo-summary .oc-t-xs, & .oc-quo-part .oc-t-xs { font: var(--fl-caption1); }
/* Typed text in the form's boxes -> Body 1 (14/20): the theme gives text areas 14/21 and the rich-text editor 14/23. */
& .oc-quo-form :is(.field-input, .field-select, .field-textarea, .oc-richtext-input) { font: var(--fl-body1); }
}
}
:is(html[data-theme-scope="admin"], html[data-theme-scope="portal"]):not(#fl) main {
/* The quotation page's figures (by request): Summary amounts are the same 14px as their labels — Body 1 for
   Subtotal and tax, Body 1 Strong for the Total — and the details card's values are Body 1 (the theme's .oc-dl dd
   is weight 500, which the ramp does not use). */
& .oc-quo-pricesum dd, & .oc-quo-pricesum .oc-number { font: var(--fl-body1); }
& .oc-quo-pricesum .is-total dd, & .oc-quo-pricesum .is-total .oc-number { font: var(--fl-body1-strong); }
& .oc-quo-view-dl dd { font: var(--fl-body1); }
& :is(.oc-quo-pricesum dd, .oc-quo-pricesum .oc-number, .oc-quo-pricesum .is-total dd, .oc-quo-pricesum .is-total .oc-number) { font-variant-numeric: tabular-nums; }   /* the font shorthand resets it */
/* The invoice page set in the same roles as the quotation page (by request): the number under the title and the
   details values are Body 1; the Invoice summary and Status amounts are 14px like their labels — Body 1, the last
   (balance / amount due) row Body 1 Strong. Scoped to the invoice page's side column, so the proposal editor's
   price summary is untouched. */
& .oc-inv-view-title, & .oc-inv-view-dl dd { font: var(--fl-body1); }
& .oc-inv-view-side .oc-price-summary :is(dt, dd, .oc-number) { font: var(--fl-body1); font-variant-numeric: tabular-nums; }
& .oc-inv-view-side .oc-price-summary .is-total :is(dt, dd, .oc-number) { font: var(--fl-body1-strong); font-variant-numeric: tabular-nums; }
}
:is(html[data-theme-scope="admin"], html[data-theme-scope="portal"]):not(#fl) {
& .oc-quo-pcols, & .oc-quo-pcols span { text-transform: none; letter-spacing: normal; }
}

/* One font everywhere (by request): numbers, amounts, counts and codes are set in Inter like the rest of the app,
   not the theme's monospaced JetBrains Mono. The theme draws every .oc-number / .oc-mono / code from --font-mono, so
   pointing that token at Inter changes them all in one place (the theme file itself is core-owned and stays as is).
   Digits keep their even widths (tabular-nums, from the theme), so figures still line up in columns. */
:root { --font-mono: var(--font-sans); }
:is(.oc-mono, .oc-number, .oc-code, .font-mono) { letter-spacing: normal; }
/* The Fluent `font:` shorthand on table cells resets font-variant, which dropped the even-width digits there;
   put them back (unlayered with :not(#fl), after the Fluent blocks, so it wins). */
:is(html[data-theme-scope="admin"], html[data-theme-scope="portal"]):not(#fl) :is(.oc-table td, .oc-table th, .oc-number, .oc-mono) {
    font-variant-numeric: tabular-nums; }

/* Portal quotation overview (Areas/Portal/Views/Quotations/Details): the key facts as a strip of tiles with the
   total leading, and the pricing table numbered with its totals in a compact block on the right — by request,
   the plain two-column facts list and full-width totals read as unfinished. Portal-only, this page only. */
html[data-theme-scope="portal"]:not(#fl) main {
    & .oc-pq-facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); margin: 0; }
    & .oc-pq-fact { display: flex; flex-direction: column; gap: .375rem; padding: 1.25rem 1.5rem; min-width: 0; }
    & .oc-pq-fact + .oc-pq-fact { border-left: 1px solid var(--line); }
    & .oc-pq-fact dt { display: flex; align-items: center; gap: .5rem; margin: 0; font: var(--fl-caption1-strong);
        color: var(--ink-500); text-transform: none; letter-spacing: 0; }
    /* Every tile alike (by request: the tinted Total tile read as inconsistent); the total stands out by size only.
       The glyph is centred in its circle: Html.Icon's w-4/h-4 box and the font's line height otherwise push it
       off-centre. */
    & .oc-pq-fact-icon { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto;
        width: 1.75rem; height: 1.75rem; border-radius: 999px; line-height: 1;
        background: color-mix(in srgb, var(--ink-500) 14%, transparent); color: var(--ink-700); }
    & .oc-pq-fact-icon > * { display: block; width: auto; height: auto; margin: 0; line-height: 1; font-size: .75rem; text-align: center; }
    & .oc-pq-fact dd { margin: 0; font: var(--fl-subtitle2); color: var(--ink-900); overflow-wrap: anywhere; }
    & .oc-pq-fact.is-total dd { font: var(--fl-title3); color: var(--ink-900); font-variant-numeric: tabular-nums; }

    & .oc-pq-items { table-layout: fixed; }
    & .oc-pq-items :is(th, td).oc-pq-n { width: 7rem; color: var(--ink-500); }
    & .oc-pq-items :is(th, td).oc-pq-item { white-space: normal; overflow-wrap: anywhere; }
    & .oc-pq-items :is(th, td).oc-pq-due { width: 9rem; white-space: nowrap; }
    & .oc-pq-items :is(th, td).oc-pq-amount { width: 10rem; text-align: right; white-space: nowrap; }
    /* Totals in the table's own foot, labels under Due date and figures under Amount (by request). No row
       stripes or hover on them, and no lines between Subtotal and tax; the grand total gets a rule and a
       larger figure, no tinted box. */
    & .oc-pq-foot :is(td, th) { background: none; border: 0; padding-top: .5rem; padding-bottom: .5rem; font: var(--fl-body1);
        color: var(--ink-700); font-variant-numeric: tabular-nums; }
    & .oc-pq-foot tr:first-child :is(td, th) { padding-top: 1rem; border-top: 1px solid var(--line); }
    & .oc-pq-foot th.oc-pq-foot-label { text-align: left; color: var(--ink-500); white-space: nowrap; }
    & .oc-pq-foot th.oc-pq-foot-label.is-right { text-align: right; }
    & .oc-pq-foot tr:hover :is(td, th) { background: none; }
    & .oc-pq-foot tr.is-total :is(th, td:not(:empty)) { padding-top: .75rem; padding-bottom: 1.25rem; border-top: 1px solid var(--line);
        font: var(--fl-subtitle2); color: var(--ink-900); font-variant-numeric: tabular-nums; }}
@media (max-width: 40rem) {
    html[data-theme-scope="portal"]:not(#fl) main {
        & .oc-pq-fact + .oc-pq-fact { border-left: 0; border-top: 1px solid var(--line); }
        /* Pricing on a phone (by request): not a sideways-scrolling table but one card per item — the number small on
           top, the description with its amount on the right, "Due …" beneath — and the totals as label | figure rows. */
        & .oc-pq-pricing .oc-table-scroll { overflow: visible; }
        & .oc-pq-items, & .oc-pq-items :is(tbody, tfoot) { display: block; width: 100%; min-width: 0; table-layout: auto; }
        & .oc-pq-items thead { display: none; }
        & .oc-pq-items tbody tr { display: grid; grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "n n" "item amount" "due due";
            gap: .125rem .75rem; padding: .875rem 1rem; border-bottom: 1px solid var(--line); }
        & .oc-pq-items tbody td { display: block; width: auto; padding: 0; border: 0; background: none; }
        & .oc-pq-items tbody tr:hover td { background: none; }
        & .oc-pq-items tbody td.oc-pq-n { grid-area: n; width: auto; font: var(--fl-caption1-strong); }
        & .oc-pq-items tbody td.oc-pq-item { grid-area: item; font: var(--fl-body1-strong); color: var(--ink-900); }
        & .oc-pq-items tbody td.oc-pq-amount { grid-area: amount; width: auto; font: var(--fl-body1-strong); color: var(--ink-900); }
        & .oc-pq-items tbody td.oc-pq-due { grid-area: due; width: auto; font: var(--fl-caption1); color: var(--ink-500); }
        & .oc-pq-items tbody td.oc-pq-due::before { content: attr(data-label) " "; }
        & .oc-pq-items tbody td.oc-pq-due:not([data-label]) { display: none; }   /* no date: no "Due —" line */
        & .oc-pq-foot tr { display: flex; justify-content: space-between; gap: .75rem; padding: 0 1rem; }
        & .oc-pq-foot td[colspan] { display: none; }
        & .oc-pq-foot :is(td, th) { display: block; width: auto; padding-inline: 0; }
        & .oc-pq-foot th.oc-pq-foot-label.is-right { text-align: left; }
        & .oc-pq-foot tr:first-child { border-top: 0; }
        & .oc-pq-foot tr:first-child :is(td, th) { border-top: 0; }
        & .oc-pq-foot tr.is-total { border-top: 1px solid var(--line); }
        & .oc-pq-foot tr.is-total :is(th, td:not(:empty)) { border-top: 0; }
    }
}
/* Portal Receipts on a phone (by request): the six-column table scrolled sideways and clipped the file name,
   invoice and status, so each receipt becomes a card — file name with its status, then labelled Invoice /
   Reference / Sent lines, then the actions across the foot. Desktop keeps the table. */
@media (max-width: 40rem) {
    html[data-theme-scope="portal"]:not(#fl) main {
        & .oc-table-scroll:has(> .oc-rc-table) { overflow: visible; }
        & .oc-rc-table, & .oc-rc-table tbody { display: block; width: 100%; min-width: 0; table-layout: auto; }
        & .oc-rc-table thead { display: none; }
        & .oc-rc-table tbody tr { display: grid; grid-template-columns: minmax(0, 1fr) auto;
            grid-template-areas: "file status" "invoice invoice" "ref ref" "sent sent" "actions actions";
            gap: .375rem .75rem; padding: 1rem; border-bottom: 1px solid var(--line); }
        & .oc-rc-table tbody tr:last-child { border-bottom: 0; }
        & .oc-rc-table tbody td { display: block; width: auto; min-width: 0; padding: 0; border: 0; background: none; text-align: left; white-space: normal; }
        & .oc-rc-table tbody tr:hover td { background: none; }
        & .oc-rc-table td.oc-rc-file { grid-area: file; align-self: center; font: var(--fl-body1-strong); color: var(--ink-900); }
        & .oc-rc-table td.oc-rc-file .oc-clamp-1 { overflow-wrap: anywhere; }
        & .oc-rc-table td.oc-rc-status { grid-area: status; justify-self: end; text-align: right; }
        & .oc-rc-table td.oc-rc-invoice { grid-area: invoice; }
        & .oc-rc-table td.oc-rc-invoice > a { font: var(--fl-body1-strong); overflow-wrap: anywhere; }
        & .oc-rc-table td.oc-rc-ref { grid-area: ref; }
        & .oc-rc-table td.oc-rc-ref.is-empty { display: none; }   /* no reference: no "Reference —" line */
        & .oc-rc-table td.oc-rc-sent { grid-area: sent; }
        & .oc-rc-table td:is(.oc-rc-invoice, .oc-rc-ref, .oc-rc-sent) { display: grid; grid-template-columns: 6rem minmax(0, 1fr); gap: .75rem; font: var(--fl-body1); }
        & .oc-rc-table td:is(.oc-rc-invoice, .oc-rc-ref, .oc-rc-sent)::before { content: attr(data-label); font: var(--fl-caption1); color: var(--ink-500); padding-top: .125rem; }
        & .oc-rc-table td.oc-rc-sent > :not(time) { grid-column: 2; }
        & .oc-rc-table td.oc-rc-actions { grid-area: actions; padding-top: .5rem; }
        & .oc-rc-table td.oc-rc-actions .oc-row-actions { display: flex; justify-content: flex-start; gap: .5rem; }
        & .oc-rc-table td.oc-rc-actions .oc-row-actions > a.btn { flex: 1 1 0; justify-content: center; min-height: 2.5rem; }
        & .oc-rc-table td.oc-rc-actions .oc-row-actions > button.btn { flex: 0 0 2.5rem; min-height: 2.5rem; }
    }
}
/* Portal quotation overview: View / Download / Reject / Accept, at the top-right above the Overview card. */
.oc-pq-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: .5rem; }
/* Phones (by request): one line — View, Download PDF and Decline show only their icons, Accept takes the rest. */
@media (max-width: 40rem) {
    .oc-pq-actions { flex-wrap: nowrap; }
    .oc-pq-actions > .btn { min-height: 2.5rem; }
    .oc-pq-actions > :is([data-testid="quotation__view"], [data-testid="quotation__download"], [data-testid="quotation__reject"]) { flex: 0 0 auto; padding-inline: .75rem; }
    .oc-pq-actions > :is([data-testid="quotation__view"], [data-testid="quotation__download"], [data-testid="quotation__reject"]) .oc-btn-label { display: none; }
    .oc-pq-actions > [data-testid="quotation__accept"] { flex: 1 1 auto; justify-content: center; }
}

/* Admin dialogs on a phone (by request): octa-core docks every modal to the bottom edge as a sheet with only
   its top corners rounded, so confirm dialogs (archive/delete client, delete proposal/agreement…) read as a
   half-rounded box. Here the sheet floats a gutter above and beside the edges, rounded on all four corners
   to the same radius as its top, with the safe-area inset kept below it rather than inside it. Admin only;
   the portal keeps core's flush sheet. Core's own rule is left untouched (octacer-theme.css is core-owned). */
@media (max-width: 639px) {
    html[data-theme-scope="admin"] .oc-modal:not([data-oc-mobile-sheet="false"]) {
        padding: 0 .75rem calc(.75rem + var(--oc-safe-bottom, env(safe-area-inset-bottom, 0px)));
    }
    html[data-theme-scope="admin"] .oc-modal:not([data-oc-mobile-sheet="false"]) > .oc-modal-panel {
        border-radius: var(--radius-2xl);
        border-bottom: 1px solid var(--line);
        padding-bottom: 0;
        max-height: calc(92dvh - .75rem);
    }
}
