/* ===========================================================================
   Ten Figure CRM — application chrome.

   site.css carries the brand: colours, type, buttons, form inputs, cards.
   It is a marketing stylesheet though — bands, heroes, testimonials — and a
   dense working screen is not a landing page. Everything here is what the
   marketing site does not have: an app header, a toolbar, a data table, a
   badge, an empty state.

   Every value below comes from site.css's :root tokens. Nothing redefines a
   colour, so the CRM stays visually identical to the site by construction
   rather than by anyone remembering to keep two palettes in step.
   =========================================================================== */

body.app {
    background: var(--paper-soft);
    display: block;
    min-height: 100vh;
}

/* ===== App header — the working equivalent of .site-nav ===== */

.app-header {
    background: var(--navy);
    color: var(--white);
    display: flex;
    align-items: center;
    gap: 24px;
    padding: 0 24px;
    height: 60px;
    position: sticky;
    top: 0;
    z-index: 10;
}

.app-header .brand img { height: 26px; display: block; }

.app-header nav {
    display: flex;
    gap: 4px;
    margin-left: 8px;
}

.app-header nav a {
    color: var(--muted);
    padding: 8px 14px;
    border-radius: 4px;
    font-size: 0.92rem;
    font-weight: 500;
}

.app-header nav a:hover {
    color: var(--white);
    background: rgba(255, 255, 255, 0.06);
    text-decoration: none;
}

.app-header nav a[aria-current="page"] {
    color: var(--white);
    background: rgba(255, 255, 255, 0.10);
}

.app-header .spacer { flex: 1; }

/* ===== Page frame ===== */

.app-main {
    max-width: 1180px;
    margin: 0 auto;
    padding: 32px 24px 64px;
}

/* The board is the one page that wants the whole monitor. 1180px is a good
   measure for reading a deal; it is the wrong shape for six columns side by
   side, which it boxed into a horizontal scrollbar on a screen with room to
   spare. Opt-in per page rather than raising the global cap, because every
   other page still wants the narrow measure. */
.app-main--wide {
    max-width: none;
    padding-left: 32px;
    padding-right: 32px;
}

.page-head {
    display: flex;
    align-items: baseline;
    gap: 16px;
    margin-bottom: 20px;
}

.page-title {
    font-size: 1.6rem;
    letter-spacing: -0.01em;
    margin: 0;
}

.page-count {
    color: var(--muted-ink);
    font-size: 0.92rem;
}

/* ===== Toolbar ===== */

.toolbar {
    display: flex;
    gap: 12px;
    align-items: center;
    margin-bottom: 18px;
    flex-wrap: wrap;
}

.toolbar .search {
    flex: 1;
    min-width: 220px;
    max-width: 420px;
    position: relative;
}

.toolbar .search input {
    width: 100%;
    padding: 11px 14px;
    background: var(--paper);
    border: 1px solid var(--line-light);
    border-radius: 4px;
    color: var(--ink);
    font: inherit;
}

.toolbar .search input:focus {
    outline: 0;
    border-color: var(--gold-ink);
}

/* ===== Data table =====
   A surface, not a grid of boxes: one card-like panel with hairline rows,
   which reads better than .card repeated per row when scanning many records. */

.panel {
    background: var(--paper);
    border: 1px solid var(--line-light);
    border-radius: 10px;
    overflow: hidden;
}

table.data {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.94rem;
}

table.data thead th {
    text-align: left;
    font-size: 0.74rem;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: var(--muted-ink);
    font-weight: 700;
    padding: 12px 18px;
    background: var(--paper-soft);
    border-bottom: 1px solid var(--line-light);
    white-space: nowrap;
}

table.data tbody td {
    padding: 13px 18px;
    border-bottom: 1px solid var(--line-light);
    vertical-align: middle;
}

table.data tbody tr:last-child td { border-bottom: 0; }
table.data tbody tr:hover { background: var(--paper-soft); }

table.data .name {
    font-weight: 600;
    color: var(--ink);
}

table.data .secondary {
    display: block;
    color: var(--muted-ink);
    font-size: 0.85rem;
    font-weight: 400;
}

table.data .muted { color: var(--muted-ink); }
table.data .nowrap { white-space: nowrap; }

/* ===== Type badge ===== */

.badge {
    display: inline-block;
    font-size: 0.72rem;
    font-weight: 700;
    letter-spacing: 0.07em;
    text-transform: uppercase;
    padding: 4px 9px;
    border-radius: 999px;
    white-space: nowrap;
}

.badge--person {
    background: var(--accent-soft);
    color: var(--gold-ink);
}

.badge--organization {
    background: var(--accent-soft-teal);
    color: #0f7f6b; /* darkened --teal for contrast on its own soft background */
}

.badge--proceed {
    background: var(--accent-soft-teal);
    color: #0f7f6b; /* darkened --teal for contrast on its own soft background */
}

/* .badge--manual_review doubles as the MANUAL_REVIEW status badge (same
   lowercased name, see opportunity.html) -- intentional, not a collision;
   gold reads correctly as "needs attention" for both. */
.badge--stipulation,
.badge--manual_review {
    background: var(--accent-soft);
    color: var(--gold-ink);
}

/* .badge--decline is the decision-outcome badge; .badge--declined is the
   status badge a declined deal's OpportunityStatus renders (see
   opportunity.html). Same rule for both so the two badges agree on the
   same screen instead of the status badge falling back to unstyled. */
.badge--decline,
.badge--declined {
    background: #fdecec; /* no red exists in the brand palette; softest tint that
                            still reads as a stop against --paper */
    color: #9b1c1c;
}

/* AI-underwriting decision badges (aiVerdictPanel, opportunity.html). The
   verdict schema's four decisions are FUND, DECLINE, COME_BACK and
   INCOMPLETE_FILE, lowercased and hyphenated the same way stepBadgeClass()
   does. DECLINE needs no rule of its own -- it coincidentally lowercases to
   the same class name as .badge--decline above and reads correctly reused,
   same idea as .badge--manual_review below. The other three had no rule at
   all before this block, so they rendered as blank pills (.badge supplies
   only shape and typography, no colour). */
.badge--fund {
    background: var(--accent-soft-teal);
    color: #0f7f6b; /* darkened --teal for contrast on its own soft background */
}
.badge--come-back {
    background: var(--accent-soft);
    color: var(--gold-ink);
}
.badge--incomplete-file {
    /* A refusal on missing data, not a verdict on the deal -- same neutral
       grey as .badge--not-run/.badge--not-available below. */
    background: #eef1f5;
    color: var(--muted-ink);
}

/* Title, outcome badge and timestamp on one line. */
.decision-head {
    display: flex;
    align-items: baseline;
    gap: 12px;
    margin-bottom: 10px;
}

/* ===== States ===== */

.state {
    padding: 56px 24px;
    text-align: center;
    color: var(--muted-ink);
}

.state h2 {
    margin: 0 0 6px;
    font-size: 1.05rem;
    color: var(--ink);
}

.state p { margin: 0; font-size: 0.92rem; }

.state--error { color: #9b1c1c; }
.state--error h2 { color: #9b1c1c; }

/* ===== Small screens ===== */

@media (max-width: 720px) {
    .app-header { gap: 12px; padding: 0 14px; }
    .app-header nav a { padding: 8px 10px; }
    .app-main { padding: 20px 14px 48px; }
    table.data thead { display: none; }
    table.data tbody td { display: block; border-bottom: 0; padding: 4px 16px; }
    table.data tbody tr {
        display: block;
        padding: 12px 0;
        border-bottom: 1px solid var(--line-light);
    }
}

/* ===== Form page ===== */

.crumb { margin: 0 0 12px; font-size: 0.9rem; }

.panel--pad { padding: 24px; }

/* form.tff-form from site.css styles the inputs; these are the layouts it lacks. */
.field-row {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 16px;
    margin-bottom: 14px;
}
.field-row.three { grid-template-columns: repeat(3, 1fr); }
.field-row.four  { grid-template-columns: repeat(4, 1fr); }

@media (max-width: 720px) {
    .field-row, .field-row.three, .field-row.four { grid-template-columns: 1fr; }
}

.form-actions {
    display: flex;
    gap: 10px;
    align-items: center;
    margin-top: 24px;
    padding-top: 20px;
    border-top: 1px solid var(--line-light);
}

/* The row of actions at the head of a record -- distinct from .form-actions,
   which closes a form. Same idiom, looser gap, and it wraps.

   The wrap matters more than it looks. These buttons used to be appended
   straight into .panel as bare inline-blocks with no whitespace between them,
   and inline-block siblings can only break at whitespace: with none, the row
   was one unbreakable run that .panel's `overflow: hidden` clipped instead of
   wrapping. That is why the buttons touched and why the last one looked cut
   off. Flex with a gap fixes both -- the gap cannot collapse, and wrapping is
   no longer conditional on the markup happening to contain spaces. */
.panel-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
    align-items: center;
    margin-top: 20px;
    padding-top: 18px;
    border-top: 1px solid var(--line-light);
}

.btn--small { padding: 7px 14px; font-size: 0.85rem; }

.gate-fix { display: flex; gap: 8px; align-items: center; margin-top: 6px; }
.gate-fix input { flex: 1; min-width: 0; }

/* Same idiom contacts.html and opportunities.html each already define inline;
   kept here instead since opportunity.html has no <style> block of its own. */
.visually-hidden {
    position: absolute; width: 1px; height: 1px;
    margin: -1px; padding: 0; overflow: hidden;
    clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ===== Inline notices ===== */

.notice {
    padding: 11px 16px;
    border-radius: 6px;
    margin-bottom: 16px;
    font-size: 0.92rem;
    border: 1px solid transparent;
}
.notice--ok {
    background: var(--accent-soft-teal);
    border-color: #b9e6da;
    color: #0f7f6b;
}
.notice--error {
    background: #fdecec;
    border-color: #f5c2c2;
    color: #9b1c1c;
}
.notice--stale {
    background: #fff6e5;
    border-color: #f0dba6;
    color: #a15c00;
}

/* ===== Repeatable phone rows ===== */

.phone-row {
    grid-template-columns: 140px 1fr 220px;
    align-items: center;
    margin-bottom: 10px;
}

.phone-primary {
    display: flex;
    align-items: center;
    gap: 12px;
}

/* form.tff-form forces inputs to 100% width; a radio must not stretch. */
form.tff-form .phone-primary input[type="radio"] { width: auto; margin: 0 6px 0 0; }

label.inline {
    display: inline-flex;
    align-items: center;
    margin: 0;
    font-size: 0.9rem;
    color: var(--muted-ink);
    white-space: nowrap;
}

@media (max-width: 720px) {
    .phone-row { grid-template-columns: 1fr; }
}

/* --- opportunities board --------------------------------------------- */
/* `safe center`, not plain `center`. A centred flex row whose content overflows
   pushes the first item off the left edge, and an overflow scroller cannot
   scroll back to it -- the leftmost column becomes unreachable. The `safe`
   keyword centres while there is room and falls back to flex-start once there
   is not, which is exactly the behaviour wanted on a board whose column count
   changes with the deals in it. */
.board { display: flex; gap: 1rem; align-items: flex-start; justify-content: safe center;
    overflow-x: auto; padding-bottom: 1rem; }
/* Grow into whatever width is going, instead of sitting at a fixed 17rem and
   scrolling. The basis keeps the old width as the starting point and the min
   stops a column collapsing so far that a merchant name wraps to three lines;
   the max stops two columns sprawling across a wide monitor. `overflow-x` on
   .board stays as the fallback for when there genuinely are more columns than
   fit. */
.board-col { flex: 1 1 17rem; min-width: 15rem; max-width: 24rem; }
.board-col__head { display: flex; justify-content: space-between; align-items: baseline;
                   margin-bottom: .5rem; font-weight: 600; font-size: .8rem;
                   text-transform: uppercase; letter-spacing: .04em; }
.board-col__count { opacity: .6; font-weight: 400; }
.board-col__more { margin-top: .5rem; font-size: .8rem; opacity: .7; }
.board-col__more-btn { margin-top: .5rem; width: 100%; font-size: .8rem; padding: .4rem .6rem;
    cursor: pointer; }

.card { display: block; padding: .7rem .8rem; margin-bottom: .5rem; border-radius: 6px;
        border: 1px solid rgba(0,0,0,.12); border-left: 3px solid transparent;
        background: #fff; text-decoration: none; color: inherit; }
.card:hover { border-color: rgba(0,0,0,.3); }
.card--focus { border-left-color: #c2410c; background: #fff8f4; }
.card__name { font-weight: 600; margin-bottom: .15rem; }
.card__meta { font-size: .78rem; opacity: .7; display: flex; gap: .6rem; flex-wrap: wrap; }
.card__flag { color: #c2410c; font-weight: 600; }

/* --- deal view -------------------------------------------------------- */
.data-table { width: 100%; border-collapse: collapse; font-size: .82rem; }
.data-table th { text-align: right; font-weight: 600; padding: .35rem .5rem;
                 border-bottom: 1px solid rgba(0,0,0,.15); white-space: nowrap; }
.data-table td { text-align: right; padding: .3rem .5rem;
                 border-bottom: 1px solid rgba(0,0,0,.06); }
.data-table th:first-child, .data-table td:first-child { text-align: left; }
.data-table tr.is-partial td { opacity: .55; font-style: italic; }
/* The offer rows on the tier the deal actually graded to. The other six are
   shown too and remain selectable -- see offersTable in opportunity.html. */
.data-table tr.is-derived-grade td { font-weight: 600; }
.table-scroll { overflow-x: auto; }
.kv { display: grid; grid-template-columns: 12rem 1fr; gap: .3rem 1rem; font-size: .85rem; }
.kv dt { opacity: .65; }
.kv dd { margin: 0; }
.kv--stale { opacity: .45; }

/* --- underwriting step override window (opportunity.html) ------------
   opportunity.html has no <style> block of its own (see the .visually-hidden
   note above), so this lives here, same as everything else the page needs. */
.modal-backdrop { position: fixed; inset: 0; background: rgba(11, 37, 69, .45);
                   display: flex; align-items: center; justify-content: center; z-index: 50; }
.modal { background: var(--paper); border: 1px solid var(--line-light);
         padding: 1.5rem; border-radius: .5rem;
         max-width: 46rem; width: 92%; max-height: 86vh; overflow-y: auto; }
/* Four columns for the header line only. The earlier six-column version put
   detail, the checkbox and a 14rem reason box on one line: 28rem of fixed
   columns plus gaps plus the reason box needed more width than the modal's
   43rem content box, so the last two columns were pushed past its edge and the
   checkbox rendered but could not be reached. Detail and reason now take their
   own full-width rows instead. */
.step-row { display: grid; grid-template-columns: 4.5rem 1fr 9rem auto;
            gap: .35rem .5rem; align-items: center; padding: .5rem 0;
            border-bottom: 1px solid var(--line-light); }
.step-detail { grid-column: 1 / -1; }
.step-override { display: inline-flex; align-items: center; gap: .35rem;
                 white-space: nowrap; font-size: .82rem; color: var(--muted-ink); }
.modal-actions { display: flex; gap: .5rem; margin-top: 1rem; }
.modal-result { margin: .75rem 0; }
.modal-result:empty { display: none; }
.step-key { color: var(--muted-ink); font-size: .82rem; }
.step-reason { grid-column: 1 / -1; width: 100%; box-sizing: border-box; }
.warn { color: #a15c00; }
/* "An override cleared this" -- sits inside the detail cell rather than taking
   a seventh grid column, which would wrap and shift the row. */
.step-cleared { color: #1a6b3c; }
/* Errors raised while the modal is open. #banner is an in-flow div at the top
   of the document and .modal-backdrop is fixed at z-index 50 over all of it, so
   a refusal written to the banner is painted behind the overlay and never seen.
   Empty until something fails, so it takes no space in the ordinary case. */
.modal-error:empty { display: none; }
.modal-error { margin: .75rem 0; }
@media (max-width: 700px) { .step-row { grid-template-columns: 1fr; } }

/* Prepare-agreement modal (opportunity.html's openAgreementWindow). Its rows
   carry one label and one input, not a checkbox/badge/reason -- reusing
   .step-row here (review-round IMPORTANT 4) crammed a label up to 39
   characters ("Purchase amount (receivables purchased)") into that grid's
   fixed 4.5rem first column while its other two columns sat empty. Two
   columns instead of four, the first sized to the label's content. */
.agreement-row { display: grid; grid-template-columns: minmax(13rem, 1fr) 2fr;
                 gap: .35rem .75rem; align-items: center; padding: .35rem 0; }
.agreement-row input { width: 100%; box-sizing: border-box; }
/* Special Conditions is the one multi-line box in this window. It sits on the
   same two-column grid, but its label belongs at the top of a four-row
   textarea rather than vertically centred beside it. */
.agreement-row textarea { width: 100%; box-sizing: border-box; resize: vertical;
                          font: inherit; }
.agreement-row--tall { align-items: start; }
@media (max-width: 700px) { .agreement-row { grid-template-columns: 1fr; } }

/* Step-state badges, so the seven states read differently at a glance.
   .badge--declined already exists above (decision-outcome/status red) and is
   reused as-is since stepBadgeClass() lowercases DECLINED the same way. */
.badge--passed { background: var(--accent-soft-teal); color: #0f7f6b; }
.badge--passed-with-condition { background: var(--accent-soft); color: var(--gold-ink); }
.badge--needs-review { background: #fff1d6; color: #8a5a00; }
.badge--in-progress { background: #e6f0fc; color: #14529e; }
/* QUEUED shares NOT_RUN/NOT_AVAILABLE's muted grey on purpose: all three mean
   "nothing to look at here right now" to a reader's eye, even though QUEUED's
   own meaning ("still coming") is the opposite of NOT_RUN's ("never will").
   What matters visually is that it reads quieter than .badge--in-progress --
   giving a queued step the same blue as the one actually on the wire is what
   makes a reader think five vendors are being called at once. */
.badge--not-run,
.badge--not-available,
.badge--queued { background: #eef1f5; color: var(--muted-ink); }

/* The KYC tab's Middesk panel (opportunity.html middeskPanel). Verification
   states first: VERIFIED reads like a pass, PARTIAL like a caution, and
   NOT_VERIFIED like a decline, reusing the same three colours the step badges
   already teach on the underwriting tab. */
.badge--verified { background: var(--accent-soft-teal); color: #0f7f6b; }
.badge--partial { background: #fff1d6; color: #8a5a00; }
.badge--not-verified { background: #fde8e8; color: #9b1c1c; }
.badge--unknown { background: #eef1f5; color: var(--muted-ink); }

/* Searched-record states. CLEAR and NOT-ORDERED are deliberately not both
   grey: "we searched and found nothing" and "nobody ever looked" render as
   the same empty row otherwise, and reading the second as the first turns an
   unasked question into an all-clear on exactly the records -- liens,
   judgments, bankruptcies -- where that mistake costs the most. CLEAR is
   given the same teal as a pass because a clean search IS a result. */
.badge--clear { background: var(--accent-soft-teal); color: #0f7f6b; }
.badge--found { background: #fff1d6; color: #8a5a00; }
.badge--pending { background: #e6f0fc; color: #14529e; }
.badge--not-ordered {
    background: repeating-linear-gradient(
        45deg, #eef1f5, #eef1f5 4px, #e3e7ee 4px, #e3e7ee 8px);
    color: var(--muted-ink);
}

.middesk-actions { display: flex; gap: .5rem; margin-bottom: .75rem; }

/* The Credit tab (opportunity.html creditPanel). The score is the thing the
   reader came for, so it is set large -- but never on its own: the model sits
   beside it because an Intelliscore (business, 1-100) and a VantageScore
   (consumer, 300-850) share no range, and a bare number invites a comparison
   between them that means nothing. */
.credit-block { margin: .25rem 0 .75rem; }
.credit-latest { display: flex; align-items: baseline; gap: .6rem; flex-wrap: wrap; }
.credit-score { font-size: 1.75rem; font-weight: 700; line-height: 1.1; }
.credit-model { font-size: .875rem; }
.credit-when { font-size: .8125rem; }
.credit-owner { margin: .75rem 0 .25rem; font-size: .9375rem; font-weight: 600; }
.credit-history { margin-top: .4rem; }
.credit-history summary { cursor: pointer; font-size: .875rem; color: var(--gold-ink); }
.credit-history-row { font-size: .8125rem; padding: .15rem 0 .15rem .75rem; }

/* DataMerch notes (opportunity.html screeningPanel). A note is a paragraph
   somebody wrote, not a field, so it gets a readable block rather than a
   table cell -- and its category and author sit above it, because "who
   reported this" is half of what the note is worth. */
.note-row {
    border-left: 3px solid var(--line-light);
    padding: .35rem 0 .35rem .75rem;
    margin: .5rem 0;
}
.note-meta {
    display: flex;
    gap: .6rem;
    align-items: baseline;
    flex-wrap: wrap;
    font-size: .8125rem;
}
.note-category { font-weight: 600; }
.note-text { margin: .2rem 0 0; line-height: 1.45; }

/* The screening states, reusing the KYC panel's meaning: a searched-and-clean
   result reads as a pass, a record on file as a caution, and never-screened
   as the same hatched grey that says "nobody asked" there. */
.badge--reported { background: #fff1d6; color: #8a5a00; }
.badge--not-screened {
    background: repeating-linear-gradient(
        45deg, #eef1f5, #eef1f5 4px, #e3e7ee 4px, #e3e7ee 8px);
    color: var(--muted-ink);
}

/* Label, badge, then the detail on its own full-width line. The detail is the
   reason the panel exists -- the address that failed delivery, the case name
   behind a judgment -- so it gets room to wrap rather than a truncating
   third column. */
.middesk-row {
    display: grid;
    grid-template-columns: minmax(9rem, max-content) max-content;
    gap: .35rem .75rem;
    align-items: center;
    padding: .4rem 0;
    border-bottom: 1px solid var(--line-light);
}
.middesk-row:last-child { border-bottom: none; }
.middesk-label { font-weight: 600; }
.middesk-detail { grid-column: 1 / -1; font-size: .875rem; line-height: 1.4; }
.middesk-detail:empty { display: none; }

/* aiVerdictPanel (opportunity.html). A decisive ground is the thing a reader
   is meant to see first, so it gets its own left-accent-stripe box rather
   than reading as a plain paragraph; teal rather than amber since this is
   cited support, not a caution. The disclosure below it just needs enough
   spacing to read as one more panel section instead of raw <details>
   defaults. */
.ai-ground {
    background: var(--accent-soft-teal);
    border-left: 3px solid var(--teal);
    padding: .5rem .75rem;
    margin: .5rem 0;
}
.ai-ground p { margin: 0 0 .2rem; }
.ai-ground p:last-child { margin-bottom: 0; }
.ai-rest { margin-top: .75rem; }
.ai-rest summary { cursor: pointer; font-weight: 600; color: var(--gold-ink); }
.ai-rest h3 { margin: .75rem 0 .25rem; }
.ai-rest p { margin: 0 0 .35rem; }

/* The gated-integrity notice (opportunity.html's aiVerdictPanel). Deliberately
   not .warn: that class is shared with the step-override window and the
   agreement-send vendor-uncertainty notice, both already shipped, and this
   block treatment is a separate visual change for those, made without
   anyone looking at the page. Under a closed integrity gate the agent could
   cite no cash-flow ground at all, so this cannot read as one more muted
   line -- the background and border-left are what .warn's plain colour
   change would not give it. */
.ai-gated {
    color: #8a4b08;
    background: #fdf3e7;
    border-left: 3px solid #c2751a;
    padding: .5rem .75rem;
    margin: .5rem 0;
}
/* ===== Opportunity page tab strip (opportunity.html render()) =========
   The header panel (reference, source, status, actions) stays above this --
   it is the deal's identity, not one facet of it. Below it, four tabs share
   one .tab-panel container whose children are replaced on switch; the
   selected tab lives in location.hash so a reload or back/forward keeps it. */
.tab-strip {
    display: flex;
    gap: 4px;
    margin: 20px 0 0;
    border-bottom: 1px solid var(--line-light);
}

.tab-btn {
    background: transparent;
    border: 0;
    border-bottom: 2px solid transparent;
    padding: 10px 16px;
    margin-bottom: -1px;
    font: inherit;
    font-weight: 600;
    font-size: 0.9rem;
    color: var(--muted-ink);
    cursor: pointer;
}

.tab-btn:hover { color: var(--ink); }

.tab-btn[aria-selected="true"] {
    color: var(--gold-ink);
    border-bottom-color: var(--gold-ink);
}

.tab-panel { margin-top: 20px; }
.tab-panel > .panel { margin-bottom: 20px; }
.tab-panel > .panel:last-child { margin-bottom: 0; }

/* ---- sign-in ---------------------------------------------------------- */

/* The sign-out control auth.js appends to .app-header on every operator page. */
.auth-control {
  display: flex;
  align-items: center;
  gap: .6rem;
  margin-left: auto;
  font-size: .85rem;
}

.auth-user {
  opacity: .85;
  white-space: nowrap;
}

.auth-signout {
  background: transparent;
  border: 1px solid currentColor;
  border-radius: 4px;
  color: inherit;
  cursor: pointer;
  font: inherit;
  padding: .2rem .6rem;
}

.auth-signout:hover { background: rgba(255, 255, 255, .12); }

/* index.html and plaid-link.html have no .app-header to sit in. */
.auth-control-floating {
  position: fixed;
  top: .75rem;
  right: 1rem;
  z-index: 50;
  background: #12263a;
  color: #fff;
  padding: .35rem .6rem;
  border-radius: 6px;
}

/* ===== Sign-in ==========================================================

   The one page reached before there is a session, and the only one that was
   still unbranded: it loaded this file without site.css, so every var() below
   resolved to nothing and the page fell back to Times New Roman on white.
   Both stylesheets are linked now, in the same order as every other page.

   No .app-header. There is no navigation to offer someone who is not signed
   in, and a bar holding nothing but a logo is a header pretending to be one.
   The logo lockup carries its own navy plate, so it reads as a brand mark on
   the soft-blue ground without a band behind it.

   The form is form.tff-form, so site.css styles the inputs -- same division of
   labour as the rest of this file. What is here is the centring, the card and
   the error block, which the marketing stylesheet has no equivalent of.
   ======================================================================== */

body.login-page {
    background: var(--paper-soft);
    display: flex;
    flex-direction: column;
    justify-content: center;
    min-height: 100vh;
    padding: 24px;
}

main.login {
    width: 100%;
    max-width: 25rem;
    margin: 0 auto;
    /* Optically centred. A sign-in card sits better a little above the midline
       than exactly on it; the margin is bottom-side so a short window runs out
       of room at the bottom rather than clipping the logo off the top. */
    margin-bottom: 6vh;
}

.login-brand {
    display: block;
    height: 68px;
    margin: 0 auto 26px;
}

main.login .panel {
    padding: 32px;
}

main.login h1 {
    font-size: 1.35rem;
    letter-spacing: -0.01em;
    margin: 0;
}

.login-intro {
    color: var(--muted-ink);
    font-size: 0.9rem;
    margin: 6px 0 0;
}

/* .tff-form's own top margin is tuned for a marketing page under a heading
   block; a card wants less air between the intro line and the first label. */
main.login form.tff-form { margin-top: 22px; }

main.login form.tff-form input:focus {
    /* site.css already moves the border to --gold-ink. The ring is added
       because this field is the only thing on the screen: on a dense CRM form
       a border change is enough to place the cursor, on an empty page it is
       easy to miss entirely. */
    box-shadow: 0 0 0 3px var(--accent-soft);
}

main.login button[type="submit"] {
    width: 100%;
    margin-top: 6px;
}

main.login button[disabled] { opacity: .6; cursor: progress; }

/* Tinted block rather than a bare red string. A wrong password is the whole
   content of the page at that moment and needs to read as a result, not as
   page furniture. Colours are .badge--decline's -- the palette has no red, and
   these two are already the established "stop" pair in this file.
   Scoped to main.login: index.html carries an #error of its own. */
main.login #error {
    margin: 20px 0 0;
    padding: 12px 14px;
    border-radius: 4px;
    background: #fdecec;
    border: 1px solid #f6cfcf;
    color: #9b1c1c;
    font-size: 0.88rem;
    line-height: 1.5;
}

.login-foot {
    color: var(--muted-ink);
    font-size: 0.8rem;
    text-align: center;
    margin: 22px 0 0;
}

@media (max-width: 420px) {
    main.login .panel { padding: 24px 20px; }
}
