/*
 * Customer Portal styles.
 *
 * Scoped to .portal-* so nothing here can reach a staff page, and vice versa.
 * Written mobile-first: the borrower on a 5-inch phone is the primary case,
 * desktop is the adaptation.
 *
 * SELF-CONTAINED, ON PURPOSE. The portal does NOT load Bootstrap.
 *
 * Bootstrap's stylesheet is 233 KB and this portal used about twenty classes
 * from it - btn, a few flex utilities, a two-column row and some margins. With
 * Bootstrap and its bundle the dashboard weighed 325 KB against a 200 KB
 * budget; reimplementing those twenty rules below brings the whole page,
 * styles included, to roughly 16 KB. On prepaid mobile data that is the
 * difference between a portal people use and one they abandon.
 *
 * Icons are inlined SVG from portal/_icons.php - no icon font. Bootstrap Icons
 * costs 100 KB of CSS plus a 130 KB woff2 to draw the ten shapes this portal
 * uses; inlined they are roughly 250 bytes each, need no extra request, and
 * cannot flash unstyled while a font loads. The page makes ZERO external
 * requests as a result, which is what makes it work on a poor connection.
 *
 * The small utility set below is deliberately Bootstrap-compatible in NAME, so
 * the markup reads the same as the rest of the codebase and could be moved back
 * onto Bootstrap without touching a template.
 */

/*
 * border-box, scoped to the portal's own body class so it cannot reach a staff
 * page.
 *
 * This was missing entirely, and several rules here set `width: 100%` on an
 * element that also has padding - .portal-container, .w-100, and every input in
 * .portal-filter. Under the default content-box that computes to 100% PLUS the
 * padding, which is wider than the parent: the page scrolls sideways. It was
 * already doing this on a phone, where the container's old max-width never
 * bound; making the portal full-bleed simply made it visible at every width.
 *
 * Bootstrap ships the same reset globally, which is why no other page in the
 * app has this problem - the portal deliberately does not load Bootstrap, so it
 * has to carry the rule itself.
 */
.portal-body,
.portal-body *,
.portal-body *::before,
.portal-body *::after { box-sizing: border-box; }

/* ---------- minimal utility layer (Bootstrap-compatible names) ---------- */
.d-flex { display: flex; }
.d-block { display: block; }
.flex-fill { flex: 1 1 auto; }
.justify-content-between { justify-content: space-between; }
.align-items-center { align-items: center; }
.text-center { text-align: center; }
.w-100 { width: 100%; }
.fs-2 { font-size: 1.5rem; }

.gap-2 { gap: 0.5rem; }
.mb-0 { margin-bottom: 0; }
.mb-1 { margin-bottom: 0.25rem; }
.mb-2 { margin-bottom: 0.5rem; }
.mb-3 { margin-bottom: 1rem; }
.mt-2 { margin-top: 0.5rem; }
.mt-3 { margin-top: 1rem; }

.row { display: flex; flex-wrap: wrap; }
.row.g-2 { gap: 0.5rem; }
.row.g-2 > [class^="col-"] { flex: 1 1 0; }
.col-6 { flex: 1 1 0; min-width: 0; }

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.4rem;
    /* 44px minimum height - anything smaller and thumbs miss it. */
    min-height: 2.75rem;
    padding: 0.5rem 1rem;
    border-radius: 0.4rem;
    border: 1px solid transparent;
    font-size: 0.95rem;
    font-weight: 600;
    text-decoration: none;
    cursor: pointer;
    background: none;
}
.btn:focus-visible { outline: 2px solid var(--portal-brand); outline-offset: 2px; }

.btn-primary { background: var(--portal-brand); border-color: var(--portal-brand); color: #fff; }
.btn-primary:hover { background: var(--portal-brand-dark); border-color: var(--portal-brand-dark); color: #fff; }

.btn-outline-primary { border-color: var(--portal-brand); color: var(--portal-brand); background: var(--portal-surface); }
.btn-outline-primary:hover { background: var(--portal-brand); color: #fff; }

.btn-outline-secondary { border-color: var(--portal-border); color: var(--portal-ink); background: var(--portal-surface); }
.btn-outline-secondary:hover { border-color: var(--portal-ink-soft); color: var(--portal-ink); }

/*
 * PALETTE, chosen deliberately - requested directly, not the staff app's
 * stock Bootstrap blue carried over by default.
 *
 *   --portal-brand / --portal-brand-dark   deep navy. The most common choice
 *     in serious lending/banking software, and it now matches the identity
 *     already used in this business's own Daily Report, so the two read as
 *     one brand instead of two.
 *   --portal-accent   brass, used SPARINGLY - the one thing meant to draw the
 *     eye (a primary call to action), never the whole UI. Two chromatic
 *     colours, not a rainbow of them; everything else here is a neutral
 *     (white/black/grey) or one of the pre-existing status colours below,
 *     which are functional (this loan is late / this one is fine), not
 *     decorative, and were kept exactly as they were.
 *   --portal-ink-soft   darkened from #5b6b7f (~5.3:1 against white) to
 *     #4a5568 (~8:1). That original was the actual cause of "faint" text -
 *     it carried dates, loan status and every label next to a number, at
 *     small sizes, on the cheap phone screens this portal is built for.
 *     Passing the legal minimum was never the bar; comfortable at a glance
 *     in bright daylight was.
 *   --portal-bg   pure white, requested directly over the previous light
 *     grey wash. With no background tint left to separate a card from the
 *     page, --portal-border was darkened slightly (#dde3ea -> #ccd5e0) so
 *     cards still read as distinct using border + shadow alone.
 */
:root {
    --portal-bg: #ffffff;
    --portal-surface: #ffffff;
    --portal-border: #ccd5e0;
    --portal-ink: #16202c;
    --portal-ink-soft: #4a5568;
    --portal-brand: #0f3556;
    --portal-brand-dark: #0a2740;
    --portal-accent: #a06a1f;
    --portal-accent-dark: #855a1a;
    --portal-ok: #1b6b4a;
    --portal-warn: #96601a;
    --portal-danger: #9b2c2c;
    --portal-nav-height: 4rem;

    /*
     * Side padding, matching the staff shell's `container-fluid px-4` (1.5rem).
     * The portal is full-bleed like every other page in the app - see
     * .portal-container.
     */
    --portal-gutter: 1rem;
}

.portal-body {
    background: var(--portal-bg);
    color: var(--portal-ink);
    margin: 0;
    /* Clear the fixed bottom bar, plus the iOS home indicator. */
    padding-bottom: calc(var(--portal-nav-height) + env(safe-area-inset-bottom, 0px));
    -webkit-font-smoothing: antialiased;
}

/*
 * Full-bleed, like every other page in this app.
 *
 * The staff shell is `container-fluid px-4` and theme-standard.css neutralises
 * Bootstrap's max-widths globally, so dashboard.php runs edge to edge. The
 * portal used to be the one exception - a centred column that left roughly half
 * of a 1920px screen empty - which read as a phone app opened on a desktop.
 *
 * Width is filled by CONTENT, not by stretching: the grids below add columns as
 * the screen grows rather than making each card wider. The three things that
 * genuinely break when stretched - forms, the empty state, and the buttons
 * inside the due card - keep their own caps.
 */
.portal-container {
    width: 100%;
    margin: 0 auto;
    padding: 0 var(--portal-gutter);
}

/* ---------- header ---------- */
.portal-header {
    background: var(--portal-brand-dark);
    color: #fff;
    padding: 0.85rem 0;
    position: sticky;
    top: 0;
    z-index: 10;
}
.portal-brand {
    color: #fff;
    text-decoration: none;
    font-weight: 600;
    font-size: 1.05rem;
}
.portal-brand:hover { color: #fff; }

/* ---------- the header menu ---------- */
/*
 * ONE control in the header instead of a row of them.
 *
 * Built on <details>/<summary>, which is why there is no JavaScript here: the
 * portal ships none and sends script-src 'self' with no 'unsafe-inline', so a
 * scripted dropdown would be both a new dependency and a CSP exception. The
 * open/closed state is the browser's, so this keeps working even if this
 * stylesheet never arrives - it degrades to a plain list under a heading.
 */
/* ---------- header, desktop promotion ---------- */
/*
 * Mobile-first: everything here is the HIDDEN state. The desktop grid that
 * actually places these three items - brand left, actions centred, account
 * right - lives in the @media (min-width: 992px) block further down, next to
 * the rest of the desktop-only header rules.
 */
.portal-header-actions {
    display: none;
    align-items: center;
    gap: 1.5rem;
}
.portal-header-actions a {
    color: rgba(255, 255, 255, 0.9);
    text-decoration: none;
    font-weight: 600;
    white-space: nowrap;
}
.portal-header-actions a:hover,
.portal-header-actions a:focus-visible { color: #fff; }
.portal-header-actions a:focus-visible { outline: 2px solid #fff; outline-offset: 3px; }

.portal-menu { position: relative; flex: none; }

.portal-menu-button {
    /* Both properties are needed: Safari uses the ::-webkit- pseudo-element,
       everyone else honours list-style. */
    list-style: none;
    color: #fff;
    border: 1px solid rgba(255, 255, 255, 0.45);
    border-radius: 0.35rem;
    /* 44px minimum touch target - below that, thumbs miss. */
    min-width: 2.75rem;
    min-height: 2.5rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0 0.6rem;
    cursor: pointer;
}
.portal-menu-button::-webkit-details-marker { display: none; }
.portal-menu-button:hover { background: rgba(255, 255, 255, 0.15); }
.portal-menu-button:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
/* Open state reads as pressed, so it is obvious which control produced the panel. */
.portal-menu[open] .portal-menu-button { background: rgba(255, 255, 255, 0.22); }

.portal-menu-panel {
    position: absolute;
    right: 0;
    top: calc(100% + 0.5rem);
    min-width: 14rem;
    /* Never wider than the screen it drops onto. */
    max-width: calc(100vw - 2rem);
    background: var(--portal-surface);
    border: 1px solid var(--portal-border);
    border-radius: 0.5rem;
    box-shadow: 0 0.75rem 1.5rem rgba(11, 32, 51, 0.18);
    overflow: hidden;
    /* The header carries z-index 10; this sits inside it, above page content. */
    z-index: 30;
    animation: portalMenuIn 0.16s ease-out both;
}

@keyframes portalMenuIn {
    from { opacity: 0; transform: translateY(-0.35rem); }
    to   { opacity: 1; transform: none; }
}

.portal-menu-item {
    display: flex;
    align-items: center;
    gap: 0.65rem;
    /* Full-width rows at 44px+, because this is a thumb target on a phone. */
    min-height: 2.9rem;
    padding: 0.5rem 0.9rem;
    color: var(--portal-ink);
    text-decoration: none;
    font-size: 0.9rem;
    font-weight: 600;
    border-bottom: 1px solid var(--portal-border);
}
.portal-menu-item:last-child { border-bottom: 0; }
.portal-menu-item .portal-icon { flex: none; color: var(--portal-ink-soft); }
.portal-menu-item:hover { background: #e8eef4; color: var(--portal-brand); }
.portal-menu-item:hover .portal-icon { color: var(--portal-brand); }
.portal-menu-item:focus-visible { outline: 2px solid var(--portal-brand); outline-offset: -2px; }

/* Signing out is the one destructive action in the list, and it is next to
   four harmless ones. It gets its own colour and a rule above it so a thumb
   aiming for "Privacy policy" does not land on it by accident. */
.portal-menu-item.is-signout { color: var(--portal-danger); border-top: 1px solid var(--portal-border); }
.portal-menu-item.is-signout .portal-icon { color: var(--portal-danger); }
.portal-menu-item.is-signout:hover { background: #fdf6f6; color: var(--portal-danger); }

.portal-main { padding: 1.25rem 0 1.5rem; }

/* ---------- greeting ---------- */
.portal-greeting { font-size: 1.35rem; font-weight: 600; margin: 0 0 0.15rem; }
.portal-lastlogin { color: var(--portal-ink-soft); font-size: 0.8rem; margin: 0 0 1.25rem; }

/* ---------- the one number that matters ---------- */
.portal-due {
    background: var(--portal-surface);
    border: 1px solid var(--portal-border);
    border-radius: 0.6rem;
    padding: 1.25rem;
    margin-bottom: 1.25rem;
    /* The one place the accent colour appears: the single card every
       customer opens this page to read. Overdue/clear below override it with
       a functional colour (something is wrong / nothing is), which stays
       correct precedence - status must win over decoration. */
    border-top: 4px solid var(--portal-accent);
}
.portal-due.is-overdue { border-top-color: var(--portal-danger); }
.portal-due.is-clear { border-top-color: var(--portal-ok); }

.portal-due-label {
    font-size: 0.72rem;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: var(--portal-ink-soft);
    font-weight: 700;
    margin: 0 0 0.35rem;
}
.portal-due-amount {
    font-size: 2rem;
    font-weight: 700;
    line-height: 1.1;
    margin: 0;
    font-variant-numeric: tabular-nums;
}
.portal-due-when { color: var(--portal-ink-soft); margin: 0.25rem 0 0; }
.portal-due-when.is-overdue { color: var(--portal-danger); font-weight: 600; }

/* Plain-language explanation under the status pill (portalStatusNote). Sized
   below the figures so it reads as guidance, not as another number. */
.portal-status-note {
    color: var(--portal-ink-soft);
    font-size: 0.85rem;
    line-height: 1.45;
    margin: 0.6rem 0 0;
}

/* ---------- loan cards ---------- */
.portal-section-title {
    font-size: 0.72rem;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: var(--portal-ink-soft);
    font-weight: 700;
    margin: 1.5rem 0 0.6rem;
}

.portal-loan {
    display: block;
    background: var(--portal-surface);
    border: 1px solid var(--portal-border);
    border-radius: 0.6rem;
    padding: 1rem;
    margin-bottom: 0.75rem;
    text-decoration: none;
    color: inherit;
}
.portal-loan:hover { border-color: var(--portal-brand); color: inherit; }

.portal-loan-top { display: flex; justify-content: space-between; align-items: baseline; gap: 0.75rem; }
.portal-loan-ref { font-weight: 600; }
.portal-loan-balance { font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; }
.portal-loan-meta { color: var(--portal-ink-soft); font-size: 0.82rem; margin-top: 0.15rem; }

.portal-progress {
    height: 0.4rem;
    background: #e9eef4;
    border-radius: 999px;
    overflow: hidden;
    margin-top: 0.7rem;
}
.portal-progress span { display: block; height: 100%; background: var(--portal-ok); }

/* ---------- empty state ---------- */
/*
 * Stays narrow and centred no matter how wide the page is. This is the one
 * place full width actively hurts: a call to action stretched across 1900px of
 * empty box reads as a banner to be ignored, not an invitation to act. Below
 * 32rem the cap does not bind, so a phone is unaffected.
 */
/* ---------- fine-pause notice (statement) ---------- */
.portal-pause-notice {
    background: #fbf3e7;
    border: 1px solid #e6cb9a;
    border-radius: 0.6rem;
    padding: 0.85rem 1rem;
    color: var(--portal-warn);
    font-size: 0.9rem;
    margin-bottom: 1rem;
}

.portal-empty {
    background: var(--portal-surface);
    border: 1px dashed var(--portal-border);
    border-radius: 0.6rem;
    /* Dashed border kept - it is the deliberate signal for "empty state" -
       the shadow is only added so the box still separates from a page that
       is now the same white as the box itself. */
    box-shadow: 0 0.3rem 0.9rem rgba(11, 32, 51, 0.05);
    padding: 2.5rem 1rem;
    text-align: center;
    color: var(--portal-ink-soft);
    max-width: 32rem;
    margin-left: auto;
    margin-right: auto;
}

/* ---------- loan cards, as a grid that grows ---------- */
/*
 * auto-FILL, not auto-fit. auto-fit collapses the empty tracks, so a customer
 * with two loans on a wide screen would get two enormous stretched cards -
 * worse than the margins this replaced. auto-fill keeps the track size, so
 * cards stay card-sized and simply flow: 1 up on a phone, 2 on a tablet, 4-5
 * across a desktop. No breakpoints needed.
 */
.portal-loan-list {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
    gap: 0.75rem;
}
.portal-loan-list .portal-loan { margin-bottom: 0; }

/* ---------- bottom navigation ---------- */
.portal-nav {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    height: calc(var(--portal-nav-height) + env(safe-area-inset-bottom, 0px));
    padding-bottom: env(safe-area-inset-bottom, 0px);
    background: var(--portal-surface);
    border-top: 1px solid var(--portal-border);
    display: flex;
    z-index: 20;
}
.portal-nav-item {
    flex: 1 1 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.15rem;
    text-decoration: none;
    color: var(--portal-ink-soft);
    font-size: 0.7rem;
    font-weight: 600;
    padding: 0.35rem 0.25rem;
}
.portal-nav-item i { font-size: 1.15rem; }
.portal-nav-item.is-active { color: var(--portal-brand); }
.portal-nav-item:focus-visible { outline: 2px solid var(--portal-brand); outline-offset: -2px; }

/* ---------- tablet ---------- */
@media (min-width: 768px) {
    /* 1.5rem === the staff shell's px-4, so the portal lines up with the rest
       of the app rather than inventing its own margin. */
    :root { --portal-gutter: 1.5rem; }

    .portal-body { padding-bottom: 2rem; }
    .portal-main { padding-top: 1.75rem; }

    /* With room to spare the bar moves under the header and reads as tabs.
       There is no thumb to reach here, so pinning it to the bottom of a large
       screen would only put it further from the content it navigates. */
    .portal-nav {
        position: static;
        height: auto;
        margin: 0 var(--portal-gutter) 1.5rem;
        border: 1px solid var(--portal-border);
        border-radius: 0.6rem;
        overflow: hidden;
    }
    .portal-nav-item { flex-direction: row; gap: 0.4rem; font-size: 0.85rem; padding: 0.7rem 0.5rem; }
    .portal-nav-item.is-active { background: #e8eef4; }
}

/* ---------- desktop ---------- */
@media (min-width: 992px) {
    /* The headline number sits beside its summary tiles instead of above them -
       the two belong together and there is finally width for it. */
    .portal-overview {
        display: grid;
        grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
        gap: 1rem;
        align-items: start;
    }
    .portal-overview .portal-due { margin-bottom: 0; height: 100%; }
    .portal-overview .portal-summary { flex-direction: column; }

    /* A customer with no open loans has no summary tiles, so the card would
       otherwise sit in the first column with the second left empty. */
    .portal-overview .portal-due:only-child { grid-column: 1 / -1; }

    .portal-due-amount { font-size: 2.35rem; }

    /* The due card is wide now, and flex-fill would stretch these two buttons to
       hundreds of pixels each. A button's width should follow its label. */
    .portal-due .d-flex .btn { flex: 0 1 14rem; }

    /* .w-100 means "fill the thumb-width column" on a phone. Full-bleed, the
       column is the whole screen, so a call to action spanning it reads as a
       banner rather than a button. */
    .portal-main .btn.w-100 { width: auto; min-width: 16rem; }

    /*
     * The header, promoted to a real desktop nav bar. Kept at this breakpoint
     * rather than the 768px tablet one, on request ("only for desktop") - five
     * links plus the account button plus the brand needs the room a true
     * desktop width gives it, and this is the same breakpoint the page's own
     * two-column layouts (.portal-overview, .portal-panel-row) already switch
     * on, so the header and the page change together rather than the header
     * arriving a step early.
     *
     * THREE COLUMNS, NOT A FLEX ROW: brand / actions / account. A flex row
     * with only two side items would centre the actions BETWEEN the brand and
     * the account button, not in the header as a whole - and those two differ
     * in width (a long company name vs. a small fixed-width icon button), so
     * that centring would drift off-centre depending on the company's own
     * name. The two 1fr side columns absorb whatever width is left over
     * equally, keeping the centre column genuinely centred regardless of
     * brand length; the .portal-container utility classes that give every
     * OTHER use of this class its flex row are overridden here, scoped to
     * .portal-header specifically, so nothing outside the header is affected.
     */
    .portal-header .portal-container {
        display: grid;
        grid-template-columns: 1fr auto 1fr;
    }
    .portal-header .portal-brand { justify-self: start; }
    .portal-header .portal-menu { justify-self: end; }

    .portal-header-actions {
        display: flex;
        justify-self: center;
        font-size: 0.95rem;
    }
    /* The button itself stays exactly the icon-only square it is on mobile -
       widening it to fit a text label was tried and reverted on request, so
       nothing here overrides its size any more. */

    /* The dropdown itself gains room to breathe once it is not fighting for
       space with a thumb - the mobile sizing (min-height: 2.9rem touch
       targets, 0.82rem text) stays exactly as it was there. */
    .portal-menu-panel { min-width: 15.5rem; }
    .portal-menu-item { font-size: 0.9rem; padding: 0.6rem 1rem; }

    /*
     * The dashboard's own Quick actions / Need help sections are now
     * redundant with the header above - hidden here, exactly as they were on
     * mobile below this breakpoint, where the header has no room for them.
     */
    .portal-panel-desktop-hidden,
    .portal-contact-desktop-hidden { display: none; }
}

/* ---------- section panels: every screen size ---------- */
/*
 * "My record with us", "Quick actions", "My loans", "Coming up after that"
 * and "Recent payments" each sat as a bare heading over raw content, with
 * nothing telling the eye where one group ends and the next begins. This
 * wraps each in one shared card - background, border, a soft shadow the rest
 * of this file otherwise never uses, so a PANEL reads as a distinct kind of
 * thing from the plain bordered LEAF components (a loan tile, an action
 * button) sitting inside it.
 *
 * ON EVERY SCREEN SIZE, INCLUDING MOBILE - requested directly, overriding an
 * earlier decision to leave mobile bare. Full-bleed width (see .portal-container
 * further up) is unaffected either way: a card with a small margin around it
 * is still edge-to-edge in the sense that mattered there - no centred column,
 * no wasted side margin - it is a separate question from whether each SECTION
 * has a visible boundary, which is what was being asked for here.
 *
 * Padding is mobile-first and tighter below 768px, where every character of
 * width is worth more - it widens at the existing tablet breakpoint rather
 * than staying fixed, matching how --portal-gutter already scales in this
 * file.
 *
 * The Next Payment area (.portal-due/.portal-overview) and Need Help
 * (.portal-contact) already carry their own card border at the component
 * level, on every screen size, and are deliberately left out of this -
 * wrapping an already-bordered box in another one reads as a box inside a
 * box, not a group.
 */
.portal-panel {
    background: var(--portal-surface);
    border: 1px solid var(--portal-border);
    border-radius: 0.65rem;
    box-shadow: 0 0.3rem 0.9rem rgba(11, 32, 51, 0.05);
    padding: 1rem 1.1rem 1.2rem;
    margin-bottom: 1rem;
}
/* The heading moves inside the card; its own top margin - meant to separate
   it from whatever sat above it on the bare page - would otherwise show up
   as odd extra space under the card's own padding. */
.portal-panel > .portal-section-title:first-child { margin-top: 0; }
.portal-panel .portal-balances { margin-bottom: 0; }

@media (min-width: 768px) {
    .portal-panel {
        border-radius: 0.75rem;
        box-shadow: 0 0.4rem 1.1rem rgba(11, 32, 51, 0.06);
        padding: 1.35rem 1.5rem 1.5rem;
        margin-bottom: 1.5rem;
    }
}

/* Coming up after that / Recent payments: two short, related lists, paired
   side by side once there is real width for it, rather than one under the
   other. Kept at the desktop breakpoint rather than tablet - two schedule
   rows (date, loan, amount) side by side at 768px reads as cramped. */
@media (min-width: 992px) {
    .portal-panel-row {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: 1.5rem;
        align-items: start;
    }
    .portal-panel-row .portal-panel { margin-bottom: 0; }
}

@media (prefers-reduced-motion: reduce) {
    * { transition: none !important; animation: none !important; }
}

/* Inline SVG icons sit on the text baseline rather than hanging below it. */
.portal-icon { vertical-align: -0.15em; flex: none; }

/* ---------- Phase 5: loan detail, schedule, statement ---------- */
.portal-back {
    display: inline-block;
    color: var(--portal-ink-soft);
    text-decoration: none;
    font-size: 0.88rem;
    font-weight: 600;
    margin-bottom: 0.85rem;
}
.portal-back:hover { color: var(--portal-brand); }

.portal-pill {
    display: inline-block;
    font-size: 0.7rem;
    font-weight: 700;
    padding: 0.1rem 0.5rem;
    border-radius: 999px;
    background: #e9eef4;
    color: var(--portal-ink-soft);
}
.portal-pill.is-open { background: #e3ecfb; color: #17457e; }
.portal-pill.is-done { background: #dff0e7; color: var(--portal-ok); }
.portal-pill.is-bad  { background: #f8e3e3; color: var(--portal-danger); }
.portal-pill.is-wait { background: #faeedc; color: var(--portal-warn); }

/* Key/value facts about a loan. */
/* auto-fit here, unlike the loan grid: the fact count is small and fixed (four),
   so collapsing the empty tracks spreads those four across the row instead of
   leaving a ragged gap. Two up on a phone, one row across a desktop. */
.portal-facts {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
    gap: 1px;
    background: var(--portal-border);
    border: 1px solid var(--portal-border);
    border-radius: 0.6rem;
    overflow: hidden;
    margin: 0 0 0.5rem;
}
.portal-facts > div { background: var(--portal-surface); padding: 0.75rem 0.9rem; }
.portal-facts dt {
    font-size: 0.68rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--portal-ink-soft);
    font-weight: 700;
    margin: 0 0 0.15rem;
}
.portal-facts dd { margin: 0; font-weight: 600; font-variant-numeric: tabular-nums; }

/* A penalty is the one fact on this list the customer needs to notice. */
.portal-facts dd.portal-fact-warn { color: var(--portal-danger); }

/* Instalment / statement rows. */
.portal-schedule { list-style: none; margin: 0; padding: 0; }
.portal-instalment {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 0.75rem;
    background: var(--portal-surface);
    border: 1px solid var(--portal-border);
    border-radius: 0.5rem;
    padding: 0.7rem 0.85rem;
    margin-bottom: 0.4rem;
}
.portal-instalment.is-paid { opacity: 0.72; }
.portal-instalment.is-late { border-color: #f0c9c9; background: #fdf6f6; }

.portal-instalment-main { display: flex; align-items: center; gap: 0.7rem; min-width: 0; }
.portal-instalment-no {
    flex: none;
    width: 1.7rem;
    height: 1.7rem;
    border-radius: 50%;
    background: #eef2f7;
    color: var(--portal-ink-soft);
    display: grid;
    place-items: center;
    font-size: 0.75rem;
    font-weight: 700;
}
.portal-instalment.is-paid .portal-instalment-no { background: #dff0e7; color: var(--portal-ok); }
.portal-instalment.is-late .portal-instalment-no { background: #f8e3e3; color: var(--portal-danger); }
.portal-instalment-no.is-in  { background: #dff0e7; color: var(--portal-ok); }
.portal-instalment-no.is-out { background: #e3ecfb; color: #17457e; }

.portal-instalment-date { display: block; font-weight: 600; font-size: 0.92rem; }
.portal-instalment-state { display: block; font-size: 0.78rem; color: var(--portal-ink-soft); }
.portal-instalment.is-late .portal-instalment-state { color: var(--portal-danger); font-weight: 600; }

.portal-instalment-amount {
    text-align: right;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.portal-instalment-running {
    display: block;
    font-size: 0.76rem;
    font-weight: 500;
    color: var(--portal-ink-soft);
}

/* Statement filter, and the apply form - both use this shell.
 *
 * Capped and centred whatever the page does. A date field or a text input
 * stretched across a full-bleed page is harder to use, not easier, and reads as
 * broken. Below 44rem the cap does not bind, so a phone is unaffected. */
/*
 * This form container relied on a border alone to separate it from the page
 * - fine while the page had its own light grey tint (--portal-bg), invisible
 * once that moved to pure white and the form's own background
 * (--portal-surface) became the exact same colour. Reported directly on
 * /portal/apply, where it is the entire page's content. Given the same
 * box-shadow treatment .portal-panel already uses for the same reason, so a
 * form reads as a distinct surface again regardless of what colour the page
 * behind it happens to be. Shared by signup.php and statement.php too - one
 * fix, three pages corrected.
 */
.portal-filter {
    background: var(--portal-surface);
    border: 1px solid var(--portal-border);
    border-radius: 0.6rem;
    box-shadow: 0 0.3rem 0.9rem rgba(11, 32, 51, 0.05);
    padding: 1rem;
    margin: 0 auto 1rem;
    max-width: 44rem;
    display: grid;
    gap: 0.75rem;
}
.portal-filter label { display: block; }
.portal-filter label > span {
    display: block;
    font-size: 0.68rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--portal-ink-soft);
    font-weight: 700;
    margin-bottom: 0.25rem;
}
/* Every text-like input, not just the three types the statement filter happened
   to use. Before the registration wizard the portal had no plain text input in
   a .portal-filter, so `text` and `tel` were never styled and rendered at the
   browser's default size - visibly narrower than the select beside them. */
.portal-filter select,
.portal-filter input[type="text"],
.portal-filter input[type="tel"],
.portal-filter input[type="email"],
.portal-filter input[type="date"] {
    width: 100%;
    min-height: 2.75rem;
    padding: 0.45rem 0.6rem;
    border: 1px solid var(--portal-border);
    border-radius: 0.4rem;
    font-size: 0.95rem;
    background: var(--portal-surface);
    color: var(--portal-ink);
}
.portal-filter-dates { display: grid; grid-template-columns: 1fr 1fr; gap: 0.6rem; }

.portal-balances {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
    gap: 0.5rem;
    margin-bottom: 1rem;
}
.portal-balances > div {
    background: var(--portal-surface);
    border: 1px solid var(--portal-border);
    border-radius: 0.6rem;
    padding: 0.75rem 0.9rem;
}
.portal-balances strong { display: block; font-size: 1.05rem; font-variant-numeric: tabular-nums; }

/* Print: the customer takes a statement to a branch or keeps it for records,
   so the app shell has no place on paper. */
@media print {
    .portal-header,
    .portal-nav,
    .portal-filter,
    .portal-back,
    button { display: none !important; }

    .portal-body { background: #fff; padding: 0; }
    .portal-instalment { break-inside: avoid; border-color: #ccc; }
}

/* ---------- Phase 6: apply form ---------- */
.portal-alert {
    background: #f8e3e3;
    border: 1px solid #f0c9c9;
    color: var(--portal-danger);
    border-radius: 0.5rem;
    padding: 0.7rem 0.9rem;
    margin-bottom: 1rem;
    font-size: 0.92rem;
}
.portal-filter textarea {
    width: 100%;
    padding: 0.55rem 0.6rem;
    border: 1px solid var(--portal-border);
    border-radius: 0.4rem;
    font-size: 0.95rem;
    font-family: inherit;
    background: var(--portal-surface);
    color: var(--portal-ink);
    resize: vertical;
}
.portal-filter input[type="number"] {
    width: 100%;
    min-height: 2.75rem;
    padding: 0.45rem 0.6rem;
    border: 1px solid var(--portal-border);
    border-radius: 0.4rem;
    font-size: 0.95rem;
    background: var(--portal-surface);
    color: var(--portal-ink);
}
.portal-filter select:focus-visible,
.portal-filter input:focus-visible,
.portal-filter textarea:focus-visible {
    outline: 2px solid var(--portal-brand);
    outline-offset: 1px;
}
/* A heading inside a form, splitting a long one into readable groups.
   The portal ships no JavaScript, so a multi-step wizard is not available -
   and on a poor connection one page that always renders beats a wizard that
   might not. These headings do the same job structurally. */
.portal-form-section {
    margin: 0.5rem 0 0;
    padding-top: 0.9rem;
    border-top: 1px solid var(--portal-border);
    font-size: 0.9rem;
    font-weight: 700;
    color: var(--portal-ink);
}
.portal-form-section span {
    display: block;
    font-size: 0.78rem;
    font-weight: 400;
    color: var(--portal-ink-soft);
    margin-top: 0.15rem;
}

/* ---------- registration wizard ---------- */
/*
 * A progress tracker for a form that spans several pages.
 *
 * On a multi-page form the applicant cannot see how much is left, and a form of
 * unknown length is one people abandon. This is a list of four steps, so it
 * still reads correctly with no CSS at all - the styling only makes visible
 * what the markup already says.
 */
.portal-steps {
    display: flex;
    list-style: none;
    margin: 0 auto 1.25rem;
    padding: 0;
    max-width: 44rem;
    counter-reset: step;
}
.portal-steps li {
    flex: 1 1 0;
    position: relative;
    text-align: center;
    font-size: 0.68rem;
    font-weight: 600;
    color: var(--portal-ink-soft);
    padding-top: 2.1rem;
    min-width: 0;
}
/* The connecting rule, drawn behind the markers. */
.portal-steps li::before {
    content: '';
    position: absolute;
    top: 0.85rem;
    left: -50%;
    width: 100%;
    height: 2px;
    background: var(--portal-border);
}
.portal-steps li:first-child::before { display: none; }

.portal-steps li .portal-step-dot {
    position: absolute;
    top: 0;
    left: 50%;
    transform: translateX(-50%);
    width: 1.75rem;
    height: 1.75rem;
    border-radius: 50%;
    background: var(--portal-surface);
    border: 2px solid var(--portal-border);
    color: var(--portal-ink-soft);
    display: grid;
    place-items: center;
    font-size: 0.75rem;
    font-weight: 700;
}

.portal-steps li.is-done { color: var(--portal-ok); }
.portal-steps li.is-done::before { background: var(--portal-ok); }
.portal-steps li.is-done .portal-step-dot {
    background: var(--portal-ok);
    border-color: var(--portal-ok);
    color: #fff;
}

.portal-steps li.is-current { color: var(--portal-brand); }
.portal-steps li.is-current .portal-step-dot {
    border-color: var(--portal-brand);
    color: var(--portal-brand);
    box-shadow: 0 0 0 3px rgba(15, 53, 86, 0.15);
}

/* Step labels are the first thing to go when the screen is narrow - the
   numbered markers still carry the meaning. */
.portal-step-label { display: none; }
@media (min-width: 480px) {
    .portal-step-label { display: block; }
}

/* Two fields to a row once there is width for it. Below 768px they stack,
   which is the phone-first default. */
@media (min-width: 768px) {
    .portal-field-pair {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: 0.75rem;
    }
}

/* Marks a field that must be filled. The asterisk is decorative - the real
   statement is the "required" attribute and the server-side check. */
.portal-req::after {
    content: ' *';
    color: var(--portal-danger);
    font-weight: 700;
}

/* Back to the previous step. Deliberately a link, not a second submit button:
   going back must never post the form. */
.portal-step-back {
    display: inline-block;
    margin-top: 0.75rem;
    color: var(--portal-ink-soft);
    text-decoration: none;
    font-size: 0.85rem;
    font-weight: 600;
}
.portal-step-back:hover { color: var(--portal-brand); }

/* An answer already given, carried forward rather than asked again - the region
   above the district list it produced. */
.portal-chosen {
    max-width: 44rem;
    margin: 0 auto .75rem;
    padding: .6rem .9rem;
    background: #e8eef4;
    border: 1px solid var(--portal-border);
    border-radius: .4rem;
    font-size: .9rem;
    display: flex;
    align-items: baseline;
    gap: .5rem;
}
.portal-chosen span {
    font-size: .68rem;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--portal-ink-soft);
    font-weight: 700;
}
.portal-chosen strong { flex: 1 1 auto; min-width: 0; }
.portal-chosen a { color: var(--portal-brand); font-weight: 600; font-size: .82rem; }

/* The review: every answer read back before it is sent. */
.portal-review {
    max-width: 44rem;
    margin: 0 auto 1rem;
    display: flex;
    flex-direction: column;
    gap: .75rem;
}
.portal-review-block {
    background: var(--portal-surface);
    border: 1px solid var(--portal-border);
    border-radius: .6rem;
    box-shadow: 0 0.3rem 0.9rem rgba(11, 32, 51, 0.05);
    padding: .9rem 1.1rem;
}
.portal-review-block h2 {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: .75rem;
    margin: 0 0 .6rem;
    font-size: .72rem;
    letter-spacing: .09em;
    text-transform: uppercase;
    color: var(--portal-ink-soft);
    font-weight: 700;
}
.portal-review-block h2 a { font-size: .78rem; text-transform: none; letter-spacing: 0; color: var(--portal-brand); font-weight: 600; }
.portal-review-block dl { margin: 0; display: grid; gap: .4rem; }
.portal-review-block dl > div { display: grid; grid-template-columns: 11rem 1fr; gap: .5rem; }
.portal-review-block dt { color: var(--portal-ink-soft); font-size: .85rem; }
.portal-review-block dd { margin: 0; font-weight: 600; font-size: .9rem; word-break: break-word; }

/* On a phone the label sits above its value rather than beside it. */
@media (max-width: 519px) {
    .portal-review-block dl > div { grid-template-columns: 1fr; gap: 0; }
    .portal-review-block dt { font-size: .72rem; text-transform: uppercase; letter-spacing: .06em; }
}

/* A validation message under the field it belongs to, so an applicant on a
   long form does not have to hunt for what went wrong. */
.portal-field-error {
    display: block;
    margin-top: 0.3rem;
    font-size: 0.78rem;
    font-weight: 600;
    color: var(--portal-danger);
}

.portal-field-hint {
    display: block;
    margin-top: 0.35rem;
    font-size: 0.75rem;
    color: var(--portal-ink-soft);
}

/* File inputs are styled explicitly: left alone they inherit none of the form's
   sizing and sit noticeably smaller than every other control next to them. */
.portal-filter input[type="file"] {
    width: 100%;
    padding: 0.5rem 0.6rem;
    border: 1px solid var(--portal-border);
    border-radius: 0.4rem;
    background: var(--portal-surface);
    color: var(--portal-ink);
    font-size: 0.85rem;
    font-family: inherit;
}
.portal-filter input[type="file"]::file-selector-button {
    margin-right: 0.6rem;
    padding: 0.35rem 0.7rem;
    border: 1px solid var(--portal-border);
    border-radius: 0.3rem;
    background: #eef2f7;
    color: var(--portal-ink);
    font-weight: 600;
    font-size: 0.8rem;
    cursor: pointer;
}

.portal-formnote {
    color: var(--portal-ink-soft);
    font-size: 0.78rem;
    text-align: center;
    margin: 0;
}

/* ==========================================================================
   Identity, documents, quick actions and company details
   ==========================================================================
   Added when the dashboard grew from "your balance" into "your account". Every
   rule below is scoped to a .portal-* class, uses the existing custom
   properties, and adds no new colour - the palette at the top of this file is
   still the whole palette.
   ========================================================================== */

/* ---------- who the customer is ---------- */
/*
 * The photo and the greeting as one block. Row layout from the smallest screen
 * up: a 72px avatar beside two lines of text fits inside 320px, so there is no
 * width at which stacking them would read better.
 */
.portal-identity {
    display: flex;
    align-items: center;
    gap: 0.9rem;
    background: var(--portal-surface);
    border: 1px solid var(--portal-border);
    border-radius: 0.6rem;
    padding: 1rem;
    margin-bottom: 1.25rem;
}
.portal-identity-body { min-width: 0; flex: 1 1 auto; }
.portal-identity .portal-greeting { font-size: 1.2rem; }

.portal-identity-meta {
    color: var(--portal-ink-soft);
    font-size: 0.8rem;
    margin: 0.1rem 0 0.4rem;
}
.portal-identity-meta strong { color: var(--portal-ink); font-variant-numeric: tabular-nums; }

.portal-identity-pills {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
    margin: 0 0 0.4rem;
}

/*
 * The avatar is a FIXED square with the image cropped to fill it. Passport
 * photos arrive at whatever aspect ratio a phone camera produced, and letting
 * them size themselves would push the greeting around differently for every
 * customer. object-fit does the cropping without distorting a face.
 */
.portal-avatar {
    flex: none;
    width: 4.5rem;
    height: 4.5rem;
    border-radius: 50%;
    overflow: hidden;
    background: #eef2f7;
    border: 2px solid var(--portal-border);
    display: grid;
    place-items: center;
}
.portal-avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
.portal-avatar-initials {
    font-size: 1.5rem;
    font-weight: 700;
    color: var(--portal-ink-soft);
    letter-spacing: 0.02em;
}

/* ---------- quick actions ---------- */
/*
 * Four targets, two up on a phone and four across once there is room. Grid
 * rather than flex so every tile is the same height whatever its label wraps to
 * - a ragged row of action tiles is the detail that makes an interface look
 * unfinished.
 */
.portal-actions {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.6rem;
}
.portal-action {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.45rem;
    text-align: center;
    /* 44px is the floor for a thumb; these carry two lines of label, so they get
       more. */
    min-height: 5.25rem;
    padding: 0.9rem 0.6rem;
    background: var(--portal-surface);
    border: 1px solid var(--portal-border);
    border-radius: 0.6rem;
    color: var(--portal-ink);
    text-decoration: none;
    font-size: 0.82rem;
    font-weight: 600;
    line-height: 1.25;
}
.portal-action .portal-icon { color: var(--portal-brand); }
.portal-action:hover { border-color: var(--portal-brand); background: #e8eef4; }
.portal-action:focus-visible { outline: 2px solid var(--portal-brand); outline-offset: 2px; }

@media (min-width: 768px) {
    .portal-actions { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

/* ---------- account notices ---------- */
/*
 * Colour carries the severity, and a left rule carries it again for anyone who
 * cannot separate the hues - the same belt-and-braces the instalment rows use.
 * Every notice also states its meaning in words, so colour is never the only
 * thing saying "this is late".
 */
.portal-alerts { display: grid; gap: 0.5rem; margin-bottom: 1.25rem; }

.portal-alert-card {
    display: flex;
    align-items: flex-start;
    gap: 0.7rem;
    padding: 0.85rem 0.95rem;
    border: 1px solid var(--portal-border);
    border-left: 4px solid var(--portal-ink-soft);
    border-radius: 0.5rem;
    background: var(--portal-surface);
}
.portal-alert-icon { flex: none; margin-top: 0.05rem; }
.portal-alert-body { flex: 1 1 auto; min-width: 0; }
.portal-alert-body strong { display: block; font-size: 0.95rem; line-height: 1.3; }
.portal-alert-body span { display: block; font-size: 0.82rem; line-height: 1.45; color: var(--portal-ink-soft); margin-top: 0.15rem; }

/*
 * The per-loan breakdown inside an arrears notice.
 *
 * Only rendered when more than one loan is behind. Each row is a link, so the
 * customer goes straight from "which of my loans is this?" to that loan's
 * schedule without hunting through the list further down the page.
 *
 * Rows sit on the card's own tint rather than on white: they are part of the
 * notice, not a table that happens to be near it.
 */
/*
 * The collapsible wrapper around the breakdown (and this alert's own
 * explanatory line) - native <details>, so no JS. The default marker/
 * cursor styling is unset because .portal-alert-toggle below is the actual
 * clickable control it draws instead - a small pill matching
 * .portal-alert-action's visual language, positioned inline rather than as
 * a flex sibling of the card since it lives inside the collapsed content's
 * own flow, not beside it.
 */
.portal-alert-details { margin-top: 0.5rem; }
.portal-alert-details summary { list-style: none; cursor: pointer; }
.portal-alert-details summary::-webkit-details-marker { display: none; }
.portal-alert-details[open] summary { margin-bottom: 0.35rem; }

.portal-alert-toggle {
    display: inline-flex;
    align-items: center;
    /* 44px touch target, as everywhere else in the portal. */
    min-height: 2.25rem;
    padding: 0 0.75rem;
    border: 1px solid currentColor;
    border-radius: 0.35rem;
    font-size: 0.8rem;
    font-weight: 700;
}
.portal-alert-toggle:hover { background: rgba(0, 0, 0, 0.04); }
.portal-alert-details summary:focus-visible .portal-alert-toggle { outline: 2px solid currentColor; outline-offset: 2px; }

/*
 * Swapped by the [open] attribute alone - no script decides which word
 * shows. The base rule is qualified with .portal-alert-toggle (rather than
 * bare .portal-alert-toggle-hide) purely for specificity: .portal-alert-body
 * span - a real, needed rule that block-ifies this alert's detail text -
 * would otherwise win against a single-class selector and leave "Hide"
 * visible at the same time as "View" even while collapsed.
 */
.portal-alert-toggle .portal-alert-toggle-hide { display: none; }
.portal-alert-details[open] .portal-alert-toggle-view { display: none; }
.portal-alert-details[open] .portal-alert-toggle-hide { display: inline; }

.portal-alert-loans {
    list-style: none;
    margin: 0.7rem 0 0;
    padding: 0;
    border-top: 1px solid rgba(0, 0, 0, 0.08);
}
.portal-alert-loans li { border-bottom: 1px solid rgba(0, 0, 0, 0.08); }
.portal-alert-loans li:last-child { border-bottom: 0; }

.portal-alert-loans a {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    /* 44px: these are the primary targets in a multi-loan notice. */
    min-height: 2.9rem;
    padding: 0.5rem 0;
    color: var(--portal-ink);
    text-decoration: none;
}
.portal-alert-loans a:hover .portal-alert-loan-name strong { text-decoration: underline; }
.portal-alert-loans a:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }

.portal-alert-loan-name { min-width: 0; }
.portal-alert-loan-name strong { display: block; font-size: 0.9rem; }
.portal-alert-loan-name > span { display: block; font-size: 0.75rem; color: var(--portal-ink-soft); margin-top: 0.1rem; }

.portal-alert-loan-amount {
    flex: none;
    font-weight: 700;
    font-size: 0.9rem;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.portal-alert-action {
    flex: none;
    align-self: center;
    /* 44px touch target, as everywhere else in the portal. */
    min-height: 2.25rem;
    display: inline-flex;
    align-items: center;
    padding: 0 0.75rem;
    border-radius: 0.35rem;
    border: 1px solid currentColor;
    font-size: 0.8rem;
    font-weight: 700;
    text-decoration: none;
    white-space: nowrap;
}

.portal-alert-card.is-danger { border-left-color: var(--portal-danger); background: #fdf6f6; }
.portal-alert-card.is-danger .portal-alert-icon,
.portal-alert-card.is-danger .portal-alert-action,
.portal-alert-card.is-danger .portal-alert-toggle { color: var(--portal-danger); }

.portal-alert-card.is-warn { border-left-color: var(--portal-warn); background: #fffaf2; }
.portal-alert-card.is-warn .portal-alert-icon,
.portal-alert-card.is-warn .portal-alert-action,
.portal-alert-card.is-warn .portal-alert-toggle { color: var(--portal-warn); }

.portal-alert-card.is-info { border-left-color: var(--portal-brand); background: #e8eef4; }
.portal-alert-card.is-info .portal-alert-icon,
.portal-alert-card.is-info .portal-alert-action,
.portal-alert-card.is-info .portal-alert-toggle { color: var(--portal-brand); }

.portal-alert-card.is-ok { border-left-color: var(--portal-ok); background: #f6fbf8; }
.portal-alert-card.is-ok .portal-alert-icon { color: var(--portal-ok); }

.portal-alert-action:hover { background: rgba(0, 0, 0, 0.04); }
.portal-alert-action:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }

/* On a narrow screen the action drops beneath the text rather than squeezing
   the sentence into a two-word column. */
@media (max-width: 419px) {
    .portal-alert-card { flex-wrap: wrap; }
    .portal-alert-action { width: 100%; justify-content: center; margin-top: 0.15rem; }
}

/* ---------- documents on file ---------- */
.portal-doclist { list-style: none; margin: 0; padding: 0; }
.portal-doclist li {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    background: var(--portal-surface);
    border: 1px solid var(--portal-border);
    border-radius: 0.5rem;
    padding: 0.75rem 0.85rem;
    margin-bottom: 0.4rem;
}
.portal-doclist li > .portal-icon { flex: none; color: var(--portal-ink-soft); }
.portal-doclist li > div { flex: 1 1 auto; min-width: 0; }
.portal-doclist strong { display: block; font-size: 0.92rem; }
.portal-doclist span { display: block; font-size: 0.78rem; color: var(--portal-ink-soft); }
/* The row's button must not grow to the height of the row it sits in. */
.portal-doclist .btn { flex: none; min-height: 2.25rem; padding: 0.35rem 0.85rem; font-size: 0.82rem; }

/* ---------- the brand mark ---------- */
/*
 * A FIXED SQUARE, whatever the institution is called.
 *
 * The mark is the logo file when one exists and the company's monogram when it
 * does not, and either way it occupies exactly the same box - so the name
 * beside it lays out identically for "ABC Bank" and for "BEJUNDAS FINANCIAL
 * SERVICES LTD", and a logo of any aspect ratio is contained rather than
 * stretching the row it sits in.
 */
.portal-mark {
    flex: none;
    display: grid;
    place-items: center;
    overflow: hidden;
    font-weight: 800;
    letter-spacing: 0.01em;
    line-height: 1;
    text-transform: uppercase;
}
/* contain, never cover: a logo cropped to a square is a damaged logo. */
.portal-mark img { width: 100%; height: 100%; object-fit: contain; display: block; }

/* On the blue bar. A translucent white tile reads as part of the header rather
   than as a sticker on top of it, and works against either blue tier. */
.portal-mark--header {
    width: 2.1rem;
    height: 2.1rem;
    border-radius: 0.4rem;
    background: rgba(255, 255, 255, 0.18);
    color: #fff;
    font-size: 0.72rem;
}
/* A real logo is usually artwork on white, so it gets a white tile and a little
   padding rather than being floated on the blue. */
.portal-mark--header.portal-mark--logo { background: #fff; padding: 0.2rem; }

.portal-mark--card {
    width: 3rem;
    height: 3rem;
    border-radius: 0.5rem;
    background: #e8eef4;
    color: var(--portal-brand);
    font-size: 0.95rem;
}
.portal-mark--card.portal-mark--logo { background: #fff; border: 1px solid var(--portal-border); padding: 0.25rem; }

.portal-mark--footer {
    width: 1.5rem;
    height: 1.5rem;
    border-radius: 0.3rem;
    background: #e9eef4;
    color: var(--portal-ink-soft);
    font-size: 0.58rem;
}
.portal-mark--footer.portal-mark--logo { background: transparent; }

/* ---------- the company ---------- */
.portal-company {
    position: relative;
    overflow: hidden;
    background: var(--portal-surface);
    border: 1px solid var(--portal-border);
    border-radius: 0.6rem;
    /* Same reason .portal-filter gained one: a border alone stopped being
       enough to separate a white card from a white page. */
    box-shadow: 0 0.3rem 0.9rem rgba(11, 32, 51, 0.05);
    padding: 1rem;
}

/*
 * The letterhead watermark: the monogram, oversized and almost invisible.
 *
 * Drawn from the `data-mark` attribute rather than an image, because a logo file
 * arrives at an unknown aspect ratio, an unknown palette and often on a white
 * rectangle - as a watermark it would need per-file scaling, tinting and
 * positioning no stylesheet can guess. A letterform tints and clips identically
 * for every institution, and exists even when no logo has been uploaded.
 *
 * 0.05 alpha: present enough to give the card a surface, far too light to
 * interfere with the text over it. Sits behind everything and takes no clicks.
 */
.portal-company::after {
    content: attr(data-mark);
    position: absolute;
    right: -0.75rem;
    bottom: -2.25rem;
    font-size: 7rem;
    font-weight: 800;
    letter-spacing: -0.05em;
    line-height: 1;
    color: var(--portal-brand);
    opacity: 0.05;
    pointer-events: none;
    user-select: none;
    animation: portalHeroSettle 0.6s ease-out both;
}
.portal-company > * { position: relative; z-index: 1; }

/*
 * The real logo, large and faint, replacing the letterform above when one
 * has actually been uploaded (.portal-company--logo, set server-side in
 * portal/about.php; the URL itself travels via the --portal-hero-logo
 * custom property so this stylesheet, not the page, owns size and
 * position). Institutions with no logo file keep the plain monogram - it
 * was always the fallback, not a placeholder waiting to be replaced.
 *
 * Fixed rem dimensions, not a percentage of the card's own height: a
 * percentage height on an absolutely-positioned element only resolves
 * reliably when its containing block has a height the browser considers
 * "definite", and a card whose height comes from min-height plus auto
 * content is exactly the case that trips some mobile browsers - the logo
 * rendered past the card's own border on-device even though overflow:
 * hidden should have clipped it. A fixed box has no such ambiguity.
 */
.portal-company--logo {
    min-height: 6rem;
}
.portal-company--logo::after {
    content: "";
    right: -0.5rem;
    bottom: -0.5rem;
    width: 8rem;
    height: 8rem;
    background-image: var(--portal-hero-logo);
    background-repeat: no-repeat;
    background-position: right bottom;
    background-size: contain;
    opacity: 0.09;
    animation: portalHeroSettleLogo 0.7s ease-out both;
}

/*
 * The one line of copy that still belongs on this card: who supervises us,
 * with the shield mark tying it visually to "this is a checked, regulated
 * lender" rather than reading as a stray fact. The company name and small
 * corner logo used to repeat here too - removed, because the header bar
 * immediately above already names the company; saying it a third time
 * (navbar, card, and now the large background mark) read as unfinished,
 * not thorough.
 */
.portal-company-trust {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    color: var(--portal-ink-soft);
    font-size: 0.85rem;
}
.portal-company-trust svg { color: var(--portal-brand); flex-shrink: 0; }

.portal-company-actions {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr));
    gap: 0.5rem;
    margin-top: 1rem;
    padding-top: 0.9rem;
    border-top: 1px solid var(--portal-border);
}

/*
 * The hero row: the company identity card and the office map, side by side
 * on a wide screen (map at the top RIGHT, by request), stacked (map below)
 * on a narrow one. flex-wrap alone handles the breakpoint - the two basis
 * values below add up to more than 100% once the row can no longer fit
 * both at a readable width, which is exactly when they should stack.
 */
.portal-hero-row {
    display: flex;
    flex-wrap: wrap;
    align-items: stretch;
    gap: 1rem;
    margin-bottom: 1.5rem;
}
.portal-hero-row .portal-company { flex: 3 1 20rem; min-width: 0; margin-bottom: 0; }

/*
 * The map card - matches .portal-company's own surface/border/shadow/radius
 * exactly, so the two read as one pair rather than two different card
 * styles competing for attention. See portal/about.php's own comment (right
 * above where this card is built) for why the coordinates are hardcoded and
 * why this is the one embedded iframe on the whole portal.
 */
.portal-map-card {
    flex: 2 1 16rem;
    min-width: 0;
    display: flex;
    flex-direction: column;
    background: var(--portal-surface);
    border: 1px solid var(--portal-border);
    border-radius: 0.6rem;
    box-shadow: 0 0.3rem 0.9rem rgba(11, 32, 51, 0.05);
    overflow: hidden;
}
.portal-map-label {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.65rem 0.9rem;
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--portal-ink-soft);
    border-bottom: 1px solid var(--portal-border);
}
.portal-map-label .portal-icon { color: var(--portal-brand); flex: none; }
.portal-map-card iframe {
    display: block;
    width: 100%;
    height: 12rem;
    border: 0;
    flex: 1 1 auto;
}
.portal-map-directions {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.4rem;
    padding: 0.55rem 0.9rem;
    font-size: 0.82rem;
    font-weight: 600;
    color: var(--portal-brand);
    text-decoration: none;
    border-top: 1px solid var(--portal-border);
}
.portal-map-directions:hover { background: #f3f6fa; }
.portal-map-directions .portal-icon { flex: none; }

/*
 * The dashboard's "Need help?" block: actions and an address, no identity.
 *
 * Distinct from .portal-company (which keeps the mark, the name and the
 * watermark) because that treatment now appears ONLY on /portal/about, where
 * the institution is the subject of the page rather than a decoration on
 * someone else's. Here the customer already knows whose portal they are in -
 * the header says so and never scrolls away - so repeating it added nothing but
 * a third copy of the same words.
 */
.portal-contact {
    background: var(--portal-surface);
    border: 1px solid var(--portal-border);
    border-radius: 0.6rem;
    padding: 1rem;
}
.portal-contact-where {
    display: flex;
    align-items: flex-start;
    gap: 0.6rem;
    margin: 0;
    font-size: 0.88rem;
    color: var(--portal-ink-soft);
}
.portal-contact-where .portal-icon { flex: none; color: var(--portal-brand); margin-top: 0.1rem; }
.portal-contact-where > span { min-width: 0; }
/* With no heading above them the buttons need no divider - the address is one
   quiet line, not a section. */
.portal-contact .portal-company-actions { border-top: 0; padding-top: 0; }

/* Contact rows: icon, label, value. */
/*
 * Deliberately small - "Contact" is where to reach us, not the reason a
 * borrower is on this page (that's "what we lend" and "how it works",
 * above it now). One light card holding every item, not one heavy bordered
 * box per item as this used to render: on a page where the primary content
 * is now the loan offer and the process, four full-height cards for a phone
 * number and an address were pulling more visual weight than the content
 * they support.
 */
.portal-contacts {
    list-style: none; margin: 0; padding: 0.8rem 0.9rem;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
    gap: 0.4rem 1rem;
    background: var(--portal-surface);
    border: 1px solid var(--portal-border);
    border-radius: 0.6rem;
}
.portal-contacts li {
    display: flex;
    align-items: baseline;
    gap: 0.45rem;
    min-width: 0;
}
.portal-contacts li > .portal-icon { flex: none; color: var(--portal-brand); align-self: center; }
.portal-contacts li > div { min-width: 0; display: flex; gap: 0.3rem; flex-wrap: wrap; align-items: baseline; }
.portal-contacts span {
    font-size: 0.72rem;
    color: var(--portal-ink-soft);
    font-weight: 600;
}
.portal-contacts span::after { content: ":"; }
.portal-contacts a,
.portal-contacts strong {
    font-weight: 500;
    font-size: 0.85rem;
    color: var(--portal-ink);
    /* A long email or URL must wrap rather than push the card sideways. */
    word-break: break-word;
}
.portal-contacts a { color: var(--portal-brand); }

/* ---------- "how it works" ---------- */
.portal-steps-list { list-style: none; margin: 0; padding: 0; counter-reset: none; }
.portal-steps-list li {
    display: flex;
    align-items: flex-start;
    gap: 0.75rem;
    background: var(--portal-surface);
    border: 1px solid var(--portal-border);
    border-radius: 0.5rem;
    padding: 0.85rem;
    margin-bottom: 0.4rem;
}
.portal-steps-list .portal-instalment-no { margin-top: 0.1rem; }
.portal-steps-list strong { display: block; font-size: 0.95rem; }
.portal-steps-list span { display: block; font-size: 0.85rem; color: var(--portal-ink-soft); line-height: 1.45; margin-top: 0.15rem; }

/*
 * On a wide screen, the four stages read better left to right, in the order
 * they actually happen, than stacked - the same reason a printed process
 * diagram runs horizontally. Stays the stacked list above on a phone, where
 * four side-by-side columns would squeeze each stage's text unreadably
 * narrow; the connecting line is themed to disappear along with the layout.
 */
@media (min-width: 768px) {
    .portal-steps-list {
        display: grid;
        grid-template-columns: repeat(4, 1fr);
        gap: 0.6rem;
    }
    .portal-steps-list li {
        flex-direction: column;
        align-items: center;
        text-align: center;
        margin-bottom: 0;
        position: relative;
    }
    /* The connecting line between step dots - drawn on every step but the
       first, running from the previous column's centre into this one's. */
    .portal-steps-list li:not(:first-child)::before {
        content: "";
        position: absolute;
        top: 1.75rem;
        left: -50%;
        width: 100%;
        height: 2px;
        background: var(--portal-border);
        z-index: 0;
    }
    .portal-steps-list .portal-instalment-no {
        position: relative;
        z-index: 1;
        margin: 0 0 0.6rem;
    }
}

/* ---------- About page: hero identity, trust stats, branch list ---------- */
.portal-company-name {
    font-size: 1.3rem;
    font-weight: 800;
    color: var(--portal-brand-dark);
    margin: 0 0 0.35rem;
}
.portal-company-mission {
    color: var(--portal-ink-soft);
    font-size: 0.95rem;
    line-height: 1.5;
    margin: 0 0 0.75rem;
    max-width: 34rem;
}

/*
 * Real, computed figures (portalTrustStats() - never a fabricated number),
 * as a strip that reads left to right like a stat bar on any professional
 * institutional site. auto-fit rather than auto-fill, deliberately unlike
 * .portal-loan-list: there are at most four of these and they are meant to
 * fill the width they're given, not stay a fixed card size.
 */
.portal-stats-strip {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
    gap: 0.6rem;
    margin: 0 0 1.5rem;
}
.portal-stat-tile {
    background: var(--portal-surface);
    border: 1px solid var(--portal-border);
    border-radius: 0.6rem;
    padding: 0.85rem 0.6rem;
    text-align: center;
}
.portal-stat-tile strong {
    display: block;
    font-size: 1.25rem;
    font-weight: 800;
    color: var(--portal-brand);
    font-variant-numeric: tabular-nums;
    line-height: 1.15;
}
.portal-stat-tile span {
    display: block;
    font-size: 0.68rem;
    color: var(--portal-ink-soft);
    text-transform: uppercase;
    letter-spacing: 0.03em;
    margin-top: 0.25rem;
}

/* Branch cards reuse .portal-loan/.portal-loan-list's card + grid treatment
   exactly (see below) - a branch and a loan product are both "a card in a
   grid of short facts", so the existing component fits without inventing a
   near-duplicate. Only this text-specific bit is new. */
.portal-branch-city { color: var(--portal-ink-soft); font-size: 0.82rem; margin-top: 0.15rem; }

/* ---------- the company line at the foot of every page ---------- */
/*
 * Sits outside .portal-container (it follows </main>), so it carries the gutter
 * itself. Muted on purpose: it is a signature, not a call to action.
 */
.portal-footer {
    padding: 1.25rem var(--portal-gutter) 1.5rem;
    text-align: center;
    color: var(--portal-ink-soft);
    font-size: 0.78rem;
    border-top: 1px solid var(--portal-border);
    margin-top: 1.5rem;
}
.portal-footer p { margin: 0 0 0.25rem; }
.portal-footer a { color: var(--portal-ink-soft); }
.portal-footer a:hover { color: var(--portal-brand); }

/* Links only. The company name is named once, in the header, and the header
   never scrolls away - repeating it here put it on screen three times in one
   scroll, which reads as an unedited template rather than as branding. */
.portal-footer-links { display: flex; justify-content: center; gap: 0.4rem; flex-wrap: wrap; }

/* ---------- the header brand lockup ---------- */
/*
 * min-width: 0 on the brand and flex: none on the buttons is the pair of rules
 * that actually fixes this. A flex item defaults to min-width: auto, so the
 * brand refused to shrink below its text and pushed the language and sign-out
 * buttons off their own row - which is what produced a three-line company name
 * sitting on top of the buttons.
 */
.portal-brand {
    display: flex;
    align-items: center;
    gap: 0.55rem;
    min-width: 0;
    flex: 1 1 auto;
    /* Breathing room before the menu button, so a two-line name never touches it. */
    padding-right: 0.75rem;
}

/*
 * Two lines maximum, then ellipsis.
 *
 * Clamping rather than nowrap-with-ellipsis: two lines fit almost every real
 * institution name in full, and truncating "BEJUNDAS FINANCIAL SERVICES LTD" to
 * "BEJUNDAS FINAN…" would show a name the company does not use. Clamp is the
 * safety net for the genuinely enormous, not the normal case.
 */
.portal-brand-name {
    min-width: 0;
    font-size: 0.8rem;
    font-weight: 700;
    line-height: 1.15;
    letter-spacing: 0.01em;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    /* Long unbroken words (a single 30-character name) still have to give way
       rather than force the row wider. */
    overflow-wrap: anywhere;
}

@media (min-width: 576px) {
    /* With room, one line at a comfortable size - the shape a brand should have. */
    .portal-brand-name { font-size: 1rem; -webkit-line-clamp: 1; letter-spacing: 0; }
    .portal-mark--header { width: 2.35rem; height: 2.35rem; font-size: 0.8rem; }
}

@media print {
    /* Tappable navigation has no meaning on paper. The identity block and the
       company line stay: on a printed page they are the two things that say
       whose account this is and who holds it. */
    .portal-actions { display: none !important; }
    .portal-identity,
    .portal-alert-card,
    .portal-footer { break-inside: avoid; }
    /* A watermark that is invisible on screen turns into a grey smear on a
       cheap printer, and costs ink for nothing. */
    .portal-company::after { display: none; }
}

/* ---------- motion ---------- */
/*
 * One 320ms rise on the cards that lead the page, staggered by 60ms.
 *
 * Deliberately restrained, and deliberately CSS-only. The portal ships no
 * JavaScript, so this cannot be scroll-driven or staged - and it should not be.
 * The purpose is to make the page feel like it settled rather than appeared,
 * which needs one short movement at load, not motion that continues while
 * somebody is trying to read a balance. Nothing here loops, and nothing moves
 * after the first third of a second.
 *
 * `both` keeps the pre-animation state applied during the delay, so a card with
 * a 180ms delay does not flash at full opacity and then start over.
 *
 * The reduced-motion block further down disables all of it - that rule is why
 * this is safe to add at all. Note it uses `animation: none`, which means these
 * elements simply render in their final state.
 */
@keyframes portalRise {
    from { opacity: 0; transform: translateY(0.4rem); }
    to   { opacity: 1; transform: none; }
}

/*
 * The About page hero watermark's one-time settle - a touch grander than
 * portalRise above (it's the largest element on the page, sitting behind
 * everything else), still one movement, still finished in well under a
 * second, still never repeats. Two variants because the letterform and the
 * real logo rest at different final opacities (0.05 vs 0.08); animating
 * both "to opacity: 1" and letting a lower resting value cut in afterward
 * would make the watermark flash bright and then visibly dim.
 */
@keyframes portalHeroSettle {
    from { opacity: 0; transform: scale(1.08) translateY(0.5rem); }
    to   { opacity: 0.05; transform: none; }
}
@keyframes portalHeroSettleLogo {
    from { opacity: 0; transform: scale(1.08) translateY(0.5rem); }
    to   { opacity: 0.08; transform: none; }
}

.portal-identity  { animation: portalRise 0.32s ease-out both; }
.portal-alerts    { animation: portalRise 0.32s ease-out 0.06s both; }
.portal-overview  { animation: portalRise 0.32s ease-out 0.12s both; }

/*
 * The sequence's last beat: the first panel below the headline card, on
 * mobile and tablet the last thing typically still visible without
 * scrolling on load. Panels further down deliberately do NOT get this - an
 * entrance animation on content nobody has scrolled to yet fires and finishes
 * before it is ever seen, which is wasted motion, not restraint.
 *
 * An explicit class (.portal-panel-lead, set on this one panel in
 * portal/index.php), NOT :first-of-type - .portal-overview sits directly
 * before the panels and is itself a <div>, so ":first-of-type" among divs
 * would have matched THAT, never a .portal-panel at all. Caught by rendering
 * the actual page and reading the real markup rather than assuming the
 * selector was right.
 */
.portal-panel-lead { animation: portalRise 0.32s ease-out 0.18s both; }

/* The brand mark fades in on its own, a touch slower: the logo is the one
   element people look at while the rest of the page is still arriving. */
.portal-mark { animation: portalRise 0.45s ease-out both; }

/*
 * The one number every customer opens this page to read, given a touch more
 * presence than everything arriving around it - a small scale-and-fade
 * rather than the plain rise the cards use, so it reads as the moment that
 * actually matters landing, not just one more element appearing. Delayed to
 * settle just after its own card (.portal-overview, 0.12s) does. Still one
 * short movement, still never loops, still killed outright by the
 * reduced-motion block below.
 */
@keyframes portalEmphasis {
    from { opacity: 0; transform: scale(0.92); }
    to   { opacity: 1; transform: none; }
}
.portal-due-amount { animation: portalEmphasis 0.3s cubic-bezier(0.22, 1, 0.36, 1) 0.2s both; }

/*
 * ---------- Promo carousel (dashboard) ----------
 *
 * Six slides, ten seconds each, one pure-CSS loop - no JavaScript, matching
 * the rest of this page (see assets/js/portal.js's own header on why).
 *
 * All six children share one 60s @keyframes animation and are stacked with
 * position: absolute so they overlap in the same box. Each slide's
 * animation-delay is staggered by 10s (nth-child below) - a POSITIVE delay,
 * not negative: with no fill-mode set, an element not yet animating renders
 * its plain (non-keyframe) CSS, which is the same opacity:0 the keyframe's
 * own 0% state uses, so a not-yet-due slide simply stays invisible until its
 * delay elapses and its local clock starts at the keyframe's 0%. That 0%
 * state is exactly the point each slide is meant to become visible, so the
 * six slides land back-to-back with no gap and no early flash.
 *
 * The motion itself pairs the horizontal travel with a scale (0.95 -> 1),
 * eased with the same "premium" curve already used for .portal-due-amount
 * above (cubic-bezier(0.22, 1, 0.36, 1)) rather than a flat linear slide -
 * one recognisable settle used everywhere something on this page arrives
 * with emphasis, not a second unrelated motion style invented for this one
 * component. Still one consistent direction of travel (right to left, never
 * a bounce back), just with more weight to the arrival and departure.
 *
 * The progress bar underneath (.portal-promo-progress) is built the exact
 * same way - one @keyframes, six segments, staggered by animation-delay -
 * so a segment fills while its slide is showing and stays lit afterwards,
 * the same "story bar" affordance rather than a plain unmarked timer.
 *
 * The brand's own deep navy (--portal-brand), not a new colour - this is
 * the one panel on the page meant to read as "an offer from us" rather than
 * "your account data", so it deliberately breaks from the white cards
 * around it. --portal-accent (brass) marks the two moments that should
 * stand out even further: the call-to-action button and the two human
 * "welcome / thank you" slides - its documented job (see the PALETTE
 * comment above) is exactly this, and until now nothing on the page
 * actually used it for a button.
 */
.portal-promo {
    background: linear-gradient(135deg, var(--portal-brand), var(--portal-brand-dark));
    border-radius: 0.75rem;
    color: #fff;
    margin-bottom: 1.5rem;
    overflow: hidden;
    padding: 1.75rem 1.5rem 1.25rem;
}

.portal-promo-track {
    position: relative;
    min-height: 7rem;
}

.portal-promo-slide {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
    opacity: 0;
    transform: translateX(2.25rem) scale(0.95);
    animation: portalPromoCycle 60s cubic-bezier(0.22, 1, 0.36, 1) infinite;
}

.portal-promo-slide:nth-child(1) { animation-delay: 0s; }
.portal-promo-slide:nth-child(2) { animation-delay: 10s; }
.portal-promo-slide:nth-child(3) { animation-delay: 20s; }
.portal-promo-slide:nth-child(4) { animation-delay: 30s; }
.portal-promo-slide:nth-child(5) { animation-delay: 40s; }
.portal-promo-slide:nth-child(6) { animation-delay: 50s; }

/*
 * Percentages are fractions of the shared 60s cycle, so 16.667% is this
 * one slide's own 10-second slot. Enters sliding in from the right while
 * scaling up to full size, exits continuing further left while scaling
 * back down - the scale is what turns a flat slide into something that
 * reads as arriving and settling, not just sliding sideways.
 */
@keyframes portalPromoCycle {
    0%      { opacity: 0; transform: translateX(2.25rem) scale(0.95); }
    3%      { opacity: 1; transform: translateX(0) scale(1); }
    14%     { opacity: 1; transform: translateX(0) scale(1); }
    16.667% { opacity: 0; transform: translateX(-2.25rem) scale(0.95); }
    100%    { opacity: 0; transform: translateX(-2.25rem) scale(0.95); }
}

.portal-promo-slide h2 {
    font-size: 1.2rem;
    font-weight: 700;
    margin: 0 0 0.4rem;
}

.portal-promo-slide p { margin: 0; opacity: 0.92; }

.portal-promo-amount { font-size: 1.35rem; font-weight: 700; }

.portal-promo-reasons {
    display: flex;
    flex-wrap: wrap;
    gap: 0.6rem 1.5rem;
    list-style: none;
    margin: 0;
    padding: 0;
}
.portal-promo-reasons li { display: flex; align-items: center; gap: 0.5rem; }
.portal-promo-reasons .portal-icon { color: var(--portal-accent); flex: none; }

/* The trailing "...and more" line - no icon, so it doesn't look like a
   fifth reason among equals, deliberately quieter than the four above it. */
.portal-promo-reason-etc { opacity: 0.75; font-style: italic; }

.portal-promo-warm h2 { color: #f0d9b0; }

.portal-promo-btn {
    background: var(--portal-accent);
    border: none;
    color: #fff;
    display: inline-block;
    margin-top: 0.65rem;
}
.portal-promo-btn:hover { background: var(--portal-accent-dark); color: #fff; }

/* Six thin bars, one per slide - lights up while its slide shows, stays lit
   after (the same "story bar" pattern Instagram/WhatsApp status use), so a
   customer glancing at the card mid-cycle can see how far through it is and
   that it will loop rather than end. */
.portal-promo-progress {
    display: flex;
    gap: 0.35rem;
    margin-top: 1.1rem;
}
.portal-promo-progress-segment {
    flex: 1 1 0;
    height: 0.2rem;
    background: rgba(255, 255, 255, 0.22);
    border-radius: 999px;
    overflow: hidden;
}
.portal-promo-progress-fill {
    display: block;
    height: 100%;
    width: 0;
    background: var(--portal-accent);
    border-radius: inherit;
    animation: portalPromoProgress 60s linear infinite;
}
.portal-promo-progress-segment:nth-child(1) .portal-promo-progress-fill { animation-delay: 0s; }
.portal-promo-progress-segment:nth-child(2) .portal-promo-progress-fill { animation-delay: 10s; }
.portal-promo-progress-segment:nth-child(3) .portal-promo-progress-fill { animation-delay: 20s; }
.portal-promo-progress-segment:nth-child(4) .portal-promo-progress-fill { animation-delay: 30s; }
.portal-promo-progress-segment:nth-child(5) .portal-promo-progress-fill { animation-delay: 40s; }
.portal-promo-progress-segment:nth-child(6) .portal-promo-progress-fill { animation-delay: 50s; }

@keyframes portalPromoProgress {
    0%      { width: 0; }
    16.667% { width: 100%; }
    100%    { width: 100%; }
}

/* Reduced motion: the shared rule above kills every animation outright, so
   without this the whole carousel would render as six invisible slides and
   six empty progress bars - the first slide and its bar are pinned to their
   "settled"/"complete" state instead of being animated in. */
@media (prefers-reduced-motion: reduce) {
    .portal-promo-slide:first-child { opacity: 1; transform: none; }
    .portal-promo-progress-segment:first-child .portal-promo-progress-fill { width: 100%; }
}

@media (max-width: 575.98px) {
    .portal-promo { padding: 1.4rem 1.1rem 1.1rem; }
    .portal-promo-slide h2 { font-size: 1.05rem; }
    .portal-promo-reasons { flex-direction: column; gap: 0.5rem; }
    .portal-promo-track { min-height: 9rem; }
}

/* ---------- Phase 7: profile / notices ---------- */
.portal-notice {
    background: #dff0e7;
    border: 1px solid #bfe0cf;
    color: var(--portal-ok);
    border-radius: 0.5rem;
    padding: 0.7rem 0.9rem;
    margin-bottom: 1rem;
    font-size: 0.92rem;
}
