/* ==========================================================================
   Yeoman Portal — brand + layout
   Single source of truth for portal styling. Components carry classes only.
   ========================================================================== */

:root {
    /* Brand (from yeomantax.com) */
    --brand-dark: #0C0224;      /* deep indigo — dark chrome */
    --brand-dark-2: #052234;    /* secondary navy */
    --brand-accent: #3BCBFF;    /* sky blue — on dark surfaces */
    --brand-link: #0d7fb0;      /* accessible cyan — links on light surfaces */
    --brand-on-dark: #FFFFFF;

    /* Neutrals */
    --bg: #f4f6fa;
    --surface: #ffffff;
    --border: #e2e6ee;
    --text: #1a1c2a;
    --text-muted: #5b6472;

    /* THE ONLY RED IN THE PALETTE, and it is a POSITION marker, not a status colour. It marks where
       TODAY falls in a date-ordered table (see .apdates__today) and means "you are here" — nothing
       about good, bad, overdue or closed. The note by .pill--done still stands: adding a red that
       means "closed" or "failed" is an app-wide decision and this is not it. Do not reuse this
       variable for a pill, a row state, or anything carrying a judgement. */
    --today-line: #c62828;

    --radius: 10px;
    --radius-sm: 6px;
    --shadow: 0 1px 3px rgba(12, 2, 36, 0.08), 0 6px 24px rgba(12, 2, 36, 0.06);
    --font: "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

    /* Photo panel height. ONE value for the hero and the empty placeholder: if they differ, paging
       between a project with photos and one without shifts everything below the card. */
    --gallery-h: 380px;
}

* { box-sizing: border-box; }

html, body {
    margin: 0;
    padding: 0;
    min-height: 100%;
}

body {
    font-family: var(--font);
    color: var(--text);
    background: var(--bg);
    -webkit-font-smoothing: antialiased;
}

h1, h2, h3 { color: var(--brand-dark); margin: 0; }

/* ---- Shell -------------------------------------------------------------- */
.portal-shell {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
}

.portal-header {
    background: var(--brand-dark);
    color: var(--brand-on-dark);
    box-shadow: var(--shadow);
}

.portal-header__inner {
    max-width: 1180px;
    margin: 0 auto;
    padding: 0.85rem 1.5rem;
    display: flex;
    align-items: center;
    gap: 1.25rem;
}

.portal-brand {
    display: flex;
    align-items: center;
    gap: 0.9rem;
    text-decoration: none;
}

.portal-brand__logo { height: 22px; width: auto; display: block; }

.portal-brand__tagline {
    color: var(--brand-accent);
    font-size: 0.72rem;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    white-space: nowrap;
}

.portal-brand__product {
    color: var(--brand-accent);
    font-size: 1.15rem;
    font-weight: 600;
    letter-spacing: 0.01em;
    padding-left: 0.9rem;
    margin-left: 0.2rem;
    border-left: 1px solid rgba(255, 255, 255, 0.22);
    line-height: 1;
}

.login__product {
    color: var(--brand-accent);
    font-size: 1.5rem;
    font-weight: 700;
    letter-spacing: 0.02em;
    margin: 0.7rem 0 0.2rem;
}

.portal-header__account {
    margin-left: auto;
    display: flex;
    align-items: center;
    gap: 0.9rem;
    font-size: 0.85rem;
}

.portal-header__role {
    color: var(--brand-dark);
    background: var(--brand-accent);
    border-radius: 999px;
    padding: 0.15rem 0.65rem;
    font-size: 0.7rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.03em;
}

.portal-header__name { color: rgba(255, 255, 255, 0.9); }

.portal-linkbtn {
    background: none;
    border: none;
    color: var(--brand-accent);
    cursor: pointer;
    font: inherit;
    padding: 0;
}
.portal-linkbtn:hover { text-decoration: underline; }

.portal-main {
    flex: 1;
    width: 100%;
    max-width: 1180px;
    margin: 0 auto;
    padding: 1.75rem 1.5rem;
}


/* ---- Page scaffolding --------------------------------------------------- */
/* Title left, consultant right, tab strip underneath both. align-items: flex-start so the
   consultant block hangs from the TOP of the row -- stretched, a two-line title would centre it and
   it would drift depending on how long the account name is. */
/* TIGHTER AROUND THE WELLS (2026-09-30, asked for): the negative top margin takes .portal-main's 28px
   padding down to 16px on the two pages whose header is pills (this row and .project-detail__bar)
   without touching every other page; the bottom went from 1.25rem to 0.75rem. */
.page__headrow {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 1.5rem;
    margin-top: -0.75rem;
    margin-bottom: 0.75rem;
}
/* The title column takes the slack; min-width: 0 lets a long account name shrink rather than
   shoving the consultant block off the right edge. The margin moved to the ROW above, or the two
   children would each contribute their own gap under the row. */
.page__headrow .page__head { flex: 1 1 auto; min-width: 0; margin-bottom: 0; }
/* Wraps under the title on a narrow screen rather than squeezing both. Once wrapped it goes
   full-width and left-aligned, which is why text-align is not set on the block itself. */
@media (max-width: 640px) {
    .page__headrow { flex-wrap: wrap; }
}
.page__head { margin-bottom: 1.25rem; }
.page__title { font-size: 1.6rem; }
/* FocusOnNavigate in Routes.razor focuses the page's <h1> after EVERY navigation, so assistive tech
   lands on the new heading instead of staying where the old page was. To focus a heading at all Blazor
   has to make it focusable (tabindex="-1"), and the browser then draws its own focus ring around the
   title -- which reads as the portfolio NAME being selected, or sitting in an input.
   The RING is removed, not the focus: a heading cannot be operated, so the outline tells the user
   nothing, while the focus move is the whole point of the feature. Deliberately scoped to h1 -- do NOT
   widen it, because every other focus ring in the portal is on something you can actually operate, and
   removing those would leave keyboard users with no idea where they are. */
h1:focus,
h1:focus-visible { outline: none; }
.page__owner {
    margin: 0 0 0.15rem;
    font-size: 0.78rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-muted);
}
.page__owner .portal-link { font-size: inherit; letter-spacing: inherit; }
.page__owner { display: flex; align-items: center; gap: 0.55rem; flex-wrap: wrap; }

.page__sub { color: var(--text-muted); margin: 0.25rem 0 0; }
/* THE WELL (2026-09-30): back button + title in a pill, on the portfolio AND project headers, so the
   two pages open the same way. Matches .consultant--single — same surface, border and padding, and
   the 2.25rem button mirrors the 2.25rem avatar, so on the portfolio the two pills come out the same
   height and frame the header from either side. center, not baseline: the button is a shape, not
   text, so it lines up with the middle of the title. flex 0 1 auto (overriding the headrow's
   1 1 auto) and justify-self: start (in the project grid) make the well hug its title instead of
   stretching across the row. */
.page__well {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.45rem 1.25rem 0.45rem 0.45rem;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--surface);
    min-width: 0;
}
.page__headrow .page__well { flex: 0 1 auto; }
.page__well .page__title { font-size: 1.35rem; overflow-wrap: anywhere; }
/* A client's portfolio has no back button (they have only the one); even padding then, or the title
   sits hard against the pill's left curve. */
.page__well:not(:has(.page__back)) { padding-left: 1.25rem; }
/* The project well holds the customer beside the button, and it MATCHES the portfolio's well title
   exactly (2026-09-30, asked for) — same size, weight, colour, no forced caps — so "‹ customer" is one
   element on both screens. It stays a <p>, not an h1: the roll under it is the project page's
   heading, and it is still the larger of the two (1.6rem against 1.35rem). */
.page__well .page__owner {
    margin: 0;
    font-size: 1.35rem;
    font-weight: 700;
    text-transform: none;
    letter-spacing: normal;
    color: var(--brand-dark);
}
/* The round back button. flex: none keeps a long title from squashing the circle. Filled with the
   consultant avatar's tint so the header's pills read as one family; hover deepens the tint. */
.page__back {
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.25rem;
    height: 2.25rem;
    border-radius: 50%;
    /* Same tint as .consultant__avatar, so the two pills mirror each other end to end. */
    background: color-mix(in srgb, var(--brand-link) 14%, var(--surface));
    color: var(--brand-link);
    transition: background-color 0.15s, transform 0.15s;
}
.page__back:hover { background: color-mix(in srgb, var(--brand-link) 26%, var(--surface)); transform: translateX(-2px); }
.page__back:focus-visible { outline: 2px solid var(--brand-link); outline-offset: 2px; }
.portal-empty { padding: 3rem 0; text-align: center; }

/* ---- Consultant (page header, above the tab strip) ----------------------- */
/* Deliberately LIGHTER than .card -- same surface and border, but no shadow: it sits beside the page
   title rather than in the content column, and at full card weight it competed with the h1 for the
   top of the page. --surface (white) on the page's --bg grey is what makes it read as a panel at
   all; a tinted background would disappear into the header. */
/* COMPACT PILL, 2026-09-30 (replaced the centred three-line card of 2026-08-21, which set the header's
   height on its own): avatar left, name over contact right, left-aligned. Fully rounded only for a
   single person (consultant--single) — it echoes the round back button beside the title; several
   stacked people keep card corners. max-width is sized so email and phone share one line; the
   email is the longest item and is the one that wraps first. */
.consultant {
    flex: 0 1 auto;
    max-width: 26rem;
    padding: 0.45rem 1.1rem 0.45rem 0.45rem;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--surface);
    font-size: 0.8rem;
    line-height: 1.35;
}
.consultant--single { border-radius: 999px; }
.consultant__person { display: flex; align-items: center; gap: 0.65rem; }
/* Only when the scope spans several customers, so this is usually absent. A rule rather than a gap
   in the markup: several consultants stack, and without a divider the second account's name reads
   as another line of the first one's details. */
.consultant__person + .consultant__person {
    margin-top: 0.45rem;
    padding-top: 0.45rem;
    border-top: 1px solid var(--border);
}
.consultant__avatar {
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.25rem;
    height: 2.25rem;
    border-radius: 50%;
    background: color-mix(in srgb, var(--brand-link) 14%, var(--surface));
    color: var(--brand-link);
    font-size: 0.78rem;
    font-weight: 700;
    letter-spacing: 0.02em;
}
.consultant__body { min-width: 0; }
.consultant__account { font-size: 0.7rem; color: var(--text-muted); }
.consultant__name { font-weight: 600; }
.consultant__job { margin-left: 0.4rem; color: var(--text-muted); font-weight: 400; font-size: 0.75rem; }
.consultant__contact { display: flex; flex-wrap: wrap; gap: 0 0.9rem; }
/* An address can be longer than the box; break it rather than letting it set the width. */
.consultant__link { font-weight: 500; overflow-wrap: anywhere; }

/* ---- Links + pills ------------------------------------------------------ */
.portal-link { color: var(--brand-link); text-decoration: none; font-weight: 600; }
.portal-link:hover { text-decoration: underline; }
.portal-link--back { display: inline-block; margin-bottom: 0.5rem; font-weight: 500; }

/* ---- Portfolio search --------------------------------------------------- */
.portal-search { margin-bottom: 1rem; }
.portal-search input {
    width: 100%;
    max-width: 340px;
    padding: 0.55rem 0.8rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    font: inherit;
    color: var(--text);
    background: var(--surface);
}
.portal-search input:focus {
    outline: none;
    border-color: var(--brand-link);
    box-shadow: 0 0 0 3px rgba(59, 203, 255, 0.18);
}

/* ---- Data grid surface -------------------------------------------------- */
.portal-grid {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    overflow: hidden;
}

/* NO SORT ICON ON AN UNSORTED COLUMN, on every grid in the portal (asked 2026-09-30). Radzen draws
   Material's "sort" glyph — three stacked lines — on hover, and it read as a menu rather than as
   "click to sort". Clicking the header still sorts, and the column you ARE sorted by keeps its
   arrow, so the indicator is not lost.
   IT ALSO RECLAIMS WIDTH, which is why this began on the appeals grid: Radzen keeps a flex slot for
   the icon in every sortable header whether or not it is drawing one, ~22px per column — measured
   as ~25-30px of unexplained space after "Year" and "Appeal #", and the reason those columns could
   not go below 65 and 100 without the heading ellipsising.
   Keyed on Radzen's own .rz-data-grid, not .portal-grid, so a grid added without our class is
   covered too. */
.rz-data-grid .rz-sortable-column-icon:not(.rzi-sort-asc):not(.rzi-sort-desc) { display: none; }

/* The row(s) whose map pin is highlighted, tinted with the same yellow family as the selected pin
   (PIN_SELECTED in location-map.js) — the colour is the only thing tying a pin to its rows, so if
   one changes the other has to.
   FLAT FILL, no per-cell accent: an inset box-shadow here lands on every <td>, so what reads as one
   left-hand bar on the row is actually one bar per column. Keep any future emphasis off the cells.
   !important because Radzen sets its own row background at a higher specificity. */
.portal-grid tr.is-map-selected > td {
    background: #FFF6DA !important;
    border-color: #FFF6DA !important;
}
.portal-grid tr.is-map-selected:hover > td { background: #FFEFC2 !important; }


/* ---- Login -------------------------------------------------------------- */
.login {
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1.5rem;
    background: radial-gradient(1200px 600px at 50% -10%, var(--brand-dark-2), var(--brand-dark));
}

.login__card {
    width: 100%;
    max-width: 380px;
    background: var(--brand-dark);
    border: 1px solid rgba(59, 203, 255, 0.18);
    border-radius: 16px;
    padding: 2.25rem 2rem;
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.45);
    text-align: center;
}

.login__logo { height: 26px; width: auto; }
.login__tagline {
    color: var(--brand-accent);
    font-size: 0.72rem;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    margin: 0.6rem 0 1.75rem;
}
.login__title { color: var(--brand-on-dark); font-size: 1.3rem; margin-bottom: 0.35rem; }
.login__sub { color: rgba(255, 255, 255, 0.7); font-size: 0.9rem; margin: 0 0 1.25rem; }
.login__hint { color: rgba(255, 255, 255, 0.55); font-size: 0.78rem; margin: 1rem 0 0; }
.login__error { color: #ff8a8a; font-size: 0.82rem; margin: 0.9rem 0 0; }

/* Form fields */
.field { display: block; text-align: left; margin-bottom: 1rem; }
.field__label {
    display: block;
    color: rgba(255, 255, 255, 0.8);
    font-size: 0.78rem;
    margin-bottom: 0.35rem;
}
.field__input {
    width: 100%;
    padding: 0.6rem 0.75rem;
    border: 1px solid rgba(255, 255, 255, 0.2);
    border-radius: var(--radius-sm);
    background: rgba(255, 255, 255, 0.96);
    color: var(--text);
    font: inherit;
}
.field__input:focus {
    outline: none;
    border-color: var(--brand-accent);
    box-shadow: 0 0 0 3px rgba(59, 203, 255, 0.25);
}

/* Buttons */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    padding: 0.62rem 1rem;
    border-radius: var(--radius-sm);
    border: 1px solid transparent;
    font: inherit;
    font-weight: 600;
    cursor: pointer;
    transition: filter 0.12s ease, opacity 0.12s ease;
}
.btn--block { width: 100%; }
.btn + .btn { margin-top: 0.6rem; }
.btn:disabled { opacity: 0.45; cursor: not-allowed; }
/* An <a> wearing .btn still carries the UA's underline -- .btn styles a <button>, which has none.
   Every anchor-button in the app wants it gone, so it belongs here rather than in each one:
   .project-detail__report had to patch exactly this locally, and it is now redundant, not wrong. */
a.btn { text-decoration: none; }

.btn--primary { background: var(--brand-accent); color: var(--brand-dark); }
.btn--primary:not(:disabled):hover { filter: brightness(1.05); }

.btn--microsoft { background: #ffffff; color: #1b1b1b; }
.btn--microsoft:hover { filter: brightness(0.97); }

.btn--ghost {
    background: transparent;
    color: rgba(255, 255, 255, 0.85);
    border-color: rgba(255, 255, 255, 0.25);
}
.btn--ghost:hover { background: rgba(255, 255, 255, 0.08); }

/* ---- Project detail ----------------------------------------------------- */
/* A GRID, so the controls hold the top-right corner whatever the address does. They span only the
   customer and roll rows; the address runs full width on the row below, under the buttons, where
   the space is. (As a flex row, a long address pushed all the controls onto a row of their own.)
   Row 1 is the well (back button, customer) and the controls pill, centred on each other; the roll
   and the address stack under them as one block — the property's identity. */
/* margin-top: -0.75rem for the same reason as .page__headrow's — the two headers start at one height. */
.project-detail__bar {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    column-gap: 1rem;
    align-items: center;
    margin-top: -0.75rem;
}
.project-detail__well { grid-column: 1; grid-row: 1; justify-self: start; }
.project-detail__right { grid-column: 2; grid-row: 1; }
/* Roll + address on one line, spanning the header's full width. baseline, so the small address sits
   on the title's text line. Flex WRAP decides placement from the address's full width, so a long one
   moves to its own line before anything shrinks; only then does max-width + ellipsis cut it, at the
   WHOLE header's width, with the full text in its title tooltip. */
.project-detail__id {
    grid-column: 1 / -1;
    grid-row: 2;
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    column-gap: 0.9rem;
    margin-top: 0.5rem;
    min-width: 0;
}
/* ONE SIZE FOR BOTH (2026-09-30, asked for): at the default 1.6rem title beside small muted text, the
   address read as a stray caption. Roll and address are now the SAME 1.2rem — weight and colour
   (bold black roll, regular darker-grey address) carry the difference, not size. Change one, change
   the other. */
.project-detail__roll { font-size: 1.2rem; }
.project-detail__address {
    margin: 0;
    max-width: 100%;
    font-size: 1.2rem;
    /* ALL CAPS on screen (2026-09-30, asked for). On the projects looked at that day the street was
       upper-case and the municipality mixed ("… DE PORTLAND, Sherbrooke, QC"), so the line read in two
       cases; not measured across the book. Display only — the tooltip and the data keep their own
       case. */
    text-transform: uppercase;
    color: color-mix(in srgb, var(--text-muted) 55%, var(--text));
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* Street · municipality · province: the dots and the place names a step lighter than the street. */
.project-detail__sep { margin: 0 0.5em; color: var(--text-muted); opacity: 0.6; }
.project-detail__place { color: var(--text-muted); }
/* Sized in em so it tracks the address text. */
.project-detail__pin { width: 0.9em; height: 0.9em; vertical-align: -0.1em; margin-right: 0.3rem; color: var(--brand-link); }
/* Narrow screens: one column, controls under the address. */
@media (max-width: 640px) {
    .project-detail__bar { grid-template-columns: minmax(0, 1fr); row-gap: 0.75rem; }
    .project-detail__right { grid-column: 1; grid-row: 3; justify-self: start; }
    .project-detail__id { margin-top: 0; }
}

.project-nav { display: flex; align-items: center; gap: 0.5rem; }

/* NO .btn--ghost IN THIS HEADER since 2026-09-30: Report and Prev/Next are tinted pills and circles
   with their own rules below, so the white-on-white correction that used to live here (.btn--ghost is
   styled for the dark login card) went with them. Anything added to this row as a .btn--ghost will
   need that correction back. */
.project-nav__pos { color: var(--text-muted); font-size: 0.85rem; min-width: 4.5rem; text-align: center; }
/* Prev/Next as round chevrons (2026-09-30), the same size and tint as .page__back. A <button>, so it
   needs the UA border, padding and font reset that the back <a> never had. */
.project-nav__step {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.25rem;
    height: 2.25rem;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: color-mix(in srgb, var(--brand-link) 14%, var(--surface));
    color: var(--brand-link);
    cursor: pointer;
    transition: background-color 0.15s;
}
.project-nav__step:hover:not(:disabled) { background: color-mix(in srgb, var(--brand-link) 26%, var(--surface)); }
.project-nav__step:focus-visible { outline: 2px solid var(--brand-link); outline-offset: 2px; }
/* At the ends of the list: greyed, not hidden, so the pill does not change width as you page. */
.project-nav__step:disabled { background: #eef0f4; color: var(--text-muted); opacity: 0.6; cursor: not-allowed; }
/* Report: a tinted pill, same 2.25rem height and colours as the round chevrons (2026-09-30). No
   longer a .btn--ghost, so the ghost correction above does not reach it. */
.project-detail__report {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    height: 2.25rem;
    padding: 0 0.95rem 0 0.8rem;
    border-radius: 999px;
    background: color-mix(in srgb, var(--brand-link) 14%, var(--surface));
    color: var(--brand-link);
    font-size: 0.88rem;
    font-weight: 600;
    text-decoration: none;
    transition: background-color 0.15s;
}
.project-detail__report:hover { background: color-mix(in srgb, var(--brand-link) 26%, var(--surface)); }
.project-detail__report:focus-visible { outline: 2px solid var(--brand-link); outline-offset: 2px; }

/* ---- Report download button + progress dialog (ReportDownloadButton) ---- */
/* The count, then the button, hard right of the search box it reports over. The count sits BEFORE
   the button so the button keeps the right edge — a control that moves left as a number beside it
   grows is a control you have to look for. */
.reportdl {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    flex-shrink: 0;
}
.reportdl__note { color: var(--text-muted); font-size: 0.85rem; }
/* NO .btn--ghost CORRECTION NEEDED, unlike the project header's Report link: this is .btn--primary,
   which is coloured for a light surface already. Only the padding is ours — .btn is sized for a
   form's submit button, and this one stands beside a text input.
   THE VERTICAL PADDING MATCHES .portal-search input (0.55rem) ON PURPOSE: the two sit on one row and
   a button a couple of pixels shorter than the box next to it reads as misaligned rather than as a
   different control. The horizontal padding is ours to choose. */
.reportdl__button { padding: 0.55rem 0.9rem; }

/* ---- Portfolio report picker (Portfolio) -------------------------------- */
/* Cancel + Run Report as one right-hand group. .btn + .btn stacks buttons vertically (it is written
   for the login card), and this row is horizontal -- so the margin it adds is taken back here. */
.portfolio-pick { display: flex; align-items: center; gap: 0.75rem; }
.portfolio-pick .btn + .btn { margin-top: 0; }
/* Same correction .reportdl__actions makes, for the same reason: .btn--ghost is coloured for the
   dark login card and is white-on-white on this toolbar. Padding matches .reportdl__button so the
   two controls beside each other are one height. */
.portfolio-pick__cancel {
    color: var(--text);
    border-color: var(--border);
    background: var(--surface);
    padding: 0.55rem 0.9rem;
}
.portfolio-pick__cancel:hover { background: #eef1f7; border-color: var(--text-muted); }

/* ---- Filter box actions (ProjectQuickFilters' Actions) ------------------ */
/* In the portal sheet, not pills.css: the box is shared with the ERP, the corner slot is ours. */
/* padding-right clears TWO 2rem icons and their gap (Excel + Portfolio Report, 2026-10-21). */
.filter-rows--actions { position: relative; padding-right: 5.75rem; min-height: 2.9rem; }
.filter-rows__actions {
    position: absolute;
    top: 0.45rem;
    right: 0.55rem;
    display: flex;
    align-items: center;
    gap: 0.5rem;
}
.filter-rows__note { font-size: 0.8rem; color: var(--text-muted); white-space: nowrap; }

/* EXPORT TO EXCEL as an icon (2026-10-20, asked for green): Excel's own green, #1d6f42, on a pale wash
   of it, so it reads as "spreadsheet" before anyone hovers for the tooltip. The same 2rem round shape as
   the page's other icon controls. */
.portfolio-export {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2rem;
    height: 2rem;
    padding: 0;
    border: 1px solid color-mix(in srgb, #1d6f42 30%, transparent);
    border-radius: 999px;
    background: color-mix(in srgb, #1d6f42 10%, var(--surface));
    color: #1d6f42;
    cursor: pointer;
}
.portfolio-export:hover { background: color-mix(in srgb, #1d6f42 18%, var(--surface)); }
.portfolio-export:focus-visible { outline: 2px solid #1d6f42; outline-offset: 2px; }
.portfolio-export:disabled { opacity: 0.55; cursor: default; }
/* PORTFOLIO REPORT, the same round icon in a PDF red (2026-10-21, asked for), beside the export. A
   FILE-TYPE colour like the Excel green, not a status — so a literal here, NOT --today-line, which
   :root reserves for the date marker. .is-active (the picker is up) fills it, so the icon reads as
   pressed while the report is being aimed. */
.portfolio-report {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2rem;
    height: 2rem;
    padding: 0;
    border: 1px solid color-mix(in srgb, #b3261e 30%, transparent);
    border-radius: 999px;
    background: color-mix(in srgb, #b3261e 10%, var(--surface));
    color: #b3261e;
    cursor: pointer;
}
.portfolio-report:hover { background: color-mix(in srgb, #b3261e 18%, var(--surface)); }
.portfolio-report:focus-visible { outline: 2px solid #b3261e; outline-offset: 2px; }
.portfolio-report.is-active { background: #b3261e; border-color: #b3261e; color: #fff; }
/* "Include Summary": the box and its words as one click target, sized like .reportdl__note. */
.portfolio-pick__summary {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    font-size: 0.85rem;
    color: var(--text);
    cursor: pointer;
    white-space: nowrap;
}

/* The All / None header. Small enough to live inside a 66px column beside a compact grid header --
   which is the whole width budget this column has; see the column's comment in Portfolio.razor. */
.portfolio-pick__head { display: inline-flex; align-items: center; gap: 0.25rem; }
.portfolio-pick__link {
    background: none;
    border: none;
    padding: 0;
    font: inherit;
    font-size: 0.72rem;
    font-weight: 600;
    color: var(--brand-link);
    cursor: pointer;
}
.portfolio-pick__link:hover { text-decoration: underline; }
.portfolio-pick__sep { color: var(--text-muted); font-size: 0.72rem; }
/* accent-color rather than a bespoke box: the browser's own checkbox is the control everyone
   already knows, and this only asks it to wear the brand. */
.portfolio-pick__box { width: 1rem; height: 1rem; cursor: pointer; accent-color: var(--brand-link); }

/* The backdrop is not decoration: it stops the list being re-filtered while a report OF that list is
   being built, which would leave the document describing a set the screen no longer shows. */
.reportdl__backdrop {
    position: fixed;
    inset: 0;
    z-index: 1000;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(12, 2, 36, 0.45);
}
.reportdl__dialog {
    width: min(26rem, calc(100vw - 2rem));
    background: var(--surface);
    border-radius: var(--radius);
    box-shadow: 0 12px 40px rgba(12, 2, 36, 0.35);
    padding: 1.25rem 1.4rem 1.1rem;
}
.reportdl__title { font-size: 1.05rem; margin-bottom: 0.35rem; }
/* min-height so the box does not jump as the line changes between "a moment" and "Still working". */
.reportdl__detail {
    color: var(--text-muted);
    font-size: 0.87rem;
    margin: 0 0 0.9rem;
    min-height: 2.4em;
}
.reportdl__bar {
    height: 6px;
    border-radius: 999px;
    background: #e6eaf2;
    overflow: hidden;
}
.reportdl__fill {
    display: block;
    height: 100%;
    width: 0;
    border-radius: 999px;
    background: var(--brand-accent);
    transition: width 0.2s ease;
}
/* Indeterminate while the server is building: there is no percentage to draw, because the endpoint
   sends nothing until the whole PDF exists. A bar that crept along would be inventing a number. */
.reportdl__fill.is-indeterminate {
    width: 35%;
    transition: none;
    animation: reportdl-slide 1.1s ease-in-out infinite;
}
@keyframes reportdl-slide {
    0% { transform: translateX(-100%); }
    100% { transform: translateX(320%); }
}
/* Respect a reader who has asked for less motion: hold the bar still and let the elapsed counter in
   the line above carry the "still working" message on its own. */
@media (prefers-reduced-motion: reduce) {
    .reportdl__fill.is-indeterminate { animation: none; width: 100%; opacity: 0.35; }
}
.reportdl__actions { display: flex; justify-content: flex-end; margin-top: 1rem; }
/* .btn--ghost is styled for the dark login card and would be white-on-white here. The project header
   carried the same correction until its buttons stopped being ghosts (2026-09-30). */
.reportdl__actions .btn--ghost {
    color: var(--text);
    border-color: var(--border);
    background: var(--surface);
    padding: 0.45rem 0.9rem;
}
.reportdl__actions .btn--ghost:hover { background: #eef1f7; border-color: var(--text-muted); }

.project-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1.25rem;
    transition: opacity 0.12s ease;
}

/* Mid-navigation: the previous project stays on screen, visibly stale, instead of the page
   collapsing to a "Loading…" line and springing back. Pointer events off so a click can't land on
   the outgoing project's controls. */
.project-grid.is-updating { opacity: 0.5; pointer-events: none; }

.card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    padding: 1.25rem 1.4rem;
}
.card__title { font-size: 1rem; margin-bottom: 0.9rem; }

/* align-items: BASELINE is load-bearing. Without it the grid defaults to stretch, and because the
   label is 0.85rem against a 1rem value their text never lines up — a row holding a <code> span or a
   padded .pill grows taller still and the label visibly sinks below its value. Baseline lines the two
   text baselines up regardless of font size, and a pill's baseline is its own inner text, so the
   label lands level with the word inside it. */
.kv { display: grid; grid-template-columns: auto 1fr; gap: 0.5rem 1.1rem; margin: 0; align-items: baseline; }
.kv dt { color: var(--text-muted); font-size: 0.85rem; }
.kv dd { margin: 0; }

/* A PIN can be a list of parcels — wrap them as separate items so three numbers never read as one
   very long one. Plain text since 2026-09-30, so the COMMA is what keeps them apart now (the code
   chips' box used to); it is on the item, not typed into the markup, so a single PIN has none. */
.kv__chips { display: flex; flex-wrap: wrap; gap: 0.3rem 0.4rem; }
.kv__chip:not(:last-child)::after { content: ","; }

/* Legal descriptions run to 231 chars of hand-transcribed survey text. Let it wrap and break, or a
   long unbroken token blows out the grid column the whole details card shares. */
.kv__legal { overflow-wrap: anywhere; line-height: 1.45; }

/* The property code and MPAC's description of it. The description is prose — up to 405 chars — so it
   wraps at the same line height as the legal description above it rather than widening the column. */
.kv__prop { line-height: 1.45; }

.project-stub__note { color: var(--text-muted); font-size: 0.8rem; margin: 1.1rem 0 0; }

.map-placeholder {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.35rem;
    min-height: 220px;
    border: 1px dashed var(--border);
    border-radius: var(--radius-sm);
    background: repeating-linear-gradient(45deg, #f4f6fa, #f4f6fa 12px, #eef1f7 12px, #eef1f7 24px);
    color: var(--text);
    text-align: center;
    padding: 1rem;
}
.map-placeholder__pin { font-size: 1.8rem; }
.map-placeholder__hint { color: var(--text-muted); font-size: 0.78rem; }

/* ---- Project tabs ------------------------------------------------------- */
/* Sticky so the sections stay reachable without scrolling back up — desktop users flip between
   these constantly.
   THE PILL TROUGH since 2026-09-30 (asked for), not an underline strip: the same shape as the
   portfolio's top-level PillTabs (.psub), so the two pages' main tabs look like one app. Styled here
   rather than swapping in PillTabs because this strip carries accent badges PillTabs has no notion
   of. Second level under it is therefore the outlined .viewswitch, as on the portfolio — see
   ProjectSubTabs. max-width + overflow-x keep sixteen tabs scrollable on a narrow screen. */
.ptabs {
    display: flex;
    width: fit-content;
    max-width: 100%;
    gap: 0.15rem;
    padding: 0.2rem;
    margin-bottom: 1.25rem;
    overflow-x: auto;
    position: sticky;
    top: 0;
    background: #eceef3;
    border-radius: 999px;
    z-index: 5;
}

.ptab {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    padding: 0.34rem 0.9rem;
    border-radius: 999px;
    text-decoration: none;
    color: var(--text-muted);
    font-weight: 600;
    font-size: 0.84rem;
    white-space: nowrap;
    transition: background 0.12s ease, color 0.12s ease, box-shadow 0.12s ease;
}
.ptab:hover { color: var(--text); }
/* Lifts out of the trough, exactly as .psub__item.is-active does. */
.ptab.is-active {
    background: var(--surface);
    color: var(--brand-dark);
    box-shadow: 0 1px 2px rgba(12, 2, 36, 0.14);
}
/* An admin-only tab (the Quebec roll), red in every state so it never passes for a client's tab. Two classes beat
   .ptab:hover and .ptab.is-active whatever their order. */
.ptab.ptab--admin,
.ptab.ptab--admin.is-active { color: #b42318; }
.ptab.ptab--admin:hover { color: #8f1c13; }
/* The same on the portfolio strip (PillTabs): the Admin Data tab. */
.psub__item.psub__item--admin,
.psub__item.psub__item--admin.is-active { color: #b42318; }
.psub__item.psub__item--admin:hover { color: #8f1c13; }

.ptab__count {
    display: inline-block;
    min-width: 1.35rem;
    padding: 0.05rem 0.35rem;
    border-radius: 999px;
    /* Translucent, as .psub__count is: the trough behind it is already #eceef3, so a solid badge in
       that grey vanished into it (2026-09-30). */
    background: rgba(12, 2, 36, 0.08);
    color: var(--text-muted);
    font-size: 0.72rem;
    text-align: center;
}
/* Accent = "needs attention". Reserved for counts that mean live work, so it keeps its meaning. */
.ptab__count.is-accent { background: var(--brand-accent); color: var(--brand-dark); }

/* ---- Sub-tabs ----------------------------------------------------------- */
/* A pill-shaped segmented control, NOT a second underline strip. The different shape is what makes
   the hierarchy read — two identical strips stacked would just look like more top-level tabs. */
/* CUSTOMERS GRID: the Customer column has no Width (it takes the slack on desktop), so in this
   fixed-layout table it gets TABLE WIDTH minus the other columns' fixed 640px. Its MinWidth lives
   only on the header cell, which Chrome honours when sizing a fixed table; on a real iPhone the name
   still vanished (2026-09-30). This rule FIXED IT ON THE DEVICE (confirmed the same day); that WebKit
   ignores the header-cell min-width is the inferred reason, not separately proven. The table itself
   is held to 640 + 200 = 840px: every engine then leaves the name its
   200px, and the grid scrolls sideways past the frozen column. Desktop is wider than 840px already,
   so nothing changes there. If a column's Width changes in Customers.razor, change this sum. */
.customers-grid .rz-grid-table { min-width: 840px; }
/* CUSTOMERS GRID SCROLLS ITSELF TOO (2026-10-05), set up exactly as the Projects grid below: the first
   paint's height guess and floor here, the exact fill from nexstackGridFill, and rows held to the 50px
   virtualization estimate so the scroll position restored on the way back from a portfolio lands on
   the same customers. See the two .portfolio-grid rules for why 50 is load-bearing. */
.customers-grid { height: calc(100vh - 18rem); min-height: 320px; }
.customers-grid tr.rz-data-row { height: 50px; }

/* PROJECTS GRID SCROLLS ITSELF (2026-09-30): no pager; a fixed height with its own scrollbar and a
   frozen header. nexstackGridFill (portal.js) sets the exact height inline, filling the window below
   the grid; this is only the FIRST PAINT's guess before that measurement lands, and the floor. */
.portfolio-grid { height: calc(100vh - 18rem); min-height: 320px; }
/* ROWS ARE EXACTLY 50px, AND THE 50 IS LOAD-BEARING. Virtualization lays the scroll area out from an
   ESTIMATED row height — Blazor's Virtualize default, 50px, which Radzen 8.3.2 gives no parameter to
   change — and corrects as it measures. At the old 56.8px rows (72x48 tile) the same scrollTop landed
   on different rows depending on how far it had measured, so coming back from a property restored the
   pixel offset but showed the wrong properties (seen 2026-09-30). With every row exactly the estimate,
   scrollTop <-> row is fixed. Hence the smaller tile below (60x40, same 3:2): 40 + 2x4px padding sits
   inside 50. Change the tile or the padding and re-check that rows still measure 50.00.
   The tile column is hidden since 2026-10-21; the tallest cell is now the roll-over-address pair
   (.portfolio-prop), which has to fit inside the same 50. */
.portfolio-grid tr.rz-data-row { height: 50px; }
.portfolio-grid .portfolio-tile { width: 60px; height: 40px; }
/* TWO-LINE COLUMN TITLES (2026-10-20, asked for) — Tax Year and Current Value, via HeaderCssClass. Radzen
   truncates every title to one line (.rz-text-truncate); these wrap at the space instead, so the column
   only has to fit its longer word beside the sort arrow and funnel. HEADER ROW ONLY: the 50px rule above
   is on data rows, and the virtualization estimate it protects never sees the header. */
.portfolio-grid th.portfolio-grid__wrap-head .rz-column-title-content {
    white-space: normal;
    overflow: visible;
    text-overflow: clip;
    line-height: 1.15;
}
/* A CENTRED TITLE OVER RIGHT-ALIGNED CELLS (2026-10-21, asked for — Current, then titled Value, which sat hard against Owner).
   TextAlign aligns header and cells together, so the header alone is re-centred here, the same
   justify-content Radzen gives a TextAlign.Center title like Year's. Radzen's own 20px left padding,
   which balances the sort arrow, is left as it is.
   THE SELECTOR IS AS LONG AS IT IS ON PURPOSE: Radzen's ".rz-grid-table thead th.rz-text-align-right
   .rz-column-title" is (0,3,2), and the shorter form of this one lost to it (measured 2026-10-21). */
.portfolio-grid .rz-grid-table thead th.portfolio-grid__center-head .rz-column-title { justify-content: center; }
/* A CENTRED TITLE OVER ITS CENTRED VALUES, sorted or not (2026-10-21, asked for — "center the Year values").
   The years WERE centred in their cells; the TITLE was not. Radzen pads a centred title 20px on the left to
   balance the 20px sort arrow, but the arrow is display:none until the column is sorted or hovered, so an
   unsorted "Year" sat ~10px right of the years under it (measured: cell centre 1384px, years 1369-1399,
   title 1381-1407). The padding goes ONLY while the column is unsorted, i.e. while there is no arrow —
   HOVER INCLUDED: the arrow stays hidden on hover too (measured, 0px wide), so keeping the padding there
   made the title jump 10px under the cursor. Sorted, the arrow fills the padding and the title sits
   over the values (1369-1395) as before. Year (TextAlign.Center) and Current (re-centred above) both. */
.portfolio-grid .rz-grid-table thead th.rz-sortable-column.rz-text-align-center .rz-column-title:has(.rzi-sort:not(.rzi-sort-asc):not(.rzi-sort-desc)),
.portfolio-grid .rz-grid-table thead th.rz-sortable-column.portfolio-grid__center-head .rz-column-title:has(.rzi-sort:not(.rzi-sort-asc):not(.rzi-sort-desc)) {
    padding-left: 0;
}
/* THE STATUS BADGE (2026-10-20, asked for): the status's first letter in a square, coloured by the same
   .pill--* class the full-word pill used — so A is the Active blue, T the Transitioning amber, I the
   Inactive grey, with no second copy of the palette. Square corners set it apart from the round pills
   elsewhere; it is a code, not a label. 1.5rem fits well inside the 50px row rule above. */
.status-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.5rem;
    height: 1.5rem;
    border-radius: 4px;
    font-size: 0.75rem;
    font-weight: 700;
    line-height: 1;
    cursor: default;
}

/* ROLL # OVER ADDRESS, the whole cell one link (2026-10-21, asked for). The roll small and muted on
   top, the address in link colour under it. THE ::after IS STRETCHED OVER THE CELL — the td is the
   positioned ancestor, and .rz-cell-data's overflow:hidden does not clip it because that span is not
   positioned — so the cell's padding opens the property too. Two lines at these sizes stay inside the
   50px row rule above. */
.portfolio-grid td.portfolio-prop__cell { position: relative; }
.portfolio-prop { display: block; min-width: 0; color: var(--brand-link); text-decoration: none; }
.portfolio-prop::after { content: ""; position: absolute; inset: 0; }
.portfolio-prop__roll {
    display: block;
    font-size: 0.72rem;
    line-height: 1.2;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}
.portfolio-prop__address {
    display: block;
    line-height: 1.3;
    font-weight: 600;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.portfolio-prop:hover .portfolio-prop__address { text-decoration: underline; }
.portfolio-prop:focus-visible { outline: none; }
.portfolio-prop:focus-visible::after { outline: 2px solid var(--brand-link); outline-offset: -2px; }

/* max-width + overflow-x (2026-09-30): at phone width the portfolio's six tabs ran off the screen and
   dragged the whole page sideways to 722px. Now the strip scrolls inside itself, as .ptabs does. */
.psub {
    display: inline-flex;
    max-width: 100%;
    overflow-x: auto;
    gap: 0.15rem;
    padding: 0.2rem;
    margin-bottom: 1rem;
    background: #eceef3;
    border-radius: 999px;
}

.psub__item {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.34rem 0.9rem;
    border-radius: 999px;
    text-decoration: none;
    color: var(--text-muted);
    font-size: 0.84rem;
    font-weight: 600;
    white-space: nowrap;
    transition: background 0.12s ease, color 0.12s ease, box-shadow 0.12s ease;
}
.psub__item:hover { color: var(--text); }

/* The active segment lifts out of the trough — that raised look is the whole affordance. */
.psub__item.is-active {
    background: var(--surface);
    color: var(--brand-dark);
    box-shadow: 0 1px 2px rgba(12, 2, 36, 0.14);
}

.psub__count {
    padding: 0.02rem 0.35rem;
    border-radius: 999px;
    background: rgba(12, 2, 36, 0.08);
    font-size: 0.7rem;
}

/* ---- View switch (ViewSwitch) ------------------------------------------- */
/* Table or calendar: two PRESENTATIONS of one set of rows, not two destinations.

   A DIFFERENT SHAPE, deliberately. Both pages' top strips are pill troughs (.psub on the portfolio,
   .ptabs restyled to match on the project page, 2026-09-30), so a trough stacked under either would
   read as one long row of tabs, which is the exact confusion .psub's own note describes. That is also
   why ProjectSubTabs wears this shape. An outlined segmented control reads
   as a control. It is the same control at both levels: the same choice must not change shape
   depending on which page you found it on. */
/* FAR RIGHT of its own row. display:flex (block-level) + width:fit-content + margin-left:auto is
   what puts it there without wrapping the control in a positioning div -- inline-flex would leave it
   an inline box that text-align has to chase from a parent this component does not own. It sits
   above the panel rather than beside the tab strip because both pages render the strip themselves
   and the panel underneath; the switch belongs to the panel, and reaching up into the page's row to
   place it would tie this control to two layouts instead of none. */
.viewswitch {
    display: flex;
    width: fit-content;
    margin-left: auto;
    margin-bottom: 0.9rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    overflow: hidden;
}

.viewswitch__item {
    padding: 0.32rem 0.9rem;
    text-decoration: none;
    color: var(--text-muted);
    font-size: 0.82rem;
    font-weight: 600;
    white-space: nowrap;
    transition: background 0.12s ease, color 0.12s ease;
}
/* On the join, not on every segment -- an outer border plus a leading segment border would double up
   against the container's own. */
.viewswitch__item + .viewswitch__item { border-left: 1px solid var(--border); }
.viewswitch__item:hover { color: var(--text); background: rgba(12, 2, 36, 0.03); }

/* The active segment FILLS rather than lifting. .psub's raised look works because it sits in a sunk
   trough; this control is outlined, so there is nothing to rise out of and a shadow would just blur
   the edge. */
.viewswitch__item.is-active { background: var(--brand-dark); color: var(--brand-on-dark); }
/* ProjectSubTabs wears this shape too (since 2026-09-30) and, unlike ViewSwitch, carries counts. It
   sits LEFT, under the tab it belongs to — it is navigation within the tab, not a presentation
   toggle for the panel, so it does not take the far-right spot. */
.viewswitch--left { margin-left: 0; }
.viewswitch__item .psub__count { margin-left: 0.35rem; }
.viewswitch__item.is-active .psub__count { background: rgba(255, 255, 255, 0.2); }

.ptab-body { transition: opacity 0.12s ease; }
.ptab-body.is-updating { opacity: 0.5; pointer-events: none; }

/* The controls pill: same surface, border and radius as the well opposite it. A hairline between
   status | report | paging keeps the three groups apart now that no button has its own border. */
.project-detail__right {
    display: flex;
    align-items: center;
    gap: 0.25rem;
    flex-wrap: wrap;
    padding: 0.3rem;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--surface);
}
/* The status badge at the controls' 2.25rem height (2026-09-30, asked for) — at .pill's own size it sat
   short between full-height buttons. Colours still come from its status class. */
.project-detail__right > .pill {
    display: inline-flex;
    align-items: center;
    height: 2.25rem;
    padding: 0 0.95rem;
    font-size: 0.82rem;
}
/* THE DIVIDERS sit CENTRED in the space between groups, with equal air each side (2026-09-30, asked
   for — the line was hard against the button on its right). margin-left widens that space to
   gap + 0.75rem = 1rem; the line sits at its middle, -0.5rem. Change one, change the other. */
.project-detail__right > .project-detail__report,
.project-detail__right > .project-nav { position: relative; margin-left: 0.75rem; }
.project-detail__right > .project-detail__report::before,
.project-detail__right > .project-nav::before {
    content: "";
    position: absolute;
    left: -0.5rem;
    top: 20%;
    bottom: 20%;
    border-left: 1px solid var(--border);
}
.project-detail__right > .project-nav { gap: 0.15rem; }

/* ---- Dashboard ---------------------------------------------------------- */
.dash__details { margin-bottom: 1.25rem; }

/* The property facts across THREE columns instead of one. A single .kv spans the whole card, so the
   nine short values it now holds would stack down the left edge with two thirds of the card empty.

   EXPLICIT BREAKPOINTS, not auto-fit(minmax()). Each column is a .kv — itself a grid of "auto 1fr" —
   so its natural width is whatever its longest VALUE happens to be, and on a project with a short
   address auto-fit would size the tracks from content and leave the same gap this rule exists to
   close. Fixed 1fr tracks at a stated width divide the card evenly whatever is in them.

   align-items: start so a short column does not stretch its rows to match a tall neighbour. */
.dash__cols { display: grid; grid-template-columns: 1fr; gap: 0.5rem 2.25rem; align-items: start; }

@media (min-width: 45rem) {
    .dash__cols { grid-template-columns: 1fr 1fr; }
}

@media (min-width: 70rem) {
    .dash__cols { grid-template-columns: 1fr 1fr 1fr; }
}

.dash__stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
    gap: 1rem;
    margin-bottom: 1.5rem;
}

/* Photo left, map right (ProjectDashboardMedia). STACKED BY DEFAULT, side by side from 45rem up — the
   same breakpoint the details card goes to two columns at, so the two rows change shape together.
   ONE HEIGHT for both halves, set here and handed to LocationMap as var(--dash-media-h), so the photo
   and the map line up whatever shape the photo is. */
.dash__media { --dash-media-h: 320px; display: grid; grid-template-columns: 1fr; gap: 1rem; margin-top: 1.5rem; }

@media (min-width: 45rem) {
    .dash__media { grid-template-columns: 1fr 1fr; }
}

.dash__mediacard { display: flex; flex-direction: column; min-width: 0; }
/* The title IS the link to its tab. Keeps the heading's own muted colour so the row reads as two
   headings, not two blue links; turning blue on hover is what says it goes somewhere. */
.dash__titlelink { color: inherit; text-decoration: none; }   /* the browser's own underline read as always-on (2026-10-29) */
.dash__titlelink:hover { color: var(--brand-link); }   /* the colour alone, no underline (2026-10-29, asked for) */
/* THE HINT THAT IT OPENS (2026-10-29, asked for, with the underline gone): a drawn chevron after the
   title, muted at rest, blue and nudged right on hover. Drawn with borders so it is crisp at any size
   and takes currentColor; always shown, because a phone has no hover. */
.dash__titlelink::after {
    content: "";
    display: inline-block;
    width: 0.42em;
    height: 0.42em;
    margin-left: 0.4em;
    border-top: 1.6px solid currentColor;
    border-right: 1.6px solid currentColor;
    transform: translateY(-0.08em) rotate(45deg);
    opacity: 0.55;
    transition: transform 0.15s ease, opacity 0.15s ease;
}
.dash__titlelink:hover::after { opacity: 1; transform: translate(0.15em, -0.08em) rotate(45deg); }

.dash__photolink {
    display: block;
    position: relative;
    overflow: hidden;
    border-radius: var(--radius-sm);
    background: #0e1116;
}

/* CONTAIN, not cover — the same call the Photos tab makes: aerials and building shots differ
   wildly, and cropping one to fill the box can cut off the building. The letterbox is no longer
   black (2026-09-30, asked for): .dash__photobg, the same image blurred and cropped, fills behind
   it, so the PHOTO is still never cropped. The dark background above stays for the moment before
   the image loads. */
.dash__photo {
    position: relative;
    display: block;
    width: 100%;
    height: var(--dash-media-h);
    object-fit: contain;
}
/* scale() pushes the blur's soft edge outside the box, where overflow hides it. */
.dash__photobg {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    filter: blur(18px) brightness(0.75);
    transform: scale(1.15);
}

/* The half with nothing in it keeps the row's height, so the other half does not sit beside a stub. */
.dash__mediaempty {
    height: var(--dash-media-h);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.4rem;
    color: var(--text-muted);
    border: 1px dashed var(--border);
    border-radius: var(--radius-sm);
}

.stat {
    background: var(--surface);
    border: 1px solid var(--border);
    border-left: 3px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    padding: 0.95rem 1.1rem;
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
}
.stat__label { color: var(--text-muted); font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.04em; }
.stat__value { font-size: 1.7rem; font-weight: 700; color: var(--brand-dark); font-variant-numeric: tabular-nums; line-height: 1.1; }
.stat__note { color: var(--text-muted); font-size: 0.78rem; }

/* The left rule is the whole signal — no colour unless something is actually live. */
.stat--accent { border-left-color: var(--brand-accent); }
.stat--warn { border-left-color: #e8a33d; }

.dash__block { margin-top: 1.5rem; }
.dash__title { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-muted); margin-bottom: 0.5rem; }

/* Two breakdowns of the same appeals. STACKED BY DEFAULT, side by side from 60rem up.
   An explicit breakpoint rather than auto-fit(minmax()): the matched-height rules below must apply
   ONLY when the cards are actually beside each other, and auto-fit collapses at a width that depends
   on the container, which no media query can be kept in step with. */
.dash__pair { display: grid; grid-template-columns: 1fr; gap: 1.5rem; }

/* Stacked: nothing to match, so the year table just scrolls at a sensible height of its own.
   COMPOUND SELECTOR, not a bare .dash__scroll: this element also carries .ptable-scroll, whose
   max-height:62vh is declared further down this file at the same specificity and would therefore
   win on source order — the box measured 497px instead of 320px. Qualifying it with .ptable-scroll
   takes specificity to 0,2,0 so it wins wherever either rule is moved to. */
.ptable-scroll.dash__scroll { max-height: 20rem; }

@media (min-width: 60rem) {
    .dash__pair { grid-template-columns: 1fr 1fr; align-items: stretch; }

    /* Both cards become full-height flex columns so the table can be told to fill what is left under
       the heading. min-height:0 is required -- without it a flex item refuses to shrink below its
       content and the scroll never engages. */
    .dash__pair > .dash__block { display: flex; flex-direction: column; min-height: 0; }

    /* THE WHOLE TRICK. The scroll box is taken OUT OF FLOW inside this wrapper, so the year table's
       33 rows contribute NOTHING to the grid row's height -- the row is sized by the status card
       alone, and this card then stretches to match and scrolls internally. Floored at 8rem so a
       portfolio with only one or two statuses does not squeeze the years into a sliver. */
    .dash__scrollwrap { position: relative; flex: 1 1 auto; min-height: 8rem; }
    /* Compound for the same reason as above — the 62vh would otherwise clamp this box on a short
       viewport, where the status card is tall enough to push past it. */
    .ptable-scroll.dash__scroll { position: absolute; inset: 0; max-height: none; }
}

/* Appeals-by-status bar. PRESENTATION ONLY -- the count in the column beside it is the fact. Scaled
   against the busiest status rather than the total: against the total, every open bucket is a sliver
   next to Closed and the column says nothing. See PortfolioDashboard.Share. */
.dash__barcell { width: 40%; min-width: 90px; }
.dash__bar { display: block; height: 8px; min-width: 2px; border-radius: 999px; background: var(--brand-accent); }

/* Closed is the largest bucket almost everywhere, so it is muted rather than dropped. Consistent
   with the pill palette note above: grey/green means FINISHED, not bad. Still no red. */
.ptable tr.is-muted td { color: var(--text-muted); }
.ptable tr.is-muted .dash__bar { background: var(--border); }

/* ---- Shared panel tables ------------------------------------------------ */
.ptable { width: 100%; border-collapse: collapse; font-size: 0.85rem; background: var(--surface); }
.ptable th, .ptable td { padding: 0.42rem 0.7rem; border-bottom: 1px solid var(--border); text-align: left; }
.ptable th { color: var(--text-muted); font-weight: 600; white-space: nowrap; }
.ptable .num { text-align: right; font-variant-numeric: tabular-nums; }
.ptable .nowrap { white-space: nowrap; }
/* CENTRED: a tax year, or a short code or counter (an assessment's Revision: 1, 2) — neither text nor money.
   The house alignment rules are in CLAUDE.md ("Table columns"). */
.ptable .center { text-align: center; }
.ptable tbody tr:hover { background: #f7f9fc; }
/* FIT: every column as wide as its content, and the LAST one (a Note, where there is one) takes the rest
   (2026-10-22, Dave: "set all cols to reasonable size and let the note take the rest"). width:1% is the
   shrink-to-content idiom under auto layout; money gets room on its left so figures do not crowd the
   column before them. */
.ptable--fit th:not(:last-child), .ptable--fit td:not(:last-child) { width: 1%; white-space: nowrap; }
.ptable--fit .num:not(:last-child) { padding-left: 2.5rem; }

/* ---- The "today" divider in the Appeal Dates table ---------------------- */
/* A full-width rule with its label in the middle, drawn BETWEEN the last past-due row and the first
   upcoming one. The table is ordered by DueOn, so today has exactly one place to sit and the line
   is the fastest way to read "what has slipped" against "what is coming".

   Its own row rather than a border on the next one: a border would move with that row's hover and
   could not carry the label. padding:0 and no bottom border so the rule IS the row's whole height
   and does not read as a cell. */
.apdates__today td { padding: 0; border-bottom: 0; }
/* Never highlights. It is a marker, not a record, and a hover on it suggests it can be clicked. */
.apdates__today:hover { background: transparent !important; }
.apdates__todaymark {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.15rem 0.7rem;
    color: var(--today-line);
    font-size: 0.72rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    white-space: nowrap;
}
/* The rule itself, growing to fill whatever the label leaves — so the label stays centred as the
   column widths change. 3px because "thick" was the ask and 1px would read as another cell border
   next to .ptable's own. */
.apdates__todaymark::before,
.apdates__todaymark::after {
    content: "";
    flex: 1 1 auto;
    height: 3px;
    background: var(--today-line);
}

/* ---- ScrollPanel (Components/ScrollPanel.razor) ---------------------------- */
/* Every box that scrolls itself (2026-10-06, asked for): one scrollbar look and EVEN SIDES.
   - THIN, SUBTLE BAR. The default 16px track drew a ~5px blank strip between the content and its
     thinner thumb, so the right side read as too wide.
   - scrollbar-gutter: stable both-edges reserves the scrollbar's width on the LEFT too, so content is
     inset equally on both sides instead of flush-left. Stable also stops the content jumping sideways
     when a short table grows enough to need the bar.
   The bar is drawn by the ::-webkit rules (Chrome, Edge, Safari) and by standard properties in Firefox — see below. */
/* padding-top 5px (2026-10-06, asked for): a little air before the first row instead of the heading
   touching the box's top edge. It scrolls away with the rows, so a sticky heading still pins at the very
   top of the box. */
.scroll-panel {
    overflow: auto;
    padding-top: 5px;
    scrollbar-gutter: stable both-edges;
}
/* IN A CARD (scroll-panel--in-card): the even gutter above suits a BORDERED box, but inside a card
   with its own heading it pushed the content ~9px right of that heading and left the bar a card-padding
   away from the edge (Assessments by Year, 2026-10-06). Here the left stays flush with the heading, and
   the box reaches out into the card's right padding so the bar sits by the card's edge — the content's
   right edge still lands a full card-padding in, mirroring the left.
   12px is the bar's width (see the ::-webkit rules below); 3px keeps it off the border. Tied to .card's 1.4rem padding. */
/* AND DOWN into the card's bottom padding (2026-10-06, asked for): stopping at the padding left a blank
   20px band under the rows that read as an empty footer. The padding-bottom keeps the last row's normal
   spacing once scrolled to the end. Tied to .card's 1.25rem vertical padding; 3px clears the border. */
.scroll-panel--in-card {
    scrollbar-gutter: stable;
    margin-right: calc(-1.4rem + 3px);
    padding-right: calc(1.4rem - 3px - 12px);
    margin-bottom: calc(-1.25rem + 3px);
    padding-bottom: calc(1.25rem - 3px);
}
/* THE BAR ITSELF. Chrome, Edge and Safari draw it from these ::-webkit rules — an EXACT size, which
   the standard scrollbar-width cannot give (it is only auto/thin; "thin" read a touch too narrow,
   2026-10-06). A 12px track with a 2px transparent border round the thumb: an 8px visible thumb. Chrome
   IGNORES these if scrollbar-width/-color are set on the element, which is why those standard
   properties now live only in the @supports block below, for Firefox. Change the 12px and the in-card
   padding-right above changes with it. */
.scroll-panel::-webkit-scrollbar { width: 12px; height: 12px; }
.scroll-panel::-webkit-scrollbar-thumb {
    background: #c3c9d4;
    border: 2px solid transparent;
    background-clip: padding-box;
    border-radius: 6px;
}
.scroll-panel::-webkit-scrollbar-thumb:hover { background-color: #9aa3b2; }
.scroll-panel::-webkit-scrollbar-track { background: transparent; }
/* THE FOOTER STRIP (ScrollPanel's Footer, 2026-10-06): the table's summary line joined to the bottom of
   its bordered box instead of floating under it. The frame carries the shadow so box and strip read as
   one object; the box gives up its bottom corners to the strip. Faint tint so it reads as the box's own
   footer, not another row. */
.scroll-panel-frame { border-radius: var(--radius); box-shadow: var(--shadow); }
.scroll-panel-frame > .ptable-scroll {
    box-shadow: none;
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
}
.scroll-panel__foot {
    padding: 0.45rem 0.8rem;
    border: 1px solid var(--border);
    border-top: 0;
    border-radius: 0 0 var(--radius) var(--radius);
    background: #f7f8fb;
    color: var(--text-muted);
    font-size: 0.78rem;
}
/* AN EMPTY FOOTER IS HIDDEN (2026-10-06, asked for), and the box gets its rounded bottom back — a
   blank tinted band reads as broken. ScrollPanel writes @Footer with no whitespace so :empty can match. */
.scroll-panel__foot:empty { display: none; }
.scroll-panel-frame:has(> .scroll-panel__foot:empty) > .ptable-scroll {
    border-bottom-left-radius: var(--radius);
    border-bottom-right-radius: var(--radius);
}
/* Firefox has no ::-webkit-scrollbar, so it gets the standard thin bar in the same colour. */
@supports not selector(::-webkit-scrollbar) {
    .scroll-panel { scrollbar-width: thin; scrollbar-color: #c3c9d4 transparent; }
}

/* Yorkdale carries 611 appeals. One bounded scroll region with a stuck header keeps that readable
   instead of turning the page into a mile of rows. */
.ptable-scroll {
    max-height: 62vh;
    overflow: auto;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
}
.ptable-scroll .ptable thead th {
    position: sticky;
    top: -5px;  /* = .scroll-panel padding-top: pins flush to the box top once scrolled, no rows peeking above */
    background: var(--surface);
    box-shadow: inset 0 -1px 0 var(--border);
    z-index: 1;
}

/* A rule between tax years instead of repeating the year on every row. */
.ptable tr.is-year-start > td { border-top: 1px solid var(--text-muted); }
.ptable__year { font-weight: 700; color: var(--brand-dark); }
/* Repeats stay present but recede, so the first row of each year still reads as the group start. */
.ptable__year.is-repeat { font-weight: 400; color: var(--text-muted); }
/* Amber row = NEEDS ATTENTION. Shared by an open PACN deadline, a due notice, and a past-due appeal
   date. One class for one meaning -- a second amber under another name is how two screens end up
   disagreeing about what is urgent. Still no red anywhere; see the palette note above .pill--muted. */
.ptable tr.is-urgent > td { background: #fff8e8; }
/* Sky row = THE YEARS YOU CAME FROM: a Change notices row matching the Assessment year tag that linked
   here (NoticeYearRange). Accent blue, not amber, because it means "this one", not "act on this". A
   row that is both keeps the amber and still gets the bar, so urgency is never painted over. */
.ptable tr.is-linked:not(.is-urgent) > td { background: rgba(59, 203, 255, 0.12); }
.ptable tr.is-linked > td:first-child { box-shadow: inset 3px 0 0 var(--brand-accent); }

/* Appeal Dates table (AppealDatesTable). Only the two columns that must not reflow are given a
   width: the date carries a time and a relative line under it that read as a wrapped sentence if the
   column narrows, and the appeals column holds a wrapping list of appeal numbers. Property and Type
   take whatever is left, which is where a long address or a 46-character type label needs it. */
.apdates__cdate { width: 8.5rem; }
/* WHO IS ON THE STEP. A width, because a name column with none takes whatever the longest row wants
   and that is taken straight out of the address beside it. 8.5rem, the date column's width, holds all
   twelve names currently assigned to an outstanding step on one line: the longest is "Inactive
   Yeoman" at 15 characters, which at 0.85rem plus the cell's 0.7rem padding needs a shade under 8rem.
   A longer name WRAPS rather than pushing the column out, which is why this is not nowrap. */
.apdates__crep { width: 8.5rem; }
/* Complete / Past due / Today / Upcoming, as a pill. 6.5rem holds the longest of them -- "Past due"
   is 8 characters at the pill's 0.75rem plus its own 0.6rem sides and the cell's 0.7rem, a shade
   under 6rem -- and the pill is told not to wrap, because a two-line chip reads as broken text rather
   than as a label. No colours of its own: the three pill--* classes carry them, so this column and
   the filter pills above it cannot drift apart. */
.apdates__cstatus { width: 6.5rem; }
.apdates__cstatus .pill { white-space: nowrap; }
/* 12rem, not the 8rem this was while the cell held one code or an "N appeals" chip: it now lists the
   appeal numbers, and two "3229897 2017" items per line is what stops a six-appeal step from being
   six rows tall.

   TWO PER LINE IS LOAD-BEARING, not just tidy: the cell caps at THREE LINES, and AppealsPerLine in
   AppealDatesTable.razor turns that budget into the six numbers it names -- or four plus a "+N more"
   toggle, which costs a line of its own. Narrow this and the code will still promise three lines
   while drawing six. */
.apdates__cappeal { width: 12rem; }
/* THE SCHEDULED TIME, on the 586 rows that carry one. Darker and slightly larger than the relative
   line below it -- "9:30 AM" is something you turn up to, and it should not recede the way "in 17
   days" can. Tabular figures so a column of times lines up on the colon. */
.apdates__time {
    font-size: 0.78rem;
    color: var(--brand-dark);
    font-variant-numeric: tabular-nums;
    margin-top: 0.1rem;
}

/* The list of appeal numbers a step covers, shared by BOTH appeal-date views -- the table and the
   calendar's drill-down. One class, because the two are the same list of the same appeals and a
   second copy under another name is how they start to look like different things.

   INLINE-WRAPPING, not one per line: a step covers up to 78 appeals, and a vertical list makes those
   rows taller than the screen. The cells cap what they draw (AppealsShown / ListedAppeals) and say
   how many are behind it, so neither ever claims to be the whole set while it is not. */
.aplist { display: flex; flex-wrap: wrap; gap: 0.15rem 0.5rem; }
.aplist__item { white-space: nowrap; }
/* STEP LABELS, not appeal numbers — the Recently completed block's Type cell. A label runs to 44
   characters ("Additonal Disclosure / Inspection Request (C)") against an appeal number's eight, and
   the nowrap above would push one straight out of the column. So this variant wraps inside an item
   and stacks them one per line rather than flowing them inline: two long labels side by side are
   unreadable even when they fit. */
.aplist--steps { flex-direction: column; gap: 0.1rem; }
.aplist--steps .aplist__item { white-space: normal; }
/* The year rides on the same line as its number rather than under it -- 1,817 of the 1,932
   multi-appeal steps span several years, so the pairing is the information and splitting the two
   onto separate lines would leave the reader matching them up by position. */
.aplist__year { color: var(--text-muted); font-size: 0.72rem; margin-left: 0.2rem; }
/* --brand-link, NOT the --brand-accent .portal-linkbtn defaults to: that sky blue is the palette's
   "on dark surfaces" colour and this button sits on a white table row. */
.aplist__more { font-size: 0.74rem; margin-top: 0.2rem; color: var(--brand-link); }
/* The calendar's and the dashboard's counterpart: TEXT, not a button. Neither list has an expanded
   state -- the Appeal Dates table is where the full set opens -- so a control that looked clickable
   would not be. */
.aplist__rest { display: block; color: var(--text-muted); font-size: 0.72rem; margin-top: 0.2rem; }
/* The dashboard's Upcoming appeal dates block, whose appeals column was a single nowrap code until
   it started listing them. Narrower than the tab's 12rem: this table also carries Days, Type and a
   wrapping address, and it caps at TWO LINES -- either two appeal numbers, or one plus a "+N more"
   that carries the rest on its tooltip. See NamedCount in PortfolioDashboard.razor. */
.dash__cappeal { width: 10rem; }
/* The Show more / Show fewer line under a dashboard block's table — see ShowMoreRows. Baseline-
   aligned rather than stacked so the overflow note reads as a continuation of the button, not as a
   second control; wraps on a narrow layout because the note is a sentence. */
.dash__more { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem; margin: 0.5rem 0 0; }
/* --brand-link, NOT the --brand-accent .portal-linkbtn defaults to: the same correction .aplist__more
   makes, and for the same reason — this control sits on a white block, not a dark surface. */
.dash__morebtn { color: var(--brand-link); font-size: 0.82rem; }
.dash__morenote { color: var(--text-muted); font-size: 0.78rem; }
.ptable__foot { color: var(--text-muted); font-size: 0.78rem; margin: 0.5rem 0 0; }
/* A caption ABOVE a table, where .ptable__foot is the count below it — same muted size, so the two
   bracket the rows as a pair. For tables whose tab label is too short to say what the rows ARE: the
   Assessment tab's "Change notices" names the sub-view without telling a client it means MPAC PACNs.
   max-width because this one carries a sentence, not a count, and a 0.78rem line run the full width
   of a desktop table is unreadable. */
.ptable__cap { color: var(--text-muted); font-size: 0.78rem; margin: 0 0 0.6rem; max-width: 68ch; }
.ptable__cap strong { color: var(--text); }
/* Right-aligned variant, used for the map's property count so it sits under the map's right edge
   rather than competing with the filter labels starting on the left below it. */
.ptable__foot--right { text-align: right; }
/* A link sitting inside that count line. Inherits the line's size so it reads as part of the
   sentence rather than a control bolted onto it. */
.ptable__foot-link {
    background: none;
    border: none;
    padding: 0;
    font: inherit;
    cursor: pointer;
}

/* Inline "working" label that takes a control's place while it runs — the per-portfolio geocode button
   becomes one of these mid-run. Colour only, no font-size: it sits inside a Radzen grid cell whose size
   is not this table's, and setting one here would change the row height the moment the button is
   swapped out. */
.portal-working { color: var(--text-muted); }

/* The staff-only list of properties with no map location, expanded from that count. Indented and
   set apart so it reads as a drill-down rather than a second section of the page. */
.unmapped-list { margin: 0.6rem 0 1rem; }
/* Title and its own Hide control on one line, pushed apart. baseline, not center: the button's text
   has to sit on the title's baseline or it rides visibly high against a 1rem heading -- the same
   thing that put the .kv labels below their values. */
.unmapped-list__head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.75rem;
}


/* Totals row inside the table, so each sum sits under the column it totals. */
.ptable tfoot tr.is-total > td {
    font-weight: 600;
    background: var(--surface);
    border-bottom: 0;
}
/* border-collapse drops the borders of a sticky cell -- draw the rule with an inset shadow instead,
   the same trick the stuck header uses. */
.ptable tfoot tr.is-total > td { box-shadow: inset 0 1px 0 var(--text-muted); }
.ptable tfoot tr.is-total:first-child > td:first-child {
    text-transform: uppercase;
    font-size: 0.72rem;
    letter-spacing: 0.04em;
    color: var(--text-muted);
}
/* A second totals row is a continuation of the first, not a new block -- hairline, not a rule. */
.ptable tfoot tr.is-total + tr.is-total > td { box-shadow: inset 0 1px 0 var(--border); font-weight: 500; }
/* Same blue the Outstanding pill uses, so the row and the pill read as one fact. */
.ptable tfoot tr.is-outstanding > td { color: #0b6a94; }
/* Matches pill--review, for the same reason. */
.ptable tfoot tr.is-writtenoff > td { color: #8a6100; }

/* The decision cell. The ONE column on this table that is allowed to be wide and to wrap: its value
   is the detail ("CHANGE TOTAL VALUE FROM $102,000.00 TO $87,000.00"), so clipping it the way the Rep
   column does would defeat the point of showing it.
   min-width stops the surrounding narrow columns from squeezing it to one word per line; max-width
   stops a 128-character line from pushing the table into horizontal scroll inside .ptable-scroll. */
.ptable__decision {
    min-width: 15rem;
    max-width: 24rem;
}
/* Each statement on its own line. Separate <div>s rather than a joined string: three statements run
   together read as one sentence that does not parse. */
.ptable__decision-line + .ptable__decision-line { margin-top: 0.15rem; }

/* Secondary line under a cell's main content — the forgiven amount beneath a Written off pill.
   Recedes so the pill still leads the cell. nowrap because that use is a short money phrase that
   must never break mid-figure. */
.ptable__sub { color: var(--text-muted); font-size: 0.74rem; margin-top: 0.15rem; white-space: nowrap; }

/* ...but a property ADDRESS is not short: they run to 114 characters, and inherited nowrap was
   clipping most of them rather than wrapping. The modifier exists instead of relaxing .ptable__sub
   because the invoice use genuinely wants nowrap. */
/* Compound on purpose: a bare .ptable__sub--wrap ties with .ptable__sub and would win only on
   source order, which is exactly how .dash__scroll lost to .ptable-scroll earlier and shipped a
   wrong height. 0,2,0 beats 0,1,0 wherever either rule ends up in this file. */
.ptable__sub.ptable__sub--wrap { white-space: normal; }

/* Stick the totals to the bottom of the scroll region: with 2,208 rows the sums are otherwise a
   long scroll away from the row you are reading. */
/* Offsets stack from the bottom up, so each row clears the ones below it. Keyed on nth-last-child
   rather than a flat "not last" rule: the footer runs to three rows (Total / Outstanding / Written
   off) and a single offset would overlap them. Keep in step with the 1.9rem row height. */
.ptable-scroll .ptable tfoot tr.is-total > td { position: sticky; bottom: 0; z-index: 1; }
.ptable-scroll .ptable tfoot tr.is-total:nth-last-child(2) > td { bottom: 1.9rem; }
.ptable-scroll .ptable tfoot tr.is-total:nth-last-child(3) > td { bottom: 3.8rem; }
.roll-muted { color: var(--text-muted); font-style: italic; }

/* Case -> Appeal linkage. The arrow reads as "became". The gap is stated plainly rather than
   coloured like an error: it is scoped to recommendations that genuinely should produce a filed
   appeal (Appeal*, Recon), so it means something — Deemed and Followup are excluded because they
   legitimately produce no appeal record. */
.link-yes { color: var(--brand-link); font-weight: 700; margin-right: 0.2rem; }
.link-gap { color: #8a6100; font-size: 0.78rem; font-style: italic; }

.appeal-summary { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-bottom: 0.85rem; }

/* CENTRED, the same rule and the same reason as .appeals-pane__grid .pill on the portfolio-wide
   appeals list — the two views of the same appeals must not disagree about how a status reads.
   It only shows once a label wraps: "Awaiting Scheduling" breaks over two lines in this column
   (auto layout squeezes it against the Outcome column, which takes what it needs), and ragged-left
   inside a rounded chip reads as broken text rather than as a label. Single-line pills are
   unaffected — the chip shrink-wraps its text, so there is nothing to centre. */
.appeal-status .pill { text-align: center; }

/* Map/satellite radios, above the map on the right so they read as a control ON the map rather than
   another action alongside Verify below it.
   The radio input itself stays in the DOM and is only visually hidden — hiding it with display:none
   would take it out of the tab order and stop arrow keys moving between the two, which is the whole
   reason for using real radios. */
.map-type-toggle { display: flex; justify-content: flex-end; gap: 0.9rem; margin-bottom: 0.35rem; }
.map-type-toggle__option {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    font-size: 0.78rem;
    color: var(--text-muted);
    cursor: pointer;
}
.map-type-toggle__option input { margin: 0; cursor: pointer; }
/* The selected one comes to full strength, so the current state is readable at a glance rather than
   only from the radio dot. */
.map-type-toggle__option:has(input:checked) { color: var(--text); font-weight: 600; }
/* In a toolbar row (the appeals map) rather than above the map: the row already supplies the gap
   below, and the search box beside it takes the free width. */
.map-type-toggle--inline { margin-bottom: 0; }

/* The Verify action under the location map. Staff-only, so it sits below the map rather than
   competing with it — this is a confirmation step, not the point of the page. */
.location-verify {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    flex-wrap: wrap;
    margin-top: 0.75rem;
}
.location-verify__hint { color: var(--text-muted); font-size: 0.78rem; }
/* .btn is styled for forms and stretches; keep it to its content here. */
.location-verify .btn { width: auto; margin-top: 0; }

/* ---- Appeals pane ------------------------------------------------------- */
/* Sizes the appeals view to the viewport so the grid's pager is always on screen. The grid is the
   only flexible child, so it absorbs whatever the search box, map and pills leave — which is why
   hiding the map gives the list its space back with no extra rule.
   --appeals-pane-offset is the chrome ABOVE this pane (portal header, page title, tab strip). It is
   the single value to adjust if the pager sits slightly off the bottom. */
.appeals-pane {
    --appeals-pane-offset: 15rem;
    display: flex;
    flex-direction: column;
    gap: 0;
    height: calc(100vh - var(--appeals-pane-offset));
    /* Below this the grid is not worth showing, so let the page scroll instead of crushing it. */
    min-height: 22rem;
}
/* Open the unmapped work queue and the pane stops being viewport-sized. It has to: the grid is the
   only flexible child here, so the card's height is taken entirely out of the appeals list, which
   collapses to nothing while the pane itself never grows -- leaving the list neither visible nor
   scrollable. With the height released the pane grows to its content and the PAGE scrolls, so the
   list is still reachable underneath the queue.
   The trade is deliberate and temporary: the pager stops being pinned on screen for as long as the
   queue is open, which is the lesser cost of the two. */
.appeals-pane--drilldown { height: auto; }

/* COLUMN WIDTHS ONLY MEAN SOMETHING UNDER FIXED LAYOUT. Radzen's default is table-layout: auto,
   where a declared width is a SUGGESTION and the longest content wins — measured 2026-08-17, the
   Outcome & Decision column took 555px whatever width it was given (300, 380, 400 all ignored),
   squeezing Property to 124px and wrapping a 114-character address to eight lines. Under fixed the
   declared widths are honoured exactly and that address wraps to three.
   SCOPED TO THIS GRID. Switching every portal grid's layout mode is a much larger blast radius, and
   the other grids hold short bounded cells where auto is fine. */
.appeals-pane__grid .rz-grid-table { table-layout: fixed; }

/* TIGHTER GUTTERS, and they buy real estate rather than just looking neater. box-sizing is
   border-box app-wide, so a declared column width INCLUDES its padding — trimming the horizontal
   half from 0.5rem to 0.3rem hands ~6px per column back to the text, which is what lets eight
   columns fit 1,132px without clipping. Vertical is left alone; these rows are already multi-line.
   Retunes Radzen's own variable rather than fighting td selectors, same as the files grids. */
.appeals-pane__grid.rz-data-grid {
    --rz-grid-cell-padding: 0.25rem;
    /* 12px cells (was 14). ONE variable covers everything that was at 14px — the cell text, the
       decision lines and the Filed/Rep columns all inherit it, so there is nothing else to chase.
       The smaller type also buys width back, which is why the columns fit as tightly as they do.
       Deliberately NOT touched: the roll code (11.5px), the address subline (11.8px) and the pills
       (11.2px) were already below 14 and set their own sizes; dragging them down in step would put
       the secondary text under 10px. The HEADER is 16px and is left alone — it was never 14. */
    --rz-grid-cell-font-size: 0.75rem;
}

/* LET CELLS WRAP. Radzen puts overflow:hidden + text-overflow:ellipsis on every .rz-cell-data, which
   is why the Status pill rendered as a cut-off "Awaiting Muni" instead of wrapping to two lines —
   the label reaches 27 characters and an inline-block pill cannot break across lines on its own.
   Rows here are already multi-line (an address, three decision statements), so wrapping costs
   nothing that clipping was buying.
   The Rep column still clips deliberately: .ptable__clip carries its own overflow/ellipsis/nowrap,
   so it is unaffected by relaxing the cell around it. */
.appeals-pane__grid .rz-grid-table td .rz-cell-data {
    overflow: visible;
    text-overflow: unset;
    /* white-space TOO, not just overflow. The theme's base rule sets nowrap on every cell, so
       relaxing only the overflow turned a clipped decision line into one that SPILLS across the next
       column instead of wrapping. Both halves are needed for a multi-line cell. */
    white-space: normal;
}

/* The roll keeps its own size. .code is 0.82em, so dropping the cell to 12px dragged the roll down
   with it to 9.84px — and the roll is the row's identifier, the thing someone is scanning for, at 20
   characters once Ontario formatting is applied. Pinned in rem so it stops tracking the cell. */
.appeals-pane__grid .code { font-size: 0.72rem; }

/* TOP-ALIGNED, because these rows are multi-line and centring makes them read as drifting. A row can
   be one line (Year, Filed) next to four (a chip plus three decision statements) and a wrapped
   address; vertically centring the short cells floats them into the middle of a tall row, so nothing
   lines up with the row's first line. Scoped to this grid — the other portal grids are single-line,
   where middle is right. */
.appeals-pane__grid .rz-grid-table td { vertical-align: top; }

/* ...and CONSTRAIN THE PILL so it wraps instead of spilling. Relaxing the cell above stops the
   "Awaiting Muni" clip, but an inline-block sizes to max-content, so on its own the pill just
   overflowed into the next column (measured 164px of pill in a 111px cell). max-width caps it at the
   column and white-space:normal lets the label break across two lines inside the chip. */
/* CENTRED, which only shows once a label wraps. "Awaiting Municipal Response" breaks over three
   lines at this column width, and ragged-left inside a rounded chip reads as broken text rather than
   as a label — the pill's own shape is symmetrical, so the text has to be. Single-line pills are
   unaffected: the chip shrink-wraps its text, so there is nothing to centre. */
.appeals-pane__grid .pill { max-width: 100%; white-space: normal; text-align: center; }

/* Fixed layout will not break a long token to fit, so anything unbreakable would spill into the next
   column. Decision lines carry figures like $41,193,000.00 and addresses carry long run-on names. */
.appeals-pane__grid .ptable__decision-line,
.appeals-pane__grid .ptable__sub { overflow-wrap: anywhere; }

.appeals-pane__grid {
    flex: 1 1 auto;
    /* Without min-height:0 a flex child refuses to shrink below its content, so the grid would grow
       past the viewport and take the pager with it -- the exact thing this pane exists to stop. */
    min-height: 0;
}

/* The map keeps EXACTLY the height LocationMap was given. Flex children shrink by default, so in a
   height-constrained column the map silently absorbed the squeeze and raising its Height parameter
   did nothing visible -- the grid must give up the space, not the map. */
.appeals-pane > .location-map { flex: 0 0 auto; }

/* Static-map preview shown while the cursor rests on a pin. EXPERIMENTAL — delete this rule with
   the markup if the preview does not stay.
   Fixed, not absolute: the coordinates come from getBoundingClientRect and are viewport-relative.
   translate(-50%, -100%) puts it centred ABOVE the pin, and the margin lifts it clear of the pin
   head so the cursor never sits on the preview itself and bounces mouseleave/mouseenter forever.
   pointer-events:none for the same reason. */
.map-preview {
    position: fixed;
    z-index: 40;
    transform: translate(-50%, -100%);
    margin-top: -0.6rem;
    pointer-events: none;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow);
    padding: 3px;
    line-height: 0;
}
.map-preview img { border-radius: calc(var(--radius-sm) - 2px); display: block; }
/* Our own property photo, sized to the same box the satellite image uses so the preview does not
   jump between a photographed property and one falling back to satellite. Cropped rather than
   letterboxed — photos arrive at all sorts of aspect ratios. */
.map-preview__photo { width: 240px; height: 150px; object-fit: cover; }
/* The roll, under whichever image was shown. line-height is reset because .map-preview zeroes it to
   keep the image flush. */
.map-preview__label {
    display: block;
    padding: 0.3rem 0.2rem 0.1rem;
    font-size: 0.72rem;
    line-height: 1.3;
    color: var(--text-muted);
    text-align: center;
}

/* A cell whose content is wider than its column. Needed because a table cell does not clip on its
   own — without this the text spills into the next column instead of being cut, and the title
   tooltip that goes with it never looks necessary. */
.ptable__clip {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The Notices tab's Note column. .ptable__clip cuts the text, but only inside a box that has a
   width to cut to -- a bare table cell sizes to its content, so without the max-width the longest
   note still sets the column and nothing is ever clipped. min-width stops the narrow date and year
   columns beside it from squeezing it to one word. Same pairing, same reason, as .ptable__decision. */
.ptable__note {
    min-width: 8rem;
    max-width: 18rem;
}

/* An icon standing in for a text link in a table cell -- the Notices tab's "open document".
   display:flex, not inline-block: an inline SVG sits on the text baseline and leaves a few px of
   descender gap under it, which is enough to push this table's 0.42rem row padding taller. Flex
   takes it out of the line box entirely, so a bigger icon does NOT grow the row. */
.ptable__iconlink {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 1.85rem;
    height: 1.85rem;
    color: var(--brand-link);
    text-decoration: none;
    border-radius: var(--radius);
}
/* The icon itself. 1.15rem against the table's 0.85rem text -- deliberately larger than the
   surrounding row, because this is the one thing in it you click. */
.ptable__iconlink svg { width: 1.15rem; height: 1.15rem; display: block; }

/* Hover is a filled target, NOT an underline: text-decoration does nothing to an SVG, so the old
   .portal-link hover left this control with no hover feedback at all once the label became an icon.
   The tinted square also shows how big the click area really is. */
.ptable__iconlink:hover {
    background: #e8f1fb;
    color: var(--brand-dark);
}
/* Keyboard users get the same affordance -- the row is reachable by Tab and the focus ring alone is
   easy to lose against a table this dense. */
.ptable__iconlink:focus-visible {
    background: #e8f1fb;
    color: var(--brand-dark);
}

/* A search box on the left and the list's own control on the right, one row above the list.
   TWO USERS: the Appeals tab (search + map toggle) and the Projects tab (search + Report (PDF)).
   Was .appeals-toolbar while the appeals map was the only thing that needed it; renamed 2026-08-28
   rather than copied, because the two rows are the same row and should not drift apart.
   space-between is what puts the right-hand control against the right edge — the search box has its
   own max-width and will not stretch to fill, so the gap between them is the whole point. */
.list-toolbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    margin-bottom: 0.5rem;
}
.portal-search--inline { margin-bottom: 0; flex: 1 1 auto; }
/* WIDER THAN THE 340px DEFAULT (2026-09-30, asked for): the Projects placeholder — "Search roll #,
   address, owner, municipality, province, BU or group…" — measured 470px of text (Segoe UI 16px,
   Chrome) against 532px inside the box at 560, and at 340 it was cut after
   "municipality,", hiding half of what the box searches. The toolbar has the room beside the
   right-hand button; it still wraps under it on a narrow screen. */
.portal-search--inline input { max-width: 560px; }
.map-toggle__button {
    background: none;
    border: none;
    padding: 0;
    font: inherit;
    font-size: 0.78rem;
    cursor: pointer;
}

/* FILTER PILLS SHOW THEIR COUNT ON HOVER ONLY (2026-10-21, Dave). Rows of numbered pills read as a wall
   of numbers — Projects showed its 477 four times, Appeals its 3,073 five. Portal only: the ERP shares
   FilterPillRow and still shows counts inline. Delete these rules to bring them back.

   The inline <b> count is VISUALLY hidden, not display:none, so a screen reader still reads "Ontario 176".
   The bubble is drawn from data-count, the same number, in the dark chrome colour — a styled bubble
   rather than title=, because the browser's own tooltip waits about a second and cannot be styled.
   Keyboard focus shows it too. */
/* SAME ON THE TABS (same day): the portfolio strip (.psub__item) and the property strip (.ptab). Their
   bubble opens BELOW — the property strip is sticky at the top of the viewport, where one above would be
   off-screen. An accent badge ("needs attention") is not given data-count and stays on its tab. */
.pill-filter b,
.psub__item[data-count] .psub__count,
.ptab[data-count] .ptab__count {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}
.pill-filter[data-count],
.psub__item[data-count],
.ptab[data-count] { position: relative; }
/* THE BUBBLE'S LOOK, ONCE — the count bubbles AND every other tooltip (.ptip, drawn by portal.js for each
   title=; 2026-10-21, asked for: "all the tooltips in the quick filters' style"). Change it here and both
   follow. Placement is per kind, below. */
.pill-filter[data-count]::after,
.psub__item[data-count]::after,
.ptab[data-count]::after,
.ptip {
    padding: 0.18rem 0.5rem;
    border-radius: 6px;
    background: var(--brand-dark);
    color: #fff;
    font-size: 0.72rem;
    font-weight: 600;
    line-height: 1.3;
    box-shadow: 0 4px 12px rgba(12, 2, 36, 0.18);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.12s ease, transform 0.12s ease;
}
.pill-filter[data-count]::after,
.psub__item[data-count]::after,
.ptab[data-count]::after {
    content: attr(data-count);
    position: absolute;
    left: 50%;
    bottom: calc(100% + 6px);
    z-index: 20;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    transform: translate(-50%, 3px);
}
/* EVERY OTHER TOOLTIP. Fixed to the viewport, so a grid cell's overflow:hidden cannot clip it; placed by
   portal.js above its element, or below when there is no room. It WRAPS where a count never needs to:
   an owner name or an MPAC description runs to a sentence. pre-line keeps a title's own line breaks.
   Above the report's progress dialog and the map. */
.ptip {
    position: fixed;
    z-index: 10000;
    max-width: 20rem;
    white-space: pre-line;
    overflow-wrap: anywhere;
    transform: translateY(3px);
}
.ptip.is-below { transform: translateY(-3px); }
.ptip.is-shown { opacity: 1; transform: none; }
/* A pill with a count AND a title shows both in the .ptip, so its own count bubble stands down. */
.pill-filter[data-count].is-tipped::after,
.psub__item[data-count].is-tipped::after,
.ptab[data-count].is-tipped::after { display: none; }
.psub__item[data-count]::after,
.ptab[data-count]::after {
    top: calc(100% + 6px);
    bottom: auto;
    transform: translate(-50%, -3px);
}
.pill-filter[data-count]:hover::after,
.pill-filter[data-count]:focus-visible::after,
.psub__item[data-count]:hover::after,
.psub__item[data-count]:focus-visible::after,
.ptab[data-count]:hover::after,
.ptab[data-count]:focus-visible::after {
    opacity: 1;
    transform: translate(-50%, 0);
}
@media (prefers-reduced-motion: reduce) {
    .pill-filter[data-count]::after,
    .psub__item[data-count]::after,
    .ptab[data-count]::after,
    .ptip { transition: none; }
}
/* The strips scroll sideways (overflow-x: auto) so phones can reach every tab — and a scroll box clips
   anything drawn outside it, the bubble included. Where there is a mouse and room, they stop clipping;
   a strip that ever runs out of room wraps instead of scrolling. Phones keep the scroll and, having no
   hover, never asked for the bubble. */
@media (hover: hover) and (min-width: 768px) {
    .ptabs, .psub { overflow: visible; flex-wrap: wrap; }
}

/* A pill that NAVIGATES, as against .pill-filter which narrows the list under it. Used for the
   "Recently visited" shortcuts on the Customers page.

   DELIBERATELY NOT ONE OF THE .pill--* COLOURS. Every one of those means something -- see the palette
   note by .pill--muted -- and a shortcut to an account is not a claim about that account's state. A
   white chip with the brand link colour reads as a control and borrows nobody's meaning.

   CLIPPED, not wrapped: customer names run long ("PURE INDUSTRIAL REAL ESTATE TRUST"), and a pill
   that wrapped to three lines would dominate the page it is meant to be a shortcut past. The element
   carries the full name in title=. */
.pill-link {
    max-width: 18rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    /* inline-block from .pill, so the clipped text sits on the row's baseline rather than below it. */
    vertical-align: bottom;
    background: var(--surface);
    color: var(--brand-link);
    border: 1px solid var(--border);
    text-decoration: none;
    transition: border-color 0.12s ease, background 0.12s ease;
}
.pill-link:hover { border-color: var(--brand-link); background: #f7fbfd; text-decoration: none; }
.pill-link:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(59, 203, 255, 0.5); }
.code { font-family: Consolas, "SF Mono", monospace; font-size: 0.82em; }
.arrow { color: var(--text-muted); }

.pstate {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.4rem;
    padding: 3rem 1rem;
    color: var(--text-muted);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}
.pstate__icon { font-size: 1.9rem; line-height: 1; display: inline-flex; opacity: 0.75; }   /* holds a LineIcon (1em) since 2026-10-29 */
/* A one-line banner rather than a centred empty-state block — for a positive "nothing to do" that
   sits among other content (the project dashboard's "Nothing outstanding"). */
.pstate--inline { flex-direction: row; justify-content: flex-start; gap: 0.6rem; padding: 0.7rem 1rem; }
.pstate--inline .pstate__icon { font-size: 1.1rem; }
/* Good news, in the teal the Ask record citations use — not an alert green. */
.pstate__ok { flex: none; color: #0f766e; }

/* Space held for data that is NOT IMPORTED YET, as opposed to a query that found nothing. Shorter
   than the full empty state and dashed, so it reads as "not built yet" rather than "nothing found" --
   the two are different answers and a client should not have to guess which one they are looking at. */
.pstate--pending { padding: 1.5rem 1rem; border-style: dashed; text-align: center; }

/* ---- Files tab ----------------------------------------------------------- */
/* Account switcher (only when a login spans several customers — 93 of 1,194 contacts) and the
   Folder/Table toggle. ONE rule for both: they are the same control in two places, and letting them
   drift apart is how a page ends up with two nearly-identical pill styles. */
.files__accounts, .files__toggle { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-bottom: 1rem; }
/* In the toolbar row with the search (2026-10-06): the row owns the spacing, and the toggle holds the
   right edge even when there is no search box beside it. */
.files__bar { margin-bottom: 1rem; }
.files__bar .files__toggle { margin: 0 0 0 auto; }
.files__account, .files__pill {
    font: inherit;
    font-size: 0.82rem;
    padding: 0.35rem 0.7rem;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--surface);
    color: var(--text);
    cursor: pointer;
}
.files__account.is-active, .files__pill.is-active {
    background: var(--brand-dark);
    color: var(--brand-on-dark);
    border-color: var(--brand-dark);
}

/* Breadcrumbs. Buttons rather than links: navigation here is component state, not a route -- the
   real path never reaches the URL, so there is nothing to deep-link to and nothing to leak. */
.files__crumbs { display: flex; flex-wrap: wrap; align-items: center; gap: 0.25rem; margin-bottom: 0.6rem; }
.files__crumb {
    font: inherit;
    font-size: 0.82rem;
    padding: 0.15rem 0.35rem;
    border: 0;
    border-radius: var(--radius-sm);
    background: none;
    color: var(--brand-link);
    cursor: pointer;
}
/* The current folder is the disabled crumb -- present so the trail reads whole, but not a target. */
.files__crumb:disabled { color: var(--text); font-weight: 600; cursor: default; }
.files__sep { color: var(--text-muted); font-size: 0.8rem; }

.files__folder, .files__up {
    font: inherit;
    border: 0;
    background: none;
    padding: 0;
    color: var(--brand-link);
    cursor: pointer;
    text-align: left;
}
.files__up { color: var(--text-muted); }
/* .ptable__clip's display:block is enough for the <span> the appeals grid puts it on — a span fills
   its cell, so the ellipsis has a width to cut against. A BUTTON sizes to its CONTENT even when
   block, so without a ceiling it would simply grow past the column and never truncate. */
.files__folder.ptable__clip { max-width: 100%; }
/* Roomier rows in the file grids ONLY. Both file views run Density.Compact like every other portal
   grid, but the others hold short cells (a roll number, a pill, a date) while these hold filenames
   that wrap to two or three lines -- at compact spacing the wrapped lines of one file ran into the
   next, so this padding does the row separation the density took away.

   RETUNES RADZEN'S OWN VARIABLE rather than overriding td/.rz-cell-data selectors. The theme sets
   --rz-grid-cell-padding: 0.25rem 0.5rem on .rz-density-compact and every cell rule reads it, so
   changing it here is the supported seam: no !important, no specificity race, and it keeps working
   if the theme reorganises which element carries the padding.

   SCOPED UNDER .files deliberately -- the Projects and Appeals grids are dense on purpose and this
   must not loosen them. The horizontal halves are left at the theme's values so column alignment
   still matches the rest of the portal. */
.files .portal-grid.rz-data-grid {
    --rz-grid-cell-padding: 0.6rem 0.5rem;
    --rz-grid-header-cell-padding: 0.5rem 0;
    /* Filenames wrap, so the single-line height the compact theme assumes is too tight for them. */
    --rz-grid-cell-line-height: 1.25rem;
}

/* COLUMN WIDTHS ONLY MEAN SOMETHING UNDER FIXED LAYOUT — the same lesson as .appeals-pane__grid
   above, and the same fix. Radzen's default is table-layout: auto, where a declared width is a
   suggestion and the longest content wins; here that content is the folder path, which would take
   the room the filename is supposed to have. Under fixed, Name is the only column with no width and
   therefore absorbs whatever Folder (190), Size (100) and Modified (120) leave.
   SCOPED TO THIS GRID, for the reason given there: switching every portal grid is a much larger
   blast radius than the problem warrants. */
.files__table .rz-grid-table { table-layout: fixed; }

/* AND FIXED LAYOUT MAKES THIS NECESSARY, which auto did not. Radzen puts overflow:hidden +
   text-overflow:ellipsis + white-space:nowrap on every .rz-cell-data; under auto the column simply
   grew and nothing clipped, so the files grid never needed relaxing. Under fixed the column can no
   longer grow, so a long filename would be cut to an ellipsis instead of wrapping — which is exactly
   what this change was meant to stop.
   BOTH HALVES, per the appeals grid: relaxing only the overflow turns a clipped line into one that
   SPILLS across the next column. overflow-wrap on top of them because filenames carry long unbroken
   runs ("Property_Profile_2110100025269000000") that have no space to break at. */
.files__table .rz-grid-table td .rz-cell-data {
    overflow: visible;
    text-overflow: unset;
    white-space: normal;
    overflow-wrap: break-word;
}

/* Files ARE links now (PortalFileEndpoint streams them). Styled like every other portal link so
   "this opens something" is the same affordance here as everywhere else. */
.files__link { color: var(--brand-link); text-decoration: none; }
.files__link:hover { text-decoration: underline; }
/* Kept for any file rendered as plain text (none today) so the class does not dangle. */
.files__file { color: var(--text); }

/* The count above the table. Carries the truncation caveat when the crawl hit its cap, so the
   number is never read as complete when it is not. */
.files__summary { margin: 0 0 0.6rem; font-size: 0.82rem; color: var(--text-muted); }
.files__summary strong { color: var(--text); }
/* A file sitting directly in the company folder, i.e. no sub-folder. Recedes: it is the absence of
   a folder, not the name of one. */
.files__root { color: var(--text-muted); font-style: italic; }
/* Second line of an empty/loading state -- explains a wait without shouting about it. */
.pstate__hint { font-size: 0.78rem; opacity: 0.75; }

/* Load-more controls under the file table. Radzen pages over what it was given, so reaching file 21
   is an explicit fetch -- each one walks the share again. */
.files__more { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin-top: 0.75rem; }
.files__more-note { font-size: 0.78rem; color: var(--text-muted); }

/* ---- Upload tab (ProjectUploadPanel) ------------------------------------ */
.upload { display: flex; flex-direction: column; gap: 0.6rem; }
/* The zone IS the file input: .upload__input is stretched over it and transparent, so a drop or a
   click anywhere on the zone lands on the browser's own picker. */
.upload__zone {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.3rem;
    padding: 2rem 1rem;
    text-align: center;
    color: var(--text-muted);
    background: var(--surface);
    border: 2px dashed var(--border);
    border-radius: var(--radius);
    transition: border-color 0.15s ease, background 0.15s ease;
}
.upload__zone:hover, .upload__zone:focus-within { border-color: var(--brand-link); background: rgba(59, 203, 255, 0.06); }
.upload__zone.is-busy { opacity: 0.7; }
.upload__input { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; }
.upload__zone.is-busy .upload__input { cursor: progress; }
.upload__icon { font-size: 1.6rem; color: var(--brand-link); }
.upload__lead { color: var(--text); font-weight: 600; }
.upload__browse { color: var(--brand-link); text-decoration: underline; }
.upload__hint { font-size: 0.78rem; }
.upload__heading { font-size: 0.95rem; margin-top: 0.6rem; }

.upload__queue { list-style: none; margin: 0; padding: 0; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); }
/* Name | size | bar | status. The name takes what is left and truncates; the tooltip has it whole. */
.upload__item {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 5rem minmax(4rem, 10rem) minmax(6rem, auto);
    align-items: center;
    gap: 0.7rem;
    padding: 0.45rem 0.7rem;
    font-size: 0.85rem;
    border-bottom: 1px solid var(--border);
}
.upload__item:last-child { border-bottom: none; }
.upload__name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.upload__size { color: var(--text-muted); text-align: right; white-space: nowrap; }
.upload__bar { height: 6px; background: var(--border); border-radius: 3px; overflow: hidden; }
.upload__bar > span { display: block; height: 100%; background: var(--brand-link); transition: width 0.2s ease; }
.upload__status { white-space: nowrap; color: var(--text-muted); }
/* pill--done's finished-green; amber for a failure, the app's "needs attention" — never red (see :root). */
.upload__status.is-done { color: #1f7a45; }
.upload__check { margin-right: 0.3em; vertical-align: -0.12em; }
.upload__status.is-failed { color: #8a5300; white-space: normal; }
.upload__error { margin: 0; font-size: 0.85rem; color: #8a5300; }
.upload__eta { margin: 0; font-size: 0.85rem; color: var(--text-muted); }
.upload__eta strong { color: var(--text); }

.upload__actions { text-align: right; width: 1%; }
.upload__trash, .upload__link {
    border: none;
    background: none;
    padding: 0.15rem 0.35rem;
    font: inherit;
    cursor: pointer;
    border-radius: var(--radius-sm);
}
.upload__trash { opacity: 0.6; }
.upload__trash:hover:not(:disabled) { opacity: 1; background: var(--bg); }
.upload__link { color: var(--brand-link); text-decoration: underline; }
.upload__link--danger { color: #8a5300; font-weight: 600; }
.upload__trash:disabled, .upload__link:disabled { opacity: 0.35; cursor: not-allowed; }
.upload__confirm { color: var(--text-muted); margin-right: 0.2rem; }
/* The Notes tab's Customer/Yeoman badge (.note__badge), beside the uploader's name. Empty — no
   ProjectUpload row for the file — draws nothing rather than a blank pill. */
.upload__who { margin-left: 0.35rem; }
.upload__who:empty { display: none; }

@media (max-width: 600px) {
    /* Bar under the name on a phone: four columns do not fit, and the status is the part to keep. */
    .upload__item { grid-template-columns: minmax(0, 1fr) auto; }
    .upload__size { display: none; }
    .upload__bar { grid-column: 1 / -1; order: 3; }
}

/* ---- Project notes ------------------------------------------------------ */
.note-public { margin: 0; white-space: pre-wrap; }

.note-compose { margin-bottom: 1.25rem; }
.note-compose__box {
    width: 100%;
    padding: 0.6rem 0.75rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    font: inherit;
    color: var(--text);
    resize: vertical;
}
.note-compose__box:focus {
    outline: none;
    border-color: var(--brand-link);
    box-shadow: 0 0 0 3px rgba(59, 203, 255, 0.18);
}
.note-compose__actions {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    margin-top: 0.5rem;
    flex-wrap: wrap;
}
.note-compose__hint { color: var(--text-muted); font-size: 0.78rem; }

.note-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.7rem; }

/* A left rule in the author's colour, so customer and Yeoman entries are told apart down the
   margin without reading the badge on every one. */
.note {
    border: 1px solid var(--border);
    border-left: 3px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 0.6rem 0.85rem;
    background: var(--surface);
}
.note.is-client { border-left-color: var(--brand-accent); }
.note.is-staff { border-left-color: #4b3fb0; background: #fafbfe; }

.note__head { display: flex; align-items: baseline; gap: 0.55rem; flex-wrap: wrap; font-size: 0.78rem; }
.note__author { font-weight: 700; color: var(--brand-dark); }
.note__badge {
    padding: 0.02rem 0.4rem;
    border-radius: 999px;
    background: #eceef3;
    color: var(--text-muted);
    font-size: 0.68rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}
.note__when { margin-left: auto; color: var(--text-muted); }
/* pre-wrap so a customer's line breaks survive; the text itself is Razor-encoded. */
.note__body { margin: 0.35rem 0 0; white-space: pre-wrap; }

/* ---- Quebec roll (admin-only tab) ---------------------------------------- */
/* The summary card, then one card per property the roll number or PIN finds, stacked. */
.qc-roll { display: grid; gap: 1.25rem; }

/* ---- Ontario roll (MPAC) ------------------------------------------------ */
.card__note { color: var(--text-muted); font-size: 0.78rem; font-weight: 400; margin-left: 0.4rem; }

.roll-value { font-variant-numeric: tabular-nums; font-weight: 600; }

/* The summary between the card title and the year table: assessed value and, when there is one, the
   freeze — ON ONE LINE, which is why this is a flex row rather than the .kv grid every other detail
   list on the page uses. .kv is "auto 1fr", so each dt/dd pair takes a row of its own.

   The gap's row half is not decoration: each pair is its own div (see the markup), so a narrow card
   wraps between the PAIRS and that is the space they land with. align-items:baseline for the same
   reason .kv declares it — the 0.85rem label has to sit on the 1rem value's baseline or it rides low.

   margin-bottom carries the gap the removed "Assessment by year" subheading used to supply; without
   it the year table butts straight onto the assessed value. */
.roll-summary {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.5rem 2.25rem;
    margin: 0 0 1.25rem;
}
.roll-summary > div { display: flex; align-items: baseline; gap: 0.6rem; }
.roll-summary dt { color: var(--text-muted); font-size: 0.85rem; }
.roll-summary dd { margin: 0; }

.roll-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
.roll-table th, .roll-table td { padding: 0.35rem 0.6rem; border-bottom: 1px solid var(--border); }
.roll-table th { text-align: left; color: var(--text-muted); font-weight: 600; }
/* A black rule under the column headings, closing the header off from the years below it. The rest
   of the table rules are --border grey, which is what makes this one read as a division rather than
   another row separator.

   var(--text), the product's ink, not #000: nothing else on the page is pure black, and at 1px on
   white the two are indistinguishable anyway. It also follows the token if a dark palette is ever
   added, which a hard-coded black would not.

   thead th, so it cannot reach the ROW headers — the single-class table puts the year in a
   <th scope="row"> in every body row, and a bare `.roll-table th` would draw this line under all
   of them. */
.roll-table thead th { border-bottom: 1px solid var(--text); }
/* Money right-aligned and tabular so the columns line up digit-for-digit down the year history. */
.roll-table .num { text-align: right; font-variant-numeric: tabular-nums; }
/* Seven columns of currency don't fit a phone — scroll the table, never the page. */
.roll-scroll { overflow-x: auto; }
/* +70px each when the Current column joined the two tables (2026-09-09): one more money column,
   holding the same widest figure the others already do. Without the bump the new column squeezes
   the rest instead of extending the scroll, which is the whole job of the min-width. */
.roll-table--year { min-width: 630px; }
.roll-table--years { min-width: 710px; }

/* ASSESSMENTS BY YEAR SCROLL IN THE CARD (2026-10-06, asked for), not the page. Capped at what is left
   of a typical window below the header and tabs, never under 320px.
   THIS BOX OWNS BOTH AXES: the inner .roll-scroll (sideways, for phones) is switched off inside it,
   because a sticky heading sticks only to its nearest SCROLLING ancestor — left on, .roll-scroll would
   be that ancestor, and it never scrolls vertically, so the headings would scroll away. */
/* The calc() is only the FIRST PAINT's guess: OntarioRollPanel measures the real room and sets an
   inline max-height (nexstackGridFill, asMax) — this guess overran the window by ~115px on one roll. */
.roll-years-scroll { max-height: max(320px, calc(100vh - 22rem)); overflow: auto; }
.roll-years-scroll .roll-scroll { overflow: visible; }
/* Frozen headings — SINGLE-CLASS LAYOUT ONLY (.roll-table--years, one long table). NOT the multi-class
   layout (.roll-table--year, one short table per year): there the year's label sits ABOVE its table,
   so a frozen heading row stayed pinned while its "2024" label scrolled away, leaving column names with
   no year (seen 2026-10-06). Each of those tables is a few rows with its heading right on top anyway.
   Background so rows do not show through; the in-force yellow keeps its own. */
.roll-years-scroll .roll-table--years thead th { position: sticky; top: -5px; /* = .scroll-panel padding-top */ z-index: 1; background: var(--surface); }
.roll-years-scroll .roll-table--years thead th.is-in-force { background: #FFF6DA; }

.roll-year { text-align: left; font-weight: 600; white-space: nowrap; }
.roll-year .roll-tag { margin-left: 0.4rem; }
/* Same gap for the "open" tag after an appeal deadline on the Change notices tables (PACN and PRAN);
   without it the tag sat against the date. */
.ptable .roll-tag { margin-left: 0.4rem; }

/* Single-class layout: highlight the current year's row rather than a whole block. */
.roll-table tbody tr.is-current > td,
.roll-table tbody tr.is-current > th {
    background: var(--brand-accent-soft, rgba(255, 196, 0, 0.12));
    font-weight: 600;
}

/* ONE of Yr 1-4 on a year that is still phasing in: the step of the schedule the year is actually
   being assessed on. Never Start, never Dest, and nothing at all on a frozen year — the tint means
   "this figure is in force", not "this figure is interesting".

   #FFF6DA is the portal's existing highlight yellow — the lighter of the two shades
   .is-map-selected uses — NOT a new colour: one yellow family across the product.

   IT WAS THE DEEPER #FFEFC2 UNTIL 2026-09-09 and the swap costs one thing, which is worth knowing
   before anyone lightens it again: the current-year ROW is itself washed in amber (roughly #FFF7DB
   on white), so a marked cell sitting on that row is now nearly invisible against it. The cue is
   not lost — the marked HEADER carries it, and the header sits on white — but a shade lighter still
   would leave the tint doing nothing on the one row a reader looks at first.

   The rules below it repeat the fill so a marked cell keeps its colour on the current-year row and
   in the totals footer, both of which paint their own background at a higher specificity. */
.roll-table .is-in-force,
.roll-table tbody tr.is-current > td.is-in-force,
.roll-table tbody tr.is-pran > td.is-in-force,
.roll-year-block--pran .roll-table tbody td.is-in-force,
.roll-table tfoot tr.is-total > td.is-in-force {
    background: #FFF6DA;
    font-weight: 600;
}

/* The header of the marked column, so the tint reads as one column rather than a loose cell. In the
   single-class table one header serves every year, so it marks the column in force NOW; in the
   multi-class tables each table is one year and the header sits directly above its own tinted cells.
   Full-strength text: a header is muted grey by default, which on yellow reads as disabled. */
.roll-table thead th.is-in-force {
    background: #FFF6DA;
    color: var(--text);
    font-weight: 700;
}

/* One block per tax year. */
.roll-year-block { margin-top: 1.1rem; }
.roll-year-block:first-of-type { margin-top: 0.4rem; }

.roll-year-head {
    display: flex;
    align-items: baseline;
    /* Wraps: the heading can now carry a range, a cycle name, a span note and the current tag, which
       is more than a phone width holds on one line. */
    flex-wrap: wrap;
    gap: 0.5rem;
    font-size: 0.95rem;
    margin: 0 0 0.35rem;
}
.roll-year-head__cycle { color: var(--text-muted); font-size: 0.78rem; font-weight: 400; }
/* "unchanged for 6 years" — same weight as the cycle name, because it is the same kind of note about
   the block rather than a status like the current tag. Only rendered on a span covering >1 year. */
.roll-year-head__span { color: var(--text-muted); font-size: 0.78rem; font-weight: 400; }

.roll-tag {
    display: inline-block;
    padding: 0.05rem 0.4rem;
    border-radius: 3px;
    background: var(--brand-accent);
    color: var(--brand-dark);
    font-size: 0.66rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
}

/* The current tax year is what the customer is actually assessed on — make it findable at a glance
   in a stack of a dozen near-identical tables. */
.roll-year-block.is-current {
    border-left: 3px solid var(--brand-accent);
    padding-left: 0.7rem;
    margin-left: -0.85rem;
}
.roll-year-block.is-current .roll-year-head { font-weight: 700; }

/* A PRAN run drawn as an assessment among the roll's own years (RollPranTitle, 2026-10-02, asked for).
   SAME TABLE, DIFFERENT DRESS: it has to read as an assessment and still never be mistaken for one of
   the roll's years, so it takes the link blue the PRAN tag already wears — a pale wash of it on the
   figures and a ↱ in the margin where the current year has a solid rule. Blue, not the sky accent, for
   the same reason the tag is: sky is "current".

   THE TITLE IS NOT BLUE (2026-10-02, asked for): it is set exactly as a year's title is, so the run
   reads as an assessment in its own right. The word PRAN, the wash and the arrow carry the difference;
   do not add a title colour back. */
.roll-table tbody tr.is-pran > th,
.roll-table tbody tr.is-pran > td,
.roll-year-block--pran .roll-table tbody td {
    background: rgba(13, 127, 176, 0.06);
}
/* INDENTED ABOVE THE YEAR IT REVISES, WITH A ↱ IN THE MARGIN (2026-10-02, asked for). The block steps
   in 2rem and the arrow is drawn in that gutter, the FULL HEIGHT of the run: up from its foot, where
   the year it replaced sits just below, then right into its title. It says this run superseded the
   roll return under it — the run sits ABOVE its year because it is the more current assessment for
   it (RollAssessmentEntry.Build). It was a ↳ under the year until the same day. It is also the block's
   left-edge marker; the dashed rule that was here first went when the arrow arrived, because two blue
   verticals side by side read as one smudge. The PDF draws the same shape (PranArrowSvg).

   DRAWN WITH BORDERS, not the ↱ glyph: a character cannot stretch to a block's height. ::before is the
   stem and the top bar (its left and top borders); ::after is the head, a border triangle centred on
   the bar. The bar sits 0.65rem down from the top — the middle of the title line.

   THICK, AND CLEAR OF THE TABLE (2026-10-02, asked for): a 4px stroke and a 9x16px head, in pixels
   because borders are. The gutter is 32px (2rem): stem at 3px, bar to 17px, head to a tip at 26px —
   6px of white between the tip and the block. Change one and re-check the others. */
.roll-year-block--pran {
    position: relative;
    margin-left: 2rem;
}
.roll-year-block--pran::before {
    content: "";
    position: absolute;
    left: -29px;
    top: 0.65rem;
    bottom: 0.3rem;
    width: 14px;
    box-sizing: border-box;
    border-left: 4px solid var(--brand-link);
    border-top: 4px solid var(--brand-link);
}
.roll-year-block--pran::after {
    content: "";
    position: absolute;
    left: -15px;
    /* Centred on the 4px bar: its centre is 2px below top:0.65rem, the head is 16px tall. */
    top: calc(0.65rem - 6px);
    border-left: 9px solid var(--brand-link);
    border-top: 8px solid transparent;
    border-bottom: 8px solid transparent;
}
/* "Run 2025 B" — a note about the block, so the same muted weight as the cycle name on a year. */
.roll-pran__run { color: var(--text-muted); font-size: 0.78rem; font-weight: 400; }
.roll-year .roll-pran__run { margin-left: 0.4rem; }
.roll-pran__none { color: var(--text-muted); font-style: italic; }
/* SINGLE-CLASS: the PRAN is a ROW, and a table row cannot be moved off the margin — the figures have to
   stay under their headings. So the indent is drawn on the label cell alone: the text steps in, and an
   inset white strip keeps the wash off the first 2rem, so the tinted band visibly starts there.
   --surface, not #fff, so it follows the card it sits on.

   THE ARROW SITS IN THAT STRIP, the same ↱ as the block's: the run here is one line, so the stem
   rises from the foot of the row and the bar turns in at its middle. */
.roll-table tbody tr.is-pran > th.roll-year {
    position: relative;
    padding-left: calc(0.6rem + 2rem);
    box-shadow: inset 2rem 0 0 var(--surface);
}
/* Same arrow as the block's, in the same 32px strip: stem at 3px, bar to 17px, tip at 26px. */
.roll-table tbody tr.is-pran > th.roll-year::before {
    content: "";
    position: absolute;
    left: 3px;
    top: calc(50% - 2px);
    bottom: 0.25rem;
    width: 14px;
    box-sizing: border-box;
    border-left: 4px solid var(--brand-link);
    border-top: 4px solid var(--brand-link);
}
.roll-table tbody tr.is-pran > th.roll-year::after {
    content: "";
    position: absolute;
    left: 17px;
    top: calc(50% - 8px);
    border-left: 9px solid var(--brand-link);
    border-top: 8px solid transparent;
    border-bottom: 8px solid transparent;
}

/* The Total row is the figure the "Assessed value" headline shows — make the tie visible. */
/* The matching rule above the totals: it separates a sum from the lines it sums, which is a real
   boundary but a lesser one than the header's, so it ranks below the black rather than competing
   with it.

   A BARE HEX because the palette has nothing at this weight — it runs --border #e2e6ee (the row
   hairlines) straight to --text-muted #5b6472, and --text-muted at 1px still reads as black to the
   eye. This sits about midway between the two: plainly grey next to the header rule, plainly a rule
   next to the hairlines. Keep it inside that gap if it moves again; a token would be a fourth
   neutral in the palette for one line of one table. */
.roll-table tfoot tr.is-total > td { font-weight: 600; border-top: 1px solid #9aa3b0; }
/* border-collapse makes that edge a CONTEST between this border-top and the last portion row's
   border-bottom, and same-width-same-style ties go to the cell further up — so the hairline would
   win and the rule above would quietly do nothing. That is not hypothetical: this border was
   var(--text-muted) before the black rules went in and had never once been visible. Clearing the
   losing border is what puts it on the page.

   :has(+ tfoot) scopes it to tables that actually have a totals row: the single-class table has no
   tfoot and keeps the hairline that closes it off. */
.roll-table tbody:has(+ tfoot) tr:last-child > td { border-bottom: 0; }
.roll-table tfoot tr.is-total > td:first-child {
    text-transform: uppercase;
    font-size: 0.72rem;
    letter-spacing: 0.04em;
}

/* ---- Project photos ----------------------------------------------------- */
/* Spans both columns of .project-grid — the photo is the anchor of the page. */
.card--full { grid-column: 1 / -1; }

.gallery { margin: 0; }

.gallery__hero {
    display: block;
    width: 100%;
    /* Fixed, not max-height: the box must not resize between a wide aerial and a tall building
       shot, or every prev/next reflows the page under the photo. */
    height: var(--gallery-h);
    object-fit: contain;             /* aerials and building shots differ wildly — never crop the subject */
    background: #0e1116;             /* letterbox fill, so a portrait shot doesn't sit on a white gap */
    border-radius: var(--radius-sm);
}

.gallery__caption {
    display: flex;
    align-items: baseline;
    gap: 0.6rem;
    flex-wrap: wrap;
    margin-top: 0.6rem;
    font-size: 0.85rem;
}
.gallery__label { font-weight: 600; }
.gallery__subtype { color: var(--text-muted); }
.gallery__count { margin-left: auto; color: var(--text-muted); }

.gallery__thumbs {
    display: flex;
    gap: 0.5rem;
    overflow-x: auto;                /* a project can hold a dozen shots; scroll rather than reflow */
    padding: 0.75rem 0 0.25rem;
}

.gallery__thumb {
    flex: 0 0 auto;
    width: 84px;
    height: 62px;
    padding: 0;
    border: 2px solid transparent;
    border-radius: var(--radius-sm);
    background: none;
    cursor: pointer;
    overflow: hidden;
    transition: border-color 0.12s ease, opacity 0.12s ease;
    opacity: 0.7;
}
.gallery__thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.gallery__thumb:hover { opacity: 1; }
.gallery__thumb.is-active { border-color: var(--brand-accent); opacity: 1; }

/* Cover tile in the portfolio grid. Fixed box so rows stay a uniform height whether or not a
   project has a photo — a ragged grid reads as broken. */
.portfolio-tile {
    display: block;
    width: 72px;
    height: 48px;
    object-fit: cover;               /* fill the box; at this size cropping beats letterboxing */
    border-radius: var(--radius-sm);

    /* THE TILE BEFORE ITS PHOTO ARRIVES. This was a near-black fill, which read as a dead or failed
       tile for as long as the thumbnail took to come down — twenty of them at once on a page, each
       its own authenticated request. A pale skeleton that breathes says "coming" instead.

       PAINTED OVER, NOT SWITCHED OFF: object-fit:cover fills the box edge to edge, so the loaded
       photo hides all of this with no JS and nothing to keep in step. There is no load event to
       listen for and no class to swap.

       A PULSE RATHER THAN A SPINNER, because twenty spinners in one column is a busy page, and they
       would be spinning in the corner of a 72px box. It is also why the animation STOPS: ~10s of
       iterations, then it rests on the flat tint. A thumbnail that has not arrived by then is not
       arriving, and an animation left running forever is compositing work on a page people leave
       open all day. */
    background-color: #eef1f6;
    animation: portfolio-tile-wait 1.1s ease-in-out 9 alternate both;
}
@keyframes portfolio-tile-wait {
    from { background-color: #eef1f6; }
    to   { background-color: #f8fafc; }
}

/* NO COVER PHOTO ON THE PROPERTY — 42,697 of the 76,926 rows this grid can show (measured
   2026-09-22), so this is the ORDINARY tile rather than an edge case, and it has to read as a
   deliberate blank rather than as something that failed to load.

   A DRAWN BUILDING, not an emoji: the 🏞️ it replaces rendered at whatever size and colour the
   platform's font felt like, and a landscape photo icon in a dashed box is the universal shape for
   "image missing". A flat-roofed block says what the tile is FOR — a property — and says it in the
   page's own muted grey.

   A BACKGROUND IMAGE, not inline <svg>, on purpose: this is one element per row either way, but the
   markup stays a single empty <span> instead of six SVG nodes per row in the render tree.

   ANIMATION CANCELLED, because it shares .portfolio-tile with the loading state above and this tile
   is not waiting for anything. */
.portfolio-tile--empty {
    background-color: #f2f4f8;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239aa3b2' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2.5 20h19'/%3E%3Cpath d='M5 20V4.5h8V20'/%3E%3Cpath d='M13 20v-8h6v8'/%3E%3Cpath d='M7.5 7.5h1.5M10.5 7.5h1.5M7.5 11h1.5M10.5 11h1.5M7.5 14.5h1.5M10.5 14.5h1.5M15.5 15h1.5'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: center;
    background-size: 26px 26px;
    border: 1px solid var(--border);
    animation: none;
}

.gallery--empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.35rem;
    min-height: var(--gallery-h);
    border: 1px dashed var(--border);
    border-radius: var(--radius-sm);
    color: var(--text-muted);
    font-size: 0.88rem;
}
.gallery__icon { font-size: 1.8rem; }

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

/* ---- Responsive --------------------------------------------------------- */
@media (max-width: 640px) {
    .portal-brand__tagline { display: none; }
    .portal-header__inner { padding: 0.75rem 1rem; }
    .portal-main { padding: 1.25rem 1rem; }

    /* PHONE TOP BAR (2026-09-30): brand + role + email + Sign out overflowed the screen, so the whole
       page scrolled sideways. The email goes (it is on the account block's tooltip, see MainLayout),
       the role badge stops wrapping to two lines, and the row may wrap as a last resort — the account
       block then drops under the brand rather than pushing past the edge. */
    .portal-header__inner { flex-wrap: wrap; row-gap: 0.5rem; }
    .portal-header__name { display: none; }
    .portal-header__role { white-space: nowrap; }

    /* TOUCH TARGETS at Apple's 44px minimum (2.75rem) on small screens only; desktop keeps 2.25rem.
       The back button, Prev/Next and Report. Width and height move together on the circles. */
    .page__back,
    .project-nav__step { width: 2.75rem; height: 2.75rem; }
    .project-detail__report { height: 2.75rem; }

    /* The address WRAPS on a phone rather than ellipsing: at 375px "…, PICKERING, ON" was cut after the
       street, and there is no hover to read the tooltip. Desktop keeps the one-line cut. */
    .project-detail__address { white-space: normal; }
}

/* Year scope selector, sitting inline with the status pills. Sized to match a pill rather than a
   form field — it selects a scope alongside them, it is not part of a form being filled in. */
.year-select {
    font: inherit;
    font-size: 0.75rem;
    font-weight: 600;
    padding: 0.15rem 0.5rem;
    color: var(--text-muted);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 999px;
    cursor: pointer;
}
.year-select:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(59, 203, 255, 0.5); }

/* FilterPillRow's trailing dropdown (More) — the full list after a long row's shortcut pills, e.g. the
   Projects Group row past 10 groups. Wears .year-select's shape; when the selection is one only the
   dropdown shows, it takes the selected pill's ring so the row reads as active. */
.filter-select { max-width: 100%; }
.filter-select.is-active { color: var(--text); border-color: transparent; box-shadow: 0 0 0 2px var(--brand-dark); }

/* ---- Appeal date calendar ----------------------------------------------- */
/* Shared by the portfolio and project Calendar tabs (Components/AppealCalendar.razor). */

.cal-pane { margin-top: 0.25rem; }

.cal__bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.75rem;
    margin-bottom: 0.75rem;
}
.cal__nav { display: flex; align-items: center; gap: 0.35rem; }
/* Fixed width so the month name changing length does not shuffle the arrows sideways as you page. */
.cal__title {
    min-width: 11ch;
    text-align: center;
    font-weight: 700;
    color: var(--brand-dark);
    font-size: 1.05rem;
}
.cal__btn {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    color: var(--text);
    font: inherit;
    line-height: 1;
    padding: 0.35rem 0.6rem;
    cursor: pointer;
}
.cal__btn:hover { background: #f0f4fa; }
.cal__btn--text { font-size: 0.8rem; color: var(--brand-link); }
.cal__note { font-size: 0.78rem; color: var(--text-muted); margin-left: auto; }

/* Seven equal columns. minmax(0,1fr) rather than 1fr: a long chip inside a grid item would
   otherwise set the column's minimum width and push the week out past the page. */
.cal__grid {
    display: grid;
    grid-template-columns: repeat(7, minmax(0, 1fr));
    gap: 1px;
    background: var(--border);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
    box-shadow: var(--shadow);
}
.cal__dow {
    background: var(--bg);
    color: var(--text-muted);
    font-size: 0.72rem;
    font-weight: 600;
    text-align: center;
    padding: 0.35rem 0;
}
/* A fixed floor, not a fixed height: a busy day grows, and every other cell in that week grows with
   it because grid rows are equal-height. Six weeks are always drawn, so the grid keeps one height
   from month to month and the list below it does not jump when you page. */
/* 124px, up from 88 (2026-09-21): a chip stopped being a one-line roll number and became a type
   over the properties under it. SIZED OFF AppealCalendar.CellLines, which is 7 — seven lines is
   both three two-line chips and one chip naming six properties, and those are the two shapes a
   cell has to hold. Keep the two in step: raising CellLines without raising this clips the cell,
   and raising this without CellLines just adds white space.

   A FIXED HEIGHT IS THE POINT. Cells share a grid row, so one day left to grow to its content
   would set the height of its whole week — and the busiest customer-day carries 378 entries. */
.cal__day {
    background: var(--surface);
    min-height: 124px;
    padding: 0.25rem;
    display: flex;
    flex-direction: column;
    gap: 2px;
    overflow: hidden;
}
/* Leading/trailing days of the neighbouring months. Kept visible but receded — removing them would
   leave holes in the first and last week and make the grid harder to read, not easier. */
.cal__day.is-outside { background: #fafbfd; }
.cal__day.is-outside .cal__daynum { color: #aab2bf; }
.cal__day.is-today { box-shadow: inset 0 0 0 2px var(--brand-link); }
/* THE PICKED DAY, by a click or by "Go to previous / next date" — the jump only selects the day it
   lands on, so this is the highlight it shows. The tint alone was the only marker until 2026-10-06
   and could not be seen: #eef7fc against chips of #e8f4fb is the same colour, so landing on a busy
   day looked like nothing happened. A RING IN --brand-dark, not --brand-link, so it cannot be read
   as today's ring; the day number becomes a filled badge as the second cue. */
.cal__day.is-selected { background: #eef7fc; box-shadow: inset 0 0 0 2px var(--brand-dark); }
.cal__day.is-outside.is-selected { background: #eaf2f8; }
/* Today AND picked: both rings, the picked one inside, so neither fact disappears. */
.cal__day.is-today.is-selected { box-shadow: inset 0 0 0 2px var(--brand-link), inset 0 0 0 4px var(--brand-dark); }

.cal__daynum {
    align-self: flex-start;
    background: none;
    border: 0;
    padding: 0 0.15rem;
    font: inherit;
    font-size: 0.78rem;
    font-weight: 600;
    color: var(--text-muted);
    cursor: pointer;
}
.cal__day.is-today .cal__daynum { color: var(--brand-link); }
.cal__day.is-selected .cal__daynum { background: var(--brand-dark); color: var(--brand-on-dark); border-radius: 999px; padding: 0 0.4rem; }

/* One entry: property + step at portfolio level, step alone at project level. The whole chip is a
   button because clicking anything in a day should get you that day's list. */
/* A COLUMN since 2026-09-21, not a row: the chip stacks WHAT over WHO where it used to sit a count
   badge beside a roll number. align-items:stretch (the default) so both lines take the full width
   and their ellipsis lands in the same place. */
.cal__chip {
    display: flex;
    flex-direction: column;
    gap: 0;
    width: 100%;
    text-align: left;
    background: #e8f4fb;
    border: 0;
    border-left: 3px solid var(--brand-link);
    border-radius: 3px;
    padding: 0.12rem 0.25rem;
    font: inherit;
    font-size: 0.68rem;
    line-height: 1.25;
    color: var(--text);
    cursor: pointer;
}
.cal__chip:hover { background: #d9ecf8; }
/* A COMPLETED chip, at BOTH levels since 2026-08-25 — this comment claimed the account calendar was
   still outstanding-only until 2026-09-21. pill--done's own two colours, not a third green: the same
   finished-green a paid invoice, a closed appeal and this step's own Status cell in the list below
   already wear. Without it a step finished in March and a step nobody actioned in March are the same
   blue chip on the same cell, which is the confusion the table's Status column exists to prevent.

   ALL OF THE GROUP, since a chip became a type group: a chip with one step still outstanding stays
   blue. See the ALL-not-Any note in AppealCalendar. */
.cal__chip--done { background: #e2f7ea; border-left-color: #1f7a45; }
.cal__chip--done:hover { background: #cfeeda; }
/* Truncated ON PURPOSE, unlike the appeals grid. A chip is a marker saying something is on that
   day; the full text is one click away in the list below, and the tooltip carries it meanwhile.
   Wrapping would let a single busy day set the height of its whole week — and type labels run to
   48 characters, so on a seventh of the page width wrapping is not a near miss. */
.cal__chiptype,
.cal__chipwho { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* WHAT the work is. Carries the weight because it is the line that tells you whether the day
   matters to you; the type labels are long and this is where the ellipsis usually lands. */
.cal__chiptype { font-weight: 600; }
/* WHO it is on — one line per property where the cell had room to name them, otherwise the one
   line that counts them; at project level, the appeal count. There may be SEVERAL of these under
   one type, which is what the budget in AppealCalendar.CellLines exists to bound. Never blank.

   Recedes so the lines read as one chip rather than as a stack: same colour family, a step down in
   size. line-height a shade under the chip's 1.25 because a list of six wears the difference. */
.cal__chipwho { font-size: 0.62rem; line-height: 1.2; color: var(--text-muted); }
/* .cal__chipn WAS HERE: the round badge counting a step's appeals. It went with the roll numbers
   on 2026-09-21 — a chip is a TYPE GROUP now and its second line already says how many, so the
   badge would have counted the same thing twice in two different units. */
.cal__more {
    align-self: flex-start;
    background: none;
    border: 0;
    padding: 0 0.25rem;
    font: inherit;
    font-size: 0.68rem;
    color: var(--brand-link);
    cursor: pointer;
}
.cal__more:hover { text-decoration: underline; }

.cal__list { margin-top: 1rem; }
.cal__listhead { display: flex; align-items: baseline; gap: 0.75rem; margin-bottom: 0.5rem; }
.cal__listhead h3 { margin: 0; font-size: 0.95rem; color: var(--brand-dark); }

/* NO TABLE OF ITS OWN SINCE 2026-09-21. The drill-down under the grid is AppealDateList, the
   table view's own component, so its columns and widths are the .apdates__* rules further up this
   file. The eight that stood here — .cal__table, .cal__cdate, .cal__time, .cal__roll, .cal__addr,
   .cal__cappeal, .cal__crep, .cal__cstatus — were a second, narrower copy of those, and every one
   that drifted was the two views of one tab disagreeing about the same step. Do not start a third:
   style .apdates__* and both views move together. */

/* The grid gets unreadable long before the page does — seven columns of chips in a phone's width is
   a column of ellipses. Below this the cells become bare day numbers and the list below carries the
   detail, which is the same information in a shape that fits. */
@media (max-width: 700px) {
    .cal__day { min-height: 52px; }
    .cal__chip, .cal__more { display: none; }
    /* Replaces the hidden chips: a dot says the day has something on it without needing the width. */
    .cal__day:has(.cal__chip) .cal__daynum::after {
        content: "";
        display: inline-block;
        width: 5px;
        height: 5px;
        margin-left: 0.25rem;
        border-radius: 50%;
        background: var(--brand-link);
        vertical-align: middle;
    }
    .cal__note { margin-left: 0; }
}

/* ---- Responsive preview (DevSizes, Development only) --------------------- */
.devsizes__bar { display: flex; gap: 0.5rem; margin-bottom: 1rem; max-width: 36rem; }
.devsizes__path {
    flex: 1;
    font: inherit;
    padding: 0.45rem 0.7rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface);
}
/* FULL BLEED: the row breaks out of the page column to the window's width (1rem in from each edge),
   then scrolls sideways itself when the frames are wider still — the page never does. */
.devsizes__row {
    display: flex;
    gap: 1rem;
    align-items: flex-start;
    overflow-x: auto;
    padding-bottom: 0.75rem;
    width: calc(100vw - 2rem);
    margin-left: calc(50% - 50vw + 1rem);
}
.devsizes__frame { margin: 0; flex: none; }
.devsizes__label { font-size: 0.78rem; font-weight: 600; color: var(--text-muted); margin-bottom: 0.35rem; }
.devsizes__frame iframe,
.devsizes__wait { display: block; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); }
.devsizes__wait { height: 820px; display: flex; align-items: center; justify-content: center; color: var(--text-muted); }

/* Change notices sub-tab: one NoticeSection per kind (PACN, PRAN, SAN, ANA). Each after the first
   follows a table (or a one-line "none"), so it needs the gap a new section gets rather than the
   caption's own tight top margin. */
.notice-section + .notice-section { margin-top: 1.5rem; }
/* An Assessment year tag scrolls a section to the top (nexstackScroll.toId); this keeps its caption out
   from under the sticky .ptabs strip. */
.notice-section { scroll-margin-top: 4rem; }
/* A notice MPAC marked cancelled (ANA's Cancelled column). Muted, not the accent "open" tag: it is
   a fact about the row, not a call to act. */
.roll-tag--muted { background: var(--border); color: var(--text-muted); }
/* PACN / PRAN / SAN / ANA beside a year on the assessment table, linking to Change notices (RollNoticeTags).
   SOLID LINK BLUE, white text (2026-10-02, asked for — the sky outline was too easy to miss). Not the
   sky accent: that is "current", and the two must stay tellable apart on the same row. --brand-link
   is the palette's colour for links on light surfaces, which is what these are. Darkens to the navy
   on hover. */
.roll-tag--notice {
    background: var(--brand-link);
    color: var(--brand-on-dark);
    text-decoration: none;
}
.roll-tag--notice:hover,
.roll-tag--notice:focus-visible { background: var(--brand-dark-2); }
.roll-year .roll-tag + .roll-tag { margin-left: 0.25rem; }

/* AiAskBox lives in Shared.Web (css/aibox.css, linked before this file). Its reduced-motion rule
   covered the Ask shimmer too; that half stays here. */
@media (prefers-reduced-motion: reduce) {
    .ask__shimmer { animation: none; }
}

/* ---- Portfolio Ask (beta) ------------------------------------------------ */
/* The box, what it searched for, the answer card, then the matched documents in the shared table. 1rem
   between blocks — the same step the tab strip leaves above the box. Citations are small numbered chips
   that open the document; the rows they point at are tinted in the table under the card. */
.ask { display: flex; flex-direction: column; gap: 1rem; }
.ask__note, .ask__count { margin: 0; max-width: 880px; color: var(--text-muted); font-size: 0.85rem; line-height: 1.45; }
.ask__reading { display: flex; flex-wrap: wrap; gap: 0.25rem 1rem; color: var(--text-muted); font-size: 0.85rem; }
.ask__reading-gap { color: var(--text); }
.ask__reading-gap:empty { display: none; }
.ask__answer {
    position: relative;
    max-width: 880px;
    padding: 1rem 1.15rem 0.85rem;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--surface);
    box-shadow: var(--shadow);
    overflow: hidden;
}
.ask__answer::before {
    content: "";
    position: absolute;
    inset: 0 0 auto 0;
    height: 3px;
    background: linear-gradient(90deg, var(--ai-1), var(--ai-2));
}
.ask__answer-head {
    margin-bottom: 0.55rem;
    display: flex;
    align-items: center;
    gap: 0.35rem;   /* the AI sparkle before "Answer" (2026-10-21) — the same mark as the box and the Ask tab */
    color: var(--text-muted);
    font-size: 0.72rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}
/* 0.9rem, not the inherited 1rem: at body size the answer read a size bigger than the table (0.85rem) and chips
   (0.82rem) around it (2026-10-21). A hair above the table, so it still leads. */
.ask__answer p { margin: 0 0 0.5rem; font-size: 0.9rem; line-height: 1.55; }
.ask__answer .ask__bullet { position: relative; padding-left: 1.1rem; }
.ask__answer .ask__bullet::before {
    content: "";
    position: absolute;
    left: 0.2rem;
    top: 0.62em;
    width: 0.38rem;
    height: 0.38rem;
    border-radius: 50%;
    background: linear-gradient(135deg, var(--ai-1), var(--ai-2));
}
.ask__cite {
    display: inline-grid;
    place-items: center;
    min-width: 1.15rem;
    height: 1.15rem;
    margin: 0 0.1rem;
    padding: 0 0.25rem;
    border-radius: 999px;
    background: #efeaff;
    color: #5a3fe0;
    font-size: 0.7rem;
    font-weight: 700;
    line-height: 1;
    text-decoration: none;
    vertical-align: 0.12em;
}
.ask__cite:hover { background: var(--ai-1); color: #fff; }
.ask__answer-foot {
    margin-top: 0.75rem;
    padding-top: 0.6rem;
    border-top: 1px solid var(--border);
    color: var(--text-muted);
    font-size: 0.8rem;
}
.ask__answer--loading { display: flex; flex-direction: column; gap: 0.55rem; }
.ask__answer--loading .ask__answer-head { margin-bottom: 0.1rem; }
.ask__shimmer {
    height: 0.8rem;
    border-radius: 999px;
    background: linear-gradient(90deg, #eef0f5 0%, #e3e0fb 40%, #eef0f5 80%) 0 0 / 200% 100%;
    animation: ask-shimmer 1.3s linear infinite;
}
.ask__shimmer--short { width: 55%; }
@keyframes ask-shimmer { to { background-position: -200% 0; } }
.ask__num {
    display: inline-grid;
    place-items: center;
    min-width: 1.3rem;
    height: 1.3rem;
    border-radius: 999px;
    color: var(--text-muted);
    font-size: 0.75rem;
    font-weight: 600;
}
.ask__label { color: var(--text-muted); }
/* "6 of 21" under a group's change when it covers only part of the group (AskRecordGroupRow.ChangeCoverage). */
.ask__coverage { display: block; font-size: 0.75em; color: var(--text-muted); }
.ask__coverage:empty { display: none; }
.ptable tr.ask__cited td { background: #f6f3ff; }
.ptable tr.ask__cited .ask__num { background: #efeaff; color: #5a3fe0; }
@media (max-width: 600px) {
    .aibox { padding-left: 0.75rem; }
    .aibox__input { font-size: 16px; }   /* below 16px iOS zooms the page on focus */
    .ask__answer { padding: 0.9rem 0.9rem 0.75rem; }
}

/* Ask: the records and documents sections under the answer, each with a small labelled heading. Record chips are
   teal rather than violet so "[R3] a property record" and "[3] a document" read apart at a glance. */
.ask__section-head {
    margin: 0 0 -0.5rem;
    color: var(--text-muted);
    font-size: 0.72rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}
/* Plain muted text, not a badge: it appears only when a list is cut short ("Showing 50 of 1,234"). */
.ask__section-count {
    margin-left: 0.5rem;
    font-weight: 500;
    letter-spacing: 0;
    text-transform: none;
}
.ask__section-count:empty, .ask__answer-foot:empty { display: none; }
.ask__reading:not(:has(.ask__reading-gap:not(:empty))) { display: none; }
.ask__records { display: flex; flex-direction: column; gap: 1rem; }
.ask__cite--record { background: #e3f6f4; color: #0f766e; }
.ask__cite--record:hover { background: #0f766e; color: #fff; }

/* SHOW MORE (2026-10-21) — pages a list already in memory (ShowMore.razor), so a click is instant. Centred under
   the table; the outlined button is the next page, the quiet one takes the rest. */
.show-more { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; justify-content: center; padding-top: 0.6rem; }
.show-more__btn {
    font: inherit;
    font-size: 0.82rem;
    font-weight: 600;
    padding: 0.35rem 0.95rem;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--surface);
    color: var(--brand-dark);
    cursor: pointer;
}
.show-more__btn:hover { border-color: var(--brand-dark); }
.show-more__btn--quiet { border-color: transparent; background: transparent; color: var(--text-muted); }
.show-more__btn--quiet:hover { border-color: transparent; color: var(--text); }

/* "DID THIS ANSWER HELP?" — the shared AiRating, styled in Shared.Web's aibox.css (moved 2026-10-26). Only the pull
   up under the answer card is the portal's. */
.aibox-rate { margin-top: -0.35rem; }
/* "Export all results to Excel" above the tables (AskExportButton): a quiet outlined button in Excel's green, with
   what the file holds beside it ("63 representatives · 1,726 appeals"). */
/* The row above an answer's tables: Export left; the map's imagery and Show map hard right (margin-left: auto). */
.ask__actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem 1.25rem; margin: 1rem 0 -0.25rem; }
.ask__map-controls { display: flex; align-items: center; gap: 1rem; margin-left: auto; }
.ask__export { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem; }
/* ONE PILL SHAPE for the row's buttons; each brings its own colour — Excel green for the export, link blue for the map. */
.ask__export-btn, .ask__pill-btn { display: inline-flex; align-items: center; gap: 0.4rem; padding: 0.35rem 0.75rem;
    border: 1px solid currentColor; border-radius: 999px; background: var(--surface, #fff); font-size: 0.82rem;
    font-weight: 600; cursor: pointer; }
.ask__export-btn { color: #1d6f42; }
.map-toggle__pill { color: var(--brand-link); }
.map-toggle__pill:hover { background: var(--brand-link); color: #fff; }
/* The map's own count sits tight under it; the panel's gap spaces it from the row above. */
.ask__map .ptable__foot { margin-top: 0.35rem; }
.ask__export-btn:hover:not(:disabled) { background: #1d6f42; color: #fff; }
.ask__export-btn:disabled { opacity: 0.6; cursor: progress; }
.ask__export-what { color: var(--text-muted); font-size: 0.82rem; }
.ask__export-note { color: var(--danger, #b42318); font-size: 0.8rem; }
.ask__export-note:empty { display: none; }
/* A count two ways (AskCrosstabTable): the label column held in place while the years scroll; totals set apart. */
.ask__crosstab th, .ask__crosstab td { white-space: nowrap; }
.ask__crosstab .ask__crosstab-label { position: sticky; left: 0; z-index: 1; background: var(--surface, #fff); min-width: 220px;
    white-space: normal; }
.ask__crosstab .ask__crosstab-total { font-weight: 700; border-left: 1px solid var(--border, #e5e7eb); }
.ask__crosstab tfoot td { font-weight: 700; border-top: 1px solid var(--border, #e5e7eb); }

/* THE SIGNED-IN WELL (2026-10-21): the badge, name, chevron and Sign out sit in one soft pill on the dark bar, and
   the chevron collapses the name and the badge — for screenshots and screen-sharing — remembered per browser (portal.js). The
   chevron points RIGHT when the name shows (fold it back toward the edge) and LEFT when folded (open it this way) —
   the well sits at the right edge and grows leftward (flipped 2026-10-21, it read backwards). */
.portal-header__account {
    padding: 0.25rem 0.85rem 0.25rem 0.3rem;
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.06);
    border: 1px solid rgba(255, 255, 255, 0.08);
    gap: 0.7rem;
}
.portal-header__toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.5rem;
    height: 1.5rem;
    padding: 0;
    border: 0;
    border-radius: 999px;
    background: transparent;
    color: rgba(255, 255, 255, 0.7);
    cursor: pointer;
    transition: background 0.12s ease, color 0.12s ease;
}
.portal-header__toggle svg { transition: transform 0.15s ease; }
.portal-header__toggle:hover, .portal-header__toggle:focus-visible { background: rgba(255, 255, 255, 0.12); color: #fff; }
.account-collapsed .portal-header__name, .account-collapsed .portal-header__role { display: none; }
.account-collapsed .portal-header__account { padding-left: 0.2rem; }
/* Sign in as someone else (development only, MainLayout): the chevron's round button look, an amber tint so it reads as
   a test tool rather than part of the product. Hidden with the name when the account is collapsed. */
.portal-header__switch {
    display: inline-flex; align-items: center; justify-content: center;
    width: 1.5rem; height: 1.5rem; border-radius: 999px;
    color: #f5c46b; transition: background 0.12s ease, color 0.12s ease;
}
.portal-header__switch:hover, .portal-header__switch:focus-visible { background: rgba(255, 255, 255, 0.12); color: #ffd98f; }
.account-collapsed .portal-header__switch { display: none; }
.account-collapsed .portal-header__toggle svg { transform: rotate(180deg); }
@media (prefers-reduced-motion: reduce) { .portal-header__toggle svg { transition: none; } }
/* Phones already hide the name (the 640px block), so there is nothing for the chevron to fold. */
@media (max-width: 640px) { .portal-header__toggle { display: none; } }

/* RANKED BULLETS (2026-10-21): a bullet that cites one row leads with that row's number in the table's own circle
   (.ptable tr.ask__cited .ask__num), not a dot with a chip at the end — 1, 2, 3 reads as a ranking and matches the
   row it points at. Still the link. */
.ask__answer .ask__bullet--ranked { padding-left: 1.85rem; }
.ask__answer .ask__bullet--ranked::before { content: none; }
.ask__marker {
    position: absolute;
    left: 0;
    top: 0.12em;
    display: inline-grid;
    place-items: center;
    min-width: 1.3rem;
    height: 1.3rem;
    padding: 0 0.2rem;
    box-sizing: border-box;
    border-radius: 999px;
    background: #efeaff;
    color: #5a3fe0;
    font-size: 0.72rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    text-decoration: none;
    transition: background 0.12s ease, color 0.12s ease;
}
.ask__marker:hover, .ask__marker:focus-visible { background: #5a3fe0; color: #fff; }

/* THE SUMMARY WELL (2026-10-21): the answer's opening sentence, when a list follows, in a soft violet tint — the
   AI mark's own family — so the card reads as a headline and then its ranked lines. */
.ask__answer .ask__summary {
    margin: 0 0 0.7rem;
    padding: 0.55rem 0.75rem;
    border-radius: calc(var(--radius) - 2px);
    background: #f6f3ff;
    border: 1px solid #ebe5ff;
    color: var(--text);
    font-weight: 500;
}

/* ---- Case law (staff only) ---------------------------------------------------------------------
   The staff landing's top switch (StaffTabs), the Case law answer's sources, and its library table.
   The answer itself is the Ask tab's .ask__answer, unchanged, so the two AI answers read alike. */
.staff-tabs .psub { margin-bottom: 0.75rem; }

.caselaw-sources { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.6rem; max-width: 980px; }
.caselaw-source {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 0.6rem;
    padding: 0.7rem 0.85rem;
    background: var(--surface, #fff);
    border: 1px solid var(--border, #e5e7eb);
    border-radius: 10px;
}
.caselaw-source .ask__num { background: #f1edff; color: #5a3fe0; }
.caselaw-source__body { min-width: 0; }
.caselaw-source__head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.25rem 0.75rem; }
.caselaw-source__cite { font-weight: 600; color: var(--brand-link, #1d4ed8); text-decoration: none; }
.caselaw-source__cite:hover { text-decoration: underline; }
.caselaw-source__where { color: var(--text-muted); font-size: 0.8rem; font-variant-numeric: tabular-nums; }
.caselaw-source__meta { color: var(--text-muted); font-size: 0.8rem; margin-top: 0.1rem; }
.caselaw-source__meta:empty { display: none; }
.caselaw-source__quote {
    margin: 0.45rem 0 0;
    padding: 0.1rem 0 0.1rem 0.75rem;
    border-left: 3px solid #d9d0ff;
    color: var(--text);
    font-size: 0.85rem;
    line-height: 1.5;
}

.caselaw-library { margin-top: 2rem; }
.caselaw-library__head { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 1rem; margin-bottom: 0.6rem; }
.caselaw-library__head .dash__title { margin: 0; }
.caselaw-library__head .appeal-summary { margin: 0; }
.caselaw-library__actions { display: flex; align-items: center; gap: 0.75rem; margin-left: auto; }
.caselaw-index-btn { color: #5a3fe0; }
.caselaw-index-btn:hover:not(:disabled) { background: #5a3fe0; color: #fff; }
.caselaw-index-btn:disabled { opacity: 0.5; cursor: default; }
.caselaw-library__scroll { max-height: 34rem; }
/* Short text columns that may still run long (a proceeding, an outcome): wrap within a sensible width
   rather than taking the row's whole width on one line. The summary is the free-text column, last. */
.ptable--fit td.caselaw-library__wrap { white-space: normal; min-width: 10rem; max-width: 16rem; }
.caselaw-library__summary { min-width: 18rem; line-height: 1.45; }
/* The decision's own number (the ARB's FILE NO.) under its citation: what duplicates are matched on. */
.caselaw-library__number { display: block; color: var(--text-muted); font-size: 0.75rem; font-variant-numeric: tabular-nums; }
.caselaw-library__number:empty { display: none; }
/* The Decision column: the label on one line, the parties wrapped under it within a sensible width. */
.ptable--fit td.caselaw-library__decision { white-space: normal; min-width: 11rem; max-width: 15rem; }
.caselaw-library__decision .portal-link { white-space: nowrap; }

/* FILE KINDS (2026-10-29): a LineIcon beside each file's name, tinted by kind in place of the 📕📗📘 emoji.
   Sized to the text and nudged onto its baseline; the tones keep the colour cue the emoji books gave. */
.file-kind { width: 1.05em; height: 1.05em; vertical-align: -0.17em; margin-right: 0.15em; color: var(--text-muted); }
.file-kind--pdf { color: #c62828; }
.file-kind--word { color: #1e5bb8; }
.file-kind--sheet { color: #1d6f42; }
.file-kind--slides { color: #c4501b; }
.file-kind--image { color: #7b4bb7; }
.file-kind--mail { color: #0f766e; }
.file-kind--folder { color: #b7791f; }

/* ---- Admin Data (portfolio, admin-only tab) ------------------------------ */
/* Address and Municipality wrap and take the spare width (plain .ptable, NOT ptable--fit: that gives the spare width to
   the last column, here Roll Value, and its one-line rule pushed Roll Value off the panel on Montréal's borough names). */
.ptable td.admin-data__wrap { white-space: normal; min-width: 10rem; }
/* The line above the table: "Hide matching values" (the report picker's checkbox look, .portfolio-pick__summary) at the
   left, Export to Excel at the right. min-height holds the line still while the export icon comes and goes. */
.admin-data__bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    min-height: 2rem;
    margin: 0 0 0.6rem;
}
.admin-data__actions { display: flex; align-items: center; gap: 0.5rem; }

/* A sortable header (SortableTh): the label as a borderless button that keeps the th's look and alignment, its arrow
   a small gap after it. */
.ptable__sort {
    background: none;
    border: none;
    padding: 0;
    font: inherit;
    color: inherit;
    cursor: pointer;
    white-space: nowrap;
}
.ptable__sort:hover { color: var(--text); }
.ptable__sort:focus-visible { outline: 2px solid var(--brand-link); outline-offset: 2px; }
.ptable__sort-arrow { margin-left: 0.25rem; font-size: 0.7em; }
.ptable__sort-arrow:empty { display: none; }

/* TEAMS (administrators, 2026-10-30): ONE COLUMN, every card the full width and every table sized to its content.
   Two columns side by side left each card half the page, and the members and what-if tables scrolled sideways ("members
   of keith has scroll bar"). The overflow stays as a phone fallback. */
.teams-admin { display: flex; flex-direction: column; gap: 1rem; }
.teams-admin .card { overflow-x: auto; }
.teams-admin .ptable { width: auto; }
.teams-admin .ptable td, .teams-admin .ptable th { white-space: nowrap; }
.teams-admin__row { cursor: pointer; }
.teams-admin__row:hover td { background: var(--border); }
.teams-admin__row.is-selected td { background: rgba(59, 203, 255, 0.15); }
.teams-admin__add { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; margin-top: 0.75rem; }
.teams-admin__toggle { margin-bottom: 0.6rem; }
/* THE PAGE'S OWN TABS under the staff pill strip: the outlined ViewSwitch, the portal's second level, on the left. */
.teams-admin__tabs { display: flex; margin-bottom: 1rem; }
.teams-admin__tabs .viewswitch { margin-left: 0; }
/* THE EDITOR ABOVE ITS LIST ("open above the list with a close button"): marked with the accent edge so it reads as
   what you picked, its title and Close on one line. */
.teams-admin__editor { border-left: 3px solid var(--brand-accent); scroll-margin-top: 5rem; }
.teams-admin__editor-head { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; }
.teams-admin__editor-head .card__title { margin-bottom: 0.6rem; }
.teams-admin__diff { font-size: 0.85rem; margin-top: 0.6rem; }

/* ---- Toggle switch (ToggleSwitch) ------------------------------------------------------------------------------
   An on/off switch for a setting that applies at once (2026-10-30: "check box ugly is there a cool toggle"). Off: a
   grey track; on: the brand accent, the knob slid right. The whole row — switch and label — is the button. */
.toggle {
    display: inline-flex; align-items: center; gap: 0.55rem;
    padding: 0.15rem 0; border: 0; background: none; cursor: pointer;
    font: inherit; font-size: 0.85rem; color: var(--text); text-align: left;
}
.toggle:disabled { opacity: 0.45; cursor: not-allowed; }
.toggle__track {
    position: relative; flex: none; width: 2.25rem; height: 1.3rem; border-radius: 999px;
    background: #c9cfdb; transition: background 0.18s ease;
}
.toggle__knob {
    position: absolute; top: 0.15rem; left: 0.15rem; width: 1rem; height: 1rem; border-radius: 50%;
    background: #fff; box-shadow: 0 1px 3px rgba(12, 2, 36, 0.3); transition: transform 0.18s ease;
}
.toggle.is-on .toggle__track { background: var(--brand-accent); }
.toggle.is-on .toggle__knob { transform: translateX(0.95rem); }
.toggle:not(:disabled):hover .toggle__track { filter: brightness(0.95); }
.toggle:focus-visible { outline: none; }
.toggle:focus-visible .toggle__track { box-shadow: 0 0 0 3px rgba(59, 203, 255, 0.5); }
@media (prefers-reduced-motion: reduce) { .toggle__track, .toggle__knob { transition: none; } }
/* The what-if's customer list: scrolls inside its card, a lost customer greyed (is-muted). */
.teams-admin__customers { margin-top: 0.75rem; max-height: 24rem; overflow-y: auto; }
