﻿/* ============================================================================
   bs5-custom.css — TotaLand's Bootstrap 5 site tweaks (permanent).
   Before the Phase-1 flip this file carried .bs5-scoped fixes for the Map
   Builder islands; Bootstrap 5 is now global (bootstrap5.3/bootstrap.min.css
   + legacy-theme.css), so these rules apply site-wide.
   ========================================================================= */

/* Brand tooltip look (cyan info style, dark text), above pinned modals. */
.tooltip {
  --bs-tooltip-bg: #0dcaf0;
  --bs-tooltip-color: #000;
  --bs-tooltip-zindex: 100010;
}
.popover {
  --bs-popover-zindex: 100005;
}

/* Legacy site chrome uses very high z-indexes; keep overlays above all of it
   (carried over from the Map Builder collision fix). */
.modal {
  z-index: 100000;
}
.modal-backdrop {
  z-index: 99990;
}

/* jquery.impromptu must clear that stack, and it cannot do so on its own: it hard-codes
   zIndex 1100 (its dimming overlay) and 1101 (the message box) as INLINE styles at prompt time
   (js/jquery.impromptu.js:47-48), with no option to change them. 1101 is comfortably below the 100000
   above, so a prompt raised from inside a modal -- which is exactly where pre-submit validation raises
   one -- rendered BEHIND the dialog it was complaining about.

   !important is doing real work here: the values it beats are inline, so an ordinary rule loses.
   Placed above .tooltip (100010) as well, because impromptu is modal and blocking -- nothing that is
   merely hovering should sit over it. If the numbers above ever move, these move with them. */
#jqifade {
  z-index: 100015 !important;
}
#jqi {
  z-index: 100016 !important;
}

/* Legacy modal headers: the grey title bar came from the block-level h2/h3
   filling the header. BS5's flex header shrinks headings to their content
   (title rendered as a small chip) — let them grow again. */
.modal-header h2,
.modal-header h3 {
  flex: 1 1 auto;
}

/* Legacy-parity modal geometry: BS2 dialogs were 560px wide and vertically
   centered. Make that the global default so converted modals need no extra
   classes; per-modal inline max-width still wins. */
.modal-dialog {
  max-width: 560px;
  display: flex;
  align-items: center;
  min-height: calc(100% - var(--bs-modal-margin) * 2);
}
.modal-dialog .modal-content {
  width: 100%;
}

/* Bootstrap 2 button paint bleeding onto Bootstrap 5 buttons.
   legacy-theme.css loads AFTER bootstrap.min.css, and its `.btn` rule sets a
   background-image gradient (#ffffff -> #e6e6e6), a white text-shadow and an inset
   light box-shadow. Bootstrap 5's own state rules -- notably
   `.btn-check:checked + .btn` -- set background-COLOR and color, never
   background-IMAGE, so the BS2 gradient keeps painting straight over the active
   colour: a selected toggle rendered as white text on a near-white gradient, with
   the inset highlight reading as a border drawn inside the control (reported on the
   Map Builder Style Editor's Renderer toggle group).
   NOT scoped to `.bs5`: that wrapper is pre-flip residue -- Bootstrap 5 is the global
   framework now (see the header), so the BS2 paint has to be cleared site-wide or the same
   washed-out state reappears on any page outside the five remaining .bs5 wrappers. */
.btn {
    background-image: none;
    text-shadow: none;
    box-shadow: none;
}

/* legacy-theme's `.btn:focus` / `input:focus` use `outline-offset: -2px`, which draws
   the focus ring INSIDE the control. Inside .bs5, let Bootstrap own focus: its ring is
   an outset box-shadow. `:focus-visible` (not `:focus`) matches BS5, so clicking a
   toggle with the mouse no longer leaves a ring behind. */
.btn:focus,
.btn-check:focus + .btn {
    outline: 0;
    box-shadow: none;
}

.btn:focus-visible,
.btn-check:focus-visible + .btn {
    outline: 0;
    box-shadow: var(--bs-btn-focus-box-shadow, 0 0 0 0.25rem rgba(13, 110, 253, 0.25));
}

/* Bootstrap 2 input styling shrinking and shading Bootstrap 5 switches.
   legacy-theme.css styles bare `input` (BS2 era) with a smaller font-size and
   `box-shadow: inset 0 1px 1px rgba(0,0,0,.075)`. Bootstrap 5 sizes a switch in `em`
   (`.form-switch .form-check-input { width: 2em; height: 1em }`), so the inherited
   font-size shrank the control to ~23x12px instead of 32x16, and the inherited INSET
   shadow painted a dark line inside the pill -- which is what reads as a border drawn
   inside the toggle, worst when checked and the pill is filled blue.
   Measured in-browser before/after: 23.36x11.67 + inset shadow -> 32x16, no shadow.
   Unscoped for the same reason as the button rules above. */
.form-check-input {
    box-shadow: none;
    font-size: 1rem;
}

.form-switch .form-check-input {
    width: 2em;
    height: 1em;
}

/* Two-track variant: Project Management, which has no third column on most pages.
   RADPI's Management/ProjectManagement views #parse a _column3 that HAS NO SOURCE FILE (NVelocity
   ignored the missing parse), so RADPI rendered PM with an empty right column; the port invented a
   Files+Notes sidebar to fill it. Pages that genuinely need a sidebar define @section Column3 and
   keep all three tracks. Replaces the earlier .pm-wide float override so PM uses the same grid as
   every other layout. */
html .tl-grid3.no-col3 {
    grid-template-columns: minmax(0, 150px) minmax(0, 1fr);
}


/* Fluid form fieldsets, with a ceiling for ultrawide displays.
   default.css pins the form geometry independently of the column it sits in:
       fieldset.form                        width: 49em
       fieldset.form span.viewhelper-label  width: 15em  (float:left)
       fieldset.form div.viewhelper-content width: 33em  (float:left)
   15em + 33em = 48em inside a 49em box, so forms rendered at a fixed size no matter how much room
   the column had. That -- not the column count -- was what restricted the fields.

   Applies to ALL layouts, not just the two-column Project Management one: even a standard
   three-column page gives column2 61%, which is ~53em at 1400px and ~73em at 1920px, so these
   forms were being squeezed well below the available space everywhere.

   THE CEILING MATTERS. Fully fluid, column2 in the two-column PM layout is about 190em on a 3440px
   ultrawide; a single-line input that wide is unreadable and drags the eye miles from its label.
   Capping the FIELDSET (rather than the input) keeps the label/content pair together and leaves the
   whitespace outside the box where it belongs. --tl-form-max is the single knob: raise or lower it
   in one place. 80em is a bit over 1.5x the old 49em -- a real gain, still a sane measure.

   The label column stays fixed at 15em on purpose: aligned labels are what make these forms
   scannable, and stretching them would only push inputs further from their labels. The content side
   goes fluid by dropping its float and establishing its own block formatting context, so it fills
   whatever remains beside the label. Deliberately NOT calc(100% - 16em) -- this stylesheet predates
   border-box, so a calc width plus padding would overflow; a BFC is immune to that.
   The input DOES need a rule -- see the box-sizing block below; the note that used to sit here
   said it did not, and that was wrong: the BFC is immune to a calc width, but not to the input's
   own padding.
   .modalContent keeps its 23em, since modals are 560px wide and would break.
   ORDER GOTCHA: _Layout loads bs5-custom.css BEFORE default.css, so an equal-specificity
   selector here LOSES to default.css. Measured in-browser: without the `html` prefix the
   fieldset stayed at 584px (50em). The `html` prefix lifts these to (0,2,1) so they win
   regardless of order, which is safer than reordering the stylesheet links. */
:root {
    --tl-form-max: 80em;

    /* Corner radius for the site's boxed chrome. One knob: fieldsets, cards and the filter bar all
       read from it, so the whole site can be squared off or softened in one edit. */
    --tl-radius: 4px;
}

/* ── Rounded fieldsets, site-wide ─────────────────────────────────────────────────────────────
   ~550 views render a fieldset, either directly or through the view-fieldset / StatefulFieldSet
   helpers (all three emit a real <fieldset>), so this is a single global rule rather than a class
   anyone has to remember to add.

   `html` prefix for the usual reason recorded above: _Layout loads bs5-custom.css BEFORE default.css,
   so an equal-specificity selector here loses. default.css sets no radius today, but prefixing keeps
   the rule safe if one is ever added there.

   .collapsed is deliberately EXCLUDED. default.css:218 strips its left, right and bottom borders and
   keeps only a top one, so it renders as a horizontal rule rather than a box — rounding the corners
   of a single line bows its ends and looks like a rendering fault. */
html fieldset {
    border-radius: var(--tl-radius);
}

/* ── Rounded headers, site-wide ───────────────────────────────────────────────────────────────
   h2/h3/h4 are the grey title bars (default.css:45-87 — #e1e4e9 with 3px 6px of padding), so
   rounding them is the same decision as rounding the fieldsets above and reads from the same
   --tl-radius knob. Square bars beside rounded fieldsets was the inconsistency.

   ALL FOUR CORNERS, not just the top two. Several of these bars sit flush at the top of a
   bordered box that has no padding — the Reports cards are `.container { border: solid 1px
   #44555f }` with the h3 hard against it, and the Desktop's .recent panels are the same. Rounding
   only the top would have needed the BOX rounded to match, and .container is a shared legacy class
   used by the Management tiles, Client role content and the Rescue pages, so changing its shape is
   a wider blast radius than this asks for. All four corners is self-contained: the header reads as
   an inset bar rather than a mitred one, and at 4px the sliver of box background showing at each
   top corner is what makes it read that way. On the Reports cards it is invisible anyway — the
   card is #E3E3E3 against the header's #e1e4e9.

   `html` prefix for the reason recorded at the top of this file: _Layout loads bs5-custom.css
   BEFORE default.css, so a bare `h2` here would lose to default.css's own `h2`. */
html h2,
html h3,
html h4 {
    border-radius: var(--tl-radius);
}

html fieldset.collapsed {
    border-radius: 0;
}

html fieldset.form {
    width: auto;
    max-width: var(--tl-form-max);
}

/* A page of tables rather than forms can opt out of that ceiling: put .tl-cards-full on its .tl-grid2 wrapper and its
   cards take the whole column (owner, 2026-10-12: the User Approval Queue "needs to span the width of the page", in
   both looks). The 80em ceiling keeps a label near its input; a card that holds only a queue table has no such pair,
   and at 1280px it stopped at 946px of a 1218px column. */
html .tl-cards-full fieldset.form {
    max-width: none;
}

/* The block formatting context is `display: flow-root`, and only the SIDES clip (owner report, 2026-10-07: the billing
   item page's Project AFE list "isn't showing over the dropdowns below it"). This used to be `overflow: hidden`, which
   makes the same formatting context -- the box sits beside the floated label and fills the rest of the row; identical
   geometry, measured field by field at 1280px and at phone width -- but clips in BOTH directions, so anything that pops
   DOWN out of a field was cut off at the bottom of its row: the selectize AFE combobox (more than 25 AFEs) opens its list
   absolutely positioned inside this box, and all but its top 4px vanished.
   overflow-x: clip keeps the sideways clip -- a too-wide select, table or unbroken path still stops at the box edge
   instead of running into the next column or scrolling a phone sideways (with no clip at all, a 640px table in a field
   widened a 375px phone page to 670px) -- and, unlike hidden, `clip` does not drag overflow-y along with it, so
   overflow-y stays visible and the list drops down over the fields below. Little in-flow content overflows a height:auto
   box downward (the -4px icon margins on the provisions pages do, by 4px), and what does now shows, as it did in RADPI;
   the sideways clip is ours, from the full-width form layout -- RADPI's float:left; width:33em box never clipped.
   Two traps, because `display` is now what makes the formatting context (overflow-x: clip alone does NOT make one):
   jQuery's .show()/.fadeIn() on a box a stylesheet hid writes display:block inline and drops the field under its label;
   and this rule's display beats a plain hide class (.ui-helper-hidden and the like), so hide a box with an inline
   style or an equally specific rule. */
html fieldset.form div.viewhelper-content {
    float: none;
    width: auto;
    display: flow-root;
    overflow-x: clip;
}

/* A browser without overflow:clip (Safari before 16) drops the declaration above and would clip nothing, scrolling a phone
   sideways on a too-wide field. It gets the old rule instead: the same layout, with pop-ups cut off at the bottom of the
   row, as before 2026-10-07. */
@supports not (overflow-x: clip) {
    html fieldset.form div.viewhelper-content { overflow: hidden; }
}

html fieldset.form div.viewhelper-content.modalContent {
    float: left;
    width: 23em;
    overflow: visible;
}

/* ── .textfield IS ITS DECLARED WIDTH ─────────────────────────────────────────────────────────
   Reported as "the Invoice Number field is cut off on the right by the fieldset", on
   _ClientInvoiceSearchingContent and on other fieldsets too.

   THE ARITHMETIC, measured rather than reasoned:

     default.css:298   fieldset input.textfield { width: 100% }      the declared width
     default.css:362   input { padding: 2px }                        +4px
     default.css:128   input,select,textarea { border: 1px each }    +2px
     legacy-theme.css:77  * { box-sizing: content-box }              so those 6px go OUTSIDE it

   A .textfield therefore measures its container + 6px, ALWAYS -- 6.00px at every container width
   tested, exactly padding+borders, not a rounding artefact. And `html fieldset.form
   div.viewhelper-content` above gave the content div `overflow: hidden` to make it a block
   formatting context beside the floated label, so the 6px was CLIPPED rather than spilled. That
   clip was the reported symptom, and it is why the field looked cut off by the fieldset rather
   than merely overhanging it. (Since 2026-10-07 that rule is display:flow-root plus
   overflow-x:clip: the sides still clip, the bottom no longer does.)

   WHY NOT 99%, OR max-width:99%. Because 6px is a constant and 1% is not. Overhang past the
   content box, measured at seven container widths (* = still clipped):

       container   100% today      99%    max-w:99%   border-box
         220px       +6.00*      +3.95*     +3.95*       0.00
         300px       +6.00*      +3.17*     +3.17*       0.00
         420px       +6.00*      +2.00*     +2.00*       0.00
         600px       +6.00*      +0.23      +0.23        0.00
         900px       +6.00*      -2.70      -2.70        0.00
        1200px       +6.00*      -3.17      -3.17        0.00
        1600px       +6.00*      -3.17      -3.17        0.00

   99% clears 6px only once 1% exceeds 6px, i.e. from ~600px of content width up -- which is where
   a maximised desktop window happens to sit, so it looks like a fix. Below that it still clips;
   above it opens a gap instead (plateauing at -3.17 once the fieldset hits --tl-form-max).
   max-width:99% is identical, because the width being capped is already 100%. border-box is
   0.00px at every width because it is the same arithmetic, not a counterweight to it.

   SCOPE. Two arms, not a lift of the global content-box reset -- that reset is a deliberate
   decision recorded in legacy-theme.css and the damage from flipping it site-wide is why it exists.

     1. .textfield, mirroring default.css:296's selector list exactly, because that is the rule
        handing out width:100%.
     2. every input and textarea inside a labeled wrapper, because div.viewhelper-content is the
        box doing the clipping and 35 views put a width:100% control in one WITHOUT .textfield.
        Measured, only two element types needed it: <select> is already border-box by UA default
        and overflows 0.00px, and a checkbox has no padding or border at all, but a bare
        <textarea class="ckEditorSimple" style="width:100%"> clips by the same 6.00px.

   THE 83 FIELDS WITH AN INLINE WIDTH shrink by exactly 6px each and none of them clip: a
   `width:120px` date box becomes a real 120px instead of 126px, and a `width:98%` becomes 98%.
   That is the author's declared value being honoured, and those 98%s are the same fudge as the
   99% above, written by hand view by view. They can be simplified to 100% now, but are left
   alone -- 83 views is its own change. */
html fieldset input.textfield,
html fieldset textarea.textfield,
html table input.textfield,
html div.viewhelper-content input,
html div.viewhelper-content textarea {
    box-sizing: border-box;
}

/* ── Grid three-column layout (trial: Contract only) ──────────────────────────────────────────
   Replaces the float row for layouts that opt in by wrapping their columns in .tl-grid3.

   Why, measured against the float version at four viewports (probe, real stylesheets):

       width   floats col2/col3/gap      grid col2/col3/gap
       1280    803 / 266 / 51            710 / 400 / 10
       1600    998 / 330 / 112          1030 / 400 / 10
       2000   1242 / 410 / 188          1430 / 400 / 10
       3440   2120 / 410 / 750          2870 / 400 / 10

   Two problems fixed. (1) The float widths are 13% + 61% + 20% = 94%, so the missing 6% became
   dead space between column2 and column3 -- 51px at 1280 but 750px on a 3440 ultrawide. The grid
   gap is a declared 10px at every width. (2) column3's `width: 20%` shrank the sidebar to 266px at
   1280 and 330px at 1600; minmax(300px,400px) holds it usable everywhere, which is what makes the
   sidebar sections readable on a laptop.

   `html` prefix: _Layout loads bs5-custom.css BEFORE default.css, so equal-specificity selectors
   here lose. The column overrides must outrank `div#column1` (0,1,1) hence `html .tl-grid3 > div#column1`.
   align-items:start also lets default.css's min-height:650px stop propping short pages open. */
html .tl-grid3 {
    display: grid;
    /* col3 is capped at BOTH 400px and a third of the row. The 400px alone was wrong between the
       stacking breakpoint and ~1000px: a fixed track is sized before 1fr gets any free space, so on
       an 834px iPad Air/Pro portrait col3 took its full 400px and left col2 just 264px -- the
       sidebar ended up wider than the content. min(400px, 33%) keeps col3 proportional while the
       row is tight and lets it settle at 400px once there is room, with no extra breakpoint and no
       jump at the boundary. Measured: 810px -> 373/267, 834 -> 389/275, 1024 -> 516/338,
       1280 -> 710/400, 3440 -> 2870/400 (col2/col3), so col2 leads at every width. */
    grid-template-columns: minmax(0, 150px) minmax(0, 1fr) minmax(0, min(400px, 33%));
    gap: 0 10px;
    align-items: start;
}

html .tl-grid3 > div#column1,
html .tl-grid3 > div#column2,
html .tl-grid3 > div#column3 {
    float: none;
    width: auto;
    max-width: none;
    min-height: 0;
    /* ⚠ border-box, because a legacy percentage width still reaches these on a phone and the document
       is content-box (legacy-theme.css:77). defaultResponsive.css:  div#column2 { width:98% !important }
       — written for the FLOAT layout, and its !important outranks the `width:auto` above. Under
       content-box that 98% is the CONTENT width, so #column2's own 10px padding and 2x1px borders are
       added on top: measured at a 375px viewport it came out 380px in a 375px track and scrolled the
       page sideways by 5px.

       Fixing the box model rather than adding another !important to the width: border-box makes the
       legacy 98% mean 98% of the track INCLUDING the chrome, which is what that rule always intended,
       and it immunises these columns against any other legacy percentage that lands on them. */
    box-sizing: border-box;
}

html .tl-grid3 > div#column3 {
    margin: 0;
}

/* Stack on small screens, matching the breakpoint defaultResponsive.css already uses for the
   float version (max-width: 800px). Without this the grid keeps three tracks on a phone.

   ⚠ .no-col3 MUST BE NAMED HERE TOO, and leaving it out was a real bug rather than an omission of
   style. A media query adds NO specificity, so this rule is (0,1,1) while `html .tl-grid3.no-col3`
   above is (0,2,1) — the two-column variant beat the phone rule and never stacked. Measured at a
   375px viewport before this line changed:

       grid-template-columns computed to   150px 215px
       #column1   0px          <- its nav is display:none at this width (defaultResponsive.css)
       #column2   159px        <- 42% of the screen

   So 150px of a 375px phone was reserved for a column that renders NOTHING, and the form, the tab
   strip and every field on Edit Project were squeezed into what was left. It affects every
   ProjectManagement view, because .no-col3 is applied to all of them except Snapshot — the only one
   that defines a Column3 section.

   Matching the selector rather than reaching for !important: same specificity, and this block sits
   later in the file, so it wins on order. */
@media (max-width: 800px) {
    html .tl-grid3,
    html .tl-grid3.no-col3 {
        /* minmax(0, 1fr), NOT a bare 1fr -- see the note on .tl-grid2's phone rule below. A bare
           1fr is minmax(AUTO, 1fr), and auto floors the track at the widest item's min-content, so a
           wide table inside would push the track past the screen exactly as it did there. */
        grid-template-columns: minmax(0, 1fr);
    }
}

/* ── Grid two-column layout (pilot: Desktop, Contract/List, Asset/Files) ──────────────────────
   Opt in by wrapping the two columns in .tl-grid2. NO markup reorder: grid fills tracks in source
   order, and column1 is already first everywhere, so it lands in the left track by itself. That is
   the whole reason this replaced the earlier .tl-flow2 float attempt, which needed the sidebar
   first in the DOM and so cost mobile stacking order and focus order.

   What it does NOT do, so nobody re-files it: column1 does not reclaim the sidebar's width lower
   down the page. A grid track spans the full row height. The float version could express that, but
   only for inline content -- <fieldset> and <table> are block-formatting-context roots and may not
   overlap a float, so on these pages (which open column1 with exactly those) it produced a dead
   band that grew whenever a sidebar section was expanded. Reverted in 740282b3.

   SIDEBAR CAP, and why it is NOT .tl-grid3's min(400px, 33%). That cap exists because the
   three-column sidebar was too NARROW (266px at 1280) and needed widening. This sidebar is already
   27% -- 345px at 1280 -- so the same cap would widen it further and take width off column1 exactly
   where it is tightest. min(27%, 400px) never exceeds today's share and stops the sidebar running
   away on wide displays. Column1, floats vs grid:

       width   col1 float (72%)   col1 grid    delta
       1280        922               925         +3
       1600       1152              1190        +38
       1920       1382              1510       +128
       3440       2477              3030       +553

   The 3440 figure is the point: at 27% the sidebar was claiming 929px of an ultrawide for three
   collapsed headers.

   `html` prefix and `>` for the reason documented on .tl-grid3: bs5-custom.css loads BEFORE
   default.css, so an equal-specificity selector would lose to `#two-column1` (1,0,0). */
:root {
    --tl-side2: 27%;   /* matches default.css:1771; the variants below narrow it */
}

html .tl-grid2 {
    display: grid;
    /* The 400px ceiling is a VARIABLE so a page whose right column is not a sidebar can lift it.
       ClientConsole/Index is that page: its Head block sets column1 34.5% / column2 64.5% because the
       eleven-column Client Stats grid lives on the RIGHT, and raising --tl-side2 alone could not
       express it -- min(64.5%, 400px) is still 400px. Defaults to 400px, so every other page is
       unchanged. */
    grid-template-columns: minmax(0, 1fr) minmax(0, min(var(--tl-side2), var(--tl-side2-cap, 400px)));
    gap: 0 10px;
    align-items: start;   /* stop default.css's min-height:650px propping short pages open */
}

html .tl-grid2 > div#two-column1,
html .tl-grid2 > div#two-column2,
html .tl-grid2 > .tl-grid2-full {
    float: none;
    width: auto;
    min-height: 0;
}

/* THE LOWER-RIGHT DEAD SPACE, and the only way a grid can address it. Column 2's track runs the
   whole row height, so once a short sidebar ends, the space beneath it cannot be used by column1 --
   on a list page that is a tall empty band beside the results table. A grid cannot reflow one
   element from narrow to wide (and the float that can, cannot: <fieldset>/<table> are
   block-formatting-context roots that may not overlap a float -- see 740282b3).

   So the wide part becomes its OWN grid item on a second row spanning both tracks. A view opts in
   by closing #two-column1 after the content that belongs beside the sidebar and putting the rest in
   a .tl-grid2-full sibling. Views that do not split behave exactly as before.

   Placement is EXPLICIT rather than relying on auto-flow, so the markup keeps its natural reading
   order -- search block, results, sidebar -- instead of having to emit the sidebar early to make
   auto-placement land correctly. */
html .tl-grid2 > div#two-column1 { grid-column: 1; grid-row: 1; }
html .tl-grid2 > div#two-column2 { grid-column: 2; grid-row: 1; }
html .tl-grid2 > .tl-grid2-full  { grid-column: 1 / -1; grid-row: 2; }

/* ONE SIDEBAR WIDTH, DELIBERATELY. default.css:1786/1795 narrow the sidebar to 19% on
   .projectOverview and 14% on .entityListPage, and this file used to lift both up to the track with
   :has(). They are gone on the user's call, extending the same decision already taken for
   Tract/list: under the grid the narrow variants are not just tighter, they are BROKEN.

   fieldset.form is content-box with 5px of padding and a 1px border each side (default.css), and
   these sidebars set width in percent -- `view-fieldset width="95%"` emits an inline `width: 95%`.
   Content-box means the border box is 95% PLUS 12px, so the fieldset outgrows its parent whenever
   5% of the track is under 12px, i.e. below a ~240px sidebar. At 14% of 1600px the track is 199px
   and the Queued Items fieldset measures 198px against 196px of available content width. Harmless
   while overflow was visible; the moment the cap below made the column a scroll container it became
   the horizontal scrollbar the user reported. Reproduced and measured, not inferred.

   default.css's own `width:14%`/`19%` still sit on the child, but `html .tl-grid2 > div#two-column2`
   (1,1,2) outranks `div#two-column2.entityListPage` (1,1,1), so they stay overridden and every
   two-column sidebar now renders at the 27% default. The box-sizing guard further down closes the
   arithmetic itself, so this cannot come back at some other width. */

/* SOME OF THESE PAGES HAVE NOTHING TO PUT IN THE SIDEBAR. Six views emit #two-column1 and never a
   #two-column2 (Asset/New, Instrument/New, MainClient/createNewBrokerage, Party/Create, Units/New,
   User/List -- an earlier revision of this comment named Project/Overview as the sixth, which is
   wrong: it has a real sidebar of RecentlyViewed/RecentlyCreated/assignments, and Instrument/New
   was converted after that list was written), and another 52 emit the div EMPTY -- 40 as a literal
   <div id="two-column2"></div>, 12 holding nothing but a comment. RADPI does the same (its six
   FileCleanup pages reserve `width:37% !important` for an empty div), and on a float layout that is
   nearly free, because an empty float is zero-height and takes no width from the block beside it.
   A grid track is reserved whether or not anything lands in it. Measured at 1600px: an empty
   sidebar costs column1 410px -- on User/List, which previously ran at width:98%, a straight loss.

   `:not(:has(> div#two-column2 *))` covers all three cases in one selector -- absent, empty, and
   comment-only -- because it asks whether any ELEMENT is inside, not whether the div exists.
   :empty would miss the comment-only twelve, and would also miss the common runtime case of a
   sidebar whose whole body sits behind an @if that rendered false, since Razor still emits the
   literal newlines around it.

   NOT paired with `display: none` on the sidebar, though that recovers a further 17px. :has() is
   live, so hiding it would silently swallow a text-only injection, and leaving it visible means an
   injection just works: verified in a browser that appending an element restores the two-track
   layout on its own, with no reload. */
html .tl-grid2:not(:has(> div#two-column2 *)) {
    grid-template-columns: minmax(0, 1fr);
}

/* CAP THE SIDEBAR'S HEIGHT. Several sidebars render one row per project -- 300+ on a real client --
   and nothing bounds them. On a split page that is not merely ugly: grid row 1 is as tall as its
   tallest item, so .tl-grid2-full begins BELOW THE WHOLE SIDEBAR. Measured on
   Queue/TractsWithoutPolygonRequests at 1600x900: column1 ends at 834px, the sidebar runs to
   6133px, and the results table -- the entire point of the page -- starts at 6132px. The float
   layout had no such failure mode because the results table sat inside column1, beside the sidebar;
   the split is what exposed it, so this rule is part of paying for that.

   RADPI caps the COLUMN itself on exactly one page: Views/Training/_coursePage.vm:255 ships
   <div id="two-column2" style="max-height:650px;">. 650px is RADPI's own min-height for these two
   columns (default.css:1763/1773) -- its idea of one screenful -- so it is the floor here, with
   80vh above it so a tall display is not pinned to a 2009 laptop.

   RADPI sets max-height with NO overflow, so its course-page sidebar visibly spills over whatever
   follows. overflow-y is what makes a cap mean anything. Two consequences, both wanted: per CSS
   Overflow 3 a non-visible overflow-y forces the computed overflow-x to `auto` too, so an
   over-wide table now scrolls inside the column instead of bleeding out of it (verified: computed
   overflow-x is "auto"); and _coursePage's INLINE 650px still beats this rule, so that page keeps
   RADPI's number and merely gains the scrollbar it always needed.

   Safe for the popups these sidebars use: jQuery UI autocomplete, datepicker and tooltip all append
   to <body>, so the new scroll container cannot clip them.

   SCOPED TO SPLIT PAGES ONLY -- `:has(> .tl-grid2-full)` -- and that restriction is load-bearing,
   not caution. A long sidebar only HIDES anything when there is a second band underneath it; on a
   wrapper-only page it merely makes the page long, which is what RADPI did. Capping every sidebar
   would have put a scroll container on three pages where it is actively wrong:

     ClientConsole/Index    -- #two-column2 is the MAIN content there, an eleven-column stats table
                               the view widens to 64.5%. Capping it at 80vh, plus the forced
                               overflow-x, would wreck the page. Not split.
     Shared/_ProjectLayout  -- its sidebar already contains 300px scrollers (ProjectFiles, Notes),
                               so a column cap would nest a third scroll region around them. A
                               LAYOUT can never be split anyway: #two-column1 holds RenderBody().
     FileCleanup/*Files, ProvisionsLibrary/list
                            -- #two-column2 ships empty and is filled by jQuery("#two-column2")
                               .html(...) on click. Those are AJAX detail panes, not summaries.

   Worth knowing that RADPI answers this question differently: 56 views cap a fieldset BODY at
   max-height:300px (layouts/Contract.vm, Instrument.vm, Party.vm and the notes/files components
   among them) and never the column. That convention is per-panel and stays available; it just does
   not solve THIS problem, because it is the column's total height that sets the grid row.

   ── PER-PAGE HEIGHT, AND WHY IT HAS TO BE PER-PAGE ────────────────────────────────────────────
   The default suits a page whose column1 is a full search form, ~700-800px: both columns end
   together and the results band follows immediately. Where column1 is SHORT the cap is still taller
   than it, and the band is pushed down by the difference -- 380px of dead space beside nothing on a
   340px column1, 640px on an 80px one. Measured.

   I tried to make this automatic first, and it does not work. The obvious trick --
   `height: 100%; min-height: 0` on the sidebar, the standard "fill the row and scroll inside"
   recipe -- was measured doing the opposite: the sidebar went to its full 6062px content height on
   all three test pages, because the row is auto-sized, so a percentage height resolves against an
   indefinite length and falls back to auto. A grid row is sized by its tallest item; nothing in CSS
   lets one item cap itself at the height of its sibling. So the page has to say.

   Add ONE of these to the .tl-grid2 wrapper, sized to that page's column1:

       <div class="tl-grid2 tl-side2-xs">     column1 under ~220px
       <div class="tl-grid2 tl-side2-sm">     column1 ~220-350px
       <div class="tl-grid2 tl-side2-md">     column1 ~350-550px
       <div class="tl-grid2 tl-side2-lg">     column1 ~550-700px
       <div class="tl-grid2 tl-side2-none">   never cap or scroll this sidebar

   -xs exists because two real pages need it: Project/Overview and Queue/BaseMaps both measure
   ~160px of column1, where even -sm leaves ~140px of band. Neither uses it yet -- 200px is a small
   window on a RecentlyViewed list, and that trade is the page author's to make, not this file's.

   ONLY CAP A SIDEBAR THAT CAN GROW. Pick the class from column1's height, but qualify FIRST on the
   sidebar's content, which is the check I skipped on the first pass:

     growable -> cap.   RecentlyViewed / RecentlyCreated are AJAX-filled and hold up to 50 rows each
                        (NavigationHistoryService.MaxHistoryRecords), the Queue tables grow with the
                        queue, "Tracts by Project" is one row per project.
     FIXED    -> NEVER. A panel of static chrome cannot push .tl-grid2-full down, so a cap buys
                        nothing and only clips it. Views/Support/_helpDeskAndSupport.cshtml was the
                        one to watch: Resource/list and NotificationLog/index were given
                        tl-side2-sm and its contacts scrolled inside 300px. Both are plain wrappers
                        again, and that panel is now .tl-supportbar below -- 129px, well under the
                        default max(650px, 80vh), so the cap never engages either way. The real
                        control on a fixed panel is making it SHORTER THAN COLUMN1, not capping it.

   Fixed px on purpose, where the default is viewport-relative: the default answers "how much screen
   may a sidebar eat", these answer "how tall is the form next to it", and that is a px quantity
   that does not change when the window does. 300px for -sm is RADPI's own per-panel number.

   An explicit class also ENABLES the cap on a wrapper-only page, which the default deliberately
   does not touch -- use it when a long sidebar makes such a page run on, but check the
   ClientConsole/_ProjectLayout/AJAX-pane list above first, because those are why the default is
   narrow. -none is the escape hatch and must stay last: it ties with the split rule on specificity
   (1,2,2 both) and wins on source order. */
html .tl-grid2               { --tl-side2-max: max(650px, 80vh); }
html .tl-grid2.tl-side2-xs   { --tl-side2-max: 200px; }
html .tl-grid2.tl-side2-sm   { --tl-side2-max: 300px; }
html .tl-grid2.tl-side2-md   { --tl-side2-max: 450px; }
html .tl-grid2.tl-side2-lg   { --tl-side2-max: 650px; }

html .tl-grid2:has(> .tl-grid2-full) > div#two-column2,
html .tl-grid2.tl-side2-xs > div#two-column2,
html .tl-grid2.tl-side2-sm > div#two-column2,
html .tl-grid2.tl-side2-md > div#two-column2,
html .tl-grid2.tl-side2-lg > div#two-column2 {
    max-height: var(--tl-side2-max);
    overflow-y: auto;
}

html .tl-grid2.tl-side2-none > div#two-column2 {
    max-height: none;
    overflow-y: visible;
}

/* Kill the percentage-plus-chrome overflow at the source rather than only at 27%. See the sidebar
   width note above: a content-box fieldset with an inline `width: 95%` occupies 95% + 12px, so it
   can always outgrow a narrow column. border-box makes the declared percentage mean the border box,
   which is what every one of these views assumes. Scoped to the sidebar because that is the only
   column narrow enough for the 12px to matter, and because a global box-sizing change would touch
   all 290 fieldsets in the app. */
html .tl-grid2 > div#two-column2 fieldset {
    box-sizing: border-box;
}

/* Stack at the breakpoint defaultResponsive.css:588 already uses for the float version. Column1 is
   first in the DOM, so the stacked order is content-then-sidebar with no ordering rule needed. */
@media (max-width: 800px) {
    html .tl-grid2 {
        /* ⚠ minmax(0, 1fr), NOT a bare `1fr`. This one line was the whole of the reported
           "project management is broken on my phone".

           A bare `1fr` is `minmax(AUTO, 1fr)`, and the auto minimum floors the track at the widest
           grid item's MIN-CONTENT. Project Management's results table has 16 columns, so the track
           refused to go below what that table needs and the page grew past the screen. Then it
           latched: FooTable picks its breakpoint from min(table, viewport, PARENT) -- and the parent
           is this track -- so a widened track made FooTable believe it was on a tablet, which kept
           all 16 columns rendered, which kept the track wide.

           And the symptom is not a scrollbar. _Layout.cshtml:43 declares width=device-width with no
           initial-scale=1, so the browser shrink-to-fits instead. Measured at a 375px screen with
           representative project names, BEFORE this change:

               grid-template-columns   639.328px
               window.innerWidth       644px  (against a 375px initial containing block)
               visualViewport.scale    0.582      <- the ENTIRE page rendered at 58%
               FooTable breakpoint     "tablet"   -> 16 of 16 columns still shown

           The desktop rules in this same file already write it correctly -- :381 and :451 both use
           minmax(0, ...). Only this override dropped it.

           NOTE FOR ANYONE REPRODUCING IT: a table of SHORT values collapses on its own and never
           floors the track, so the defect hides behind unrealistic test data. And
           `document.scrollWidth <= clientWidth` cannot see it either, because shrink-to-fit keeps
           clientWidth at 375 while innerWidth balloons. Measure the TRACK, the FooTable breakpoint,
           or visualViewport.scale. */
        grid-template-columns: minmax(0, 1fr);
    }

    /* The explicit rows above MUST be released here. Both columns are pinned to grid-row 1, which
       in a single-track grid would stack them on top of each other in the same cell rather than one
       after the other. Back to auto placement, which follows DOM order: search block, results,
       sidebar -- content first, which is what the float version needed an order:-1 rule to get. */
    html .tl-grid2 > div#two-column1,
    html .tl-grid2 > div#two-column2,
    html .tl-grid2 > .tl-grid2-full {
        grid-column: 1;
        grid-row: auto;
    }

    /* Release the cap once the sidebar is stacked UNDER the content -- the per-page tl-side2-*
       classes too, since they are the same cap at a different height. There is no row height to
       bound here (the thing the cap protects, .tl-grid2-full, is already above the sidebar) and a
       scroll region nested inside a single-column phone layout is a trap: the page stops scrolling
       under your thumb and the panel starts. */
    html .tl-grid2:has(> .tl-grid2-full) > div#two-column2,
    html .tl-grid2.tl-side2-xs > div#two-column2,
    html .tl-grid2.tl-side2-sm > div#two-column2,
    html .tl-grid2.tl-side2-md > div#two-column2,
    html .tl-grid2.tl-side2-lg > div#two-column2 {
        max-height: none;
        overflow-y: visible;
    }
}

/* ── Help-desk contact cards (Views/Support/_helpDeskAndSupport.cshtml) ───────────────────────
   Lives in #two-column2, and the whole design constraint is ONE NUMBER: the panel must be no
   taller than #two-column1, because a grid row is as tall as its tallest item and anything over
   that pushes .tl-grid2-full -- the results -- down by the difference. Sitting in the sidebar
   costs nothing while that holds; the panel is free real estate.

   That is why this is not RADPI's stack of 60%-wide tables, which measured 573px against a 155px
   search form and shoved the Resource/list results 615px below the heading (9 of 15 rows above the
   fold at 1600x900, 3 of 15 at 1280x800). The cards are 129px in a 397px sidebar, so the results
   now start 170px below the heading -- flush under the search form, pushed by nothing.

   The panel was cut to the contact cards on the way. What went, and why it was safe:
     * FAQ / Browsers / Other Products -- a 1280x800 recommendation, Firefox and Chrome logos and
       a Get Adobe Reader badge. Retired.
     * "Submit Support Ticket" -- Views/HelpDesk/index.cshtml already renders "Create New Support
       Ticket" above its search filters, same helpDesk/new target, and every page carrying this
       panel also carries the Help Desk tab.
     * SubmitForm() and the empty #col2 div -- both dead. #supportForm exists nowhere in the
       codebase and nothing ever populated #col2; RADPI's copies are equally inert.

   MEASURED against worst-case content -- two contacts with long hyphenated names, 40-character
   emails and all three phone numbers each -- as push in px over #two-column1, by grid width:

       1920 1600 1440 1280 1152 1024  900  810
   Resource/list      0    0    0    4    4   31   31   46      (col1 155px, the shorter form)
   NotificationLog    0    0    0    0    0    0    0    5      (col1 196px)

   Clean to 1280, which is the resolution the app itself recommends, and the residue below that is
   tens of px against a 573px starting point. Under 800 the grid stacks and the sidebar sits below
   the results, where it cannot push anything.

   Two rejected alternatives, both measured, so nobody re-tries them:
     * Cards side by side INSIDE the sidebar (~194px each) is shorter still -- 90px -- and holds 0
       push all the way to 1024. It also spills a long email 13px out of the card at 1600, 41px at
       1280 and 75px at 1024, because an email has no break opportunity a browser will take. The
       height win is not worth silently clipping a contact address.
     * A full-width horizontal band above the two-column block. Works, but costs its own 96px of
       vertical space on every page, where the sidebar costs zero. */
.tl-supportbar {
    display: flex;
    flex-wrap: wrap;
    align-items: stretch;
    gap: 8px;
    margin: 0 0 14px;
}

.tl-supportbar-card {
    /* 280px basis is the wrap point: a pair needs 2x280 + 8, so they break to one per row below
       ~568px of container, which is every sidebar this renders in (266-397px). The cap only bites
       in a wide container and stops a lone card -- the no-help-desk-users case renders three --
       stretching the full width for one phone number. */
    flex: 1 1 280px;
    max-width: 420px;
    min-width: 0;
    border: 1px solid #d5d5d5;
    background: #fff;
}

.tl-supportbar-card > .tl-sb-head {
    background: #0079a8;   /* tr.blue th, from the partial's own <style> before it was removed */
    color: #fff;
    font-weight: bold;
    font-size: .9em;
    padding: 2px 6px;
}

.tl-supportbar-card > .tl-sb-body {
    background: #f7f7f7;   /* tr.gray td */
    padding: 3px 6px;
    line-height: 1.35;
    font-size: .95em;
}

.tl-supportbar-card .tl-sb-name { font-weight: bold; }
.tl-supportbar-card .tl-sb-none { color: #777; font-style: italic; }

/* Name and email share a row -- the mail link is deliberately NOT display:block. That one line
   saved is what takes the pair from 159px to 129px and clears #two-column1 at 1440 and 1600.
   The separator hangs off the LINK, not the name, so it disappears with the link rather than
   leaving a dangling dash. break-word is a guard for an address longer than the 40-char worst
   case measured above; it costs nothing at any width tested. */
.tl-supportbar-card .tl-sb-mail { overflow-wrap: break-word; }
.tl-supportbar-card .tl-sb-mail::before { content: " — "; color: #aaa; }

/* Numbers run inline with separators instead of one <br/> per line. Three stacked lines was most
   of each card's height in RADPI's version. */
.tl-supportbar-card .tl-sb-phones { color: #555; font-size: .9em; }
.tl-supportbar-card .tl-sb-phones > span + span::before { content: " · "; color: #aaa; }

/* No md breakpoint here on purpose. flex-wrap already gives one card per row once the container
   drops below the basis pair, and stacking at 767.98 would break a pair that still fits side by
   side at 760 (measured 372px each) if this ever renders somewhere wide. Below the wrap point the
   cap is the only thing holding a single-column card short of the full width, so release it there
   and nowhere else. */
@media (max-width: 567.98px) {
    .tl-supportbar-card {
        max-width: none;
    }
}

/* ── Compact filter header (Views/Project/Cards.cshtml) ───────────────────────────────────────
   Replaces the "Project Search" fieldset, which spent a 33em label column and a whole grid track on
   four controls. One wrapping row instead, labels stacked above their inputs so each field is only
   as wide as it needs to be. No Search button: every control re-queries on change/input.

   flex-wrap, not a media query — the fields have honest intrinsic widths, so they reflow at whatever
   width the page happens to be rather than at breakpoints picked in advance. */
/* align-items: flex-start, NOT flex-end. The controls are different natural heights (text input vs
   select vs 18px switch vs 22px button), so bottom-aligning them left every LABEL sitting at a
   different y — the staggered look. Pinning both rows to fixed heights and aligning from the top
   puts every label on one line and every control on another. */
.tl-filterbar {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: 8px 12px;
    margin: 0 0 12px;
    padding: 8px 10px;
    border: 1px solid #d5d5d5;
    border-radius: 3px;
    background: #f7f7f7;
}
.tl-filterbar-field { display: flex; flex-direction: column; gap: 2px; min-width: 0; }

/* Fixed label row: an explicit height means a wrapped or short label cannot shift the control
   beneath it out of line with its neighbours. */
.tl-filterbar-field > label {
    font-size: .8em;
    color: #777;
    line-height: 14px;
    height: 14px;
    white-space: nowrap;
    overflow: hidden;
}

/* Fixed control row. Every control is forced to the same box so the row reads as one line —
   border-box because the legacy theme puts padding and borders on inputs and selects. */
.tl-filterbar-field > input,
.tl-filterbar-field > select,
.tl-filterbar-field > .tl-dirtoggle {
    height: 24px;
    box-sizing: border-box;
    margin: 0;
    vertical-align: top;
}
/* The segmented control is 24px tall by construction (22px line + 1px borders). */
.tl-filterbar-field > .tl-seg { height: 24px; box-sizing: border-box; }
/* The name box takes the slack again. What stops it pushing the selects onto a second row is not a
   width cap on the box but .tl-filterbar-controls below, which keeps the other controls together as
   one unwrappable unit — so the row breaks in one predictable place instead of shedding one control
   at a time. */
.tl-filterbar-grow { flex: 1 1 220px; }
.tl-filterbar-grow > input { width: 100%; box-sizing: border-box; }

/* nowrap: the selects and toggles move to the next line TOGETHER or not at all. */
.tl-filterbar-controls {
    display: flex;
    flex-wrap: nowrap;
    align-items: flex-end;
    gap: 8px 12px;
    flex: 0 0 auto;
}

/* Sort direction as a two-state button rather than a second dropdown — one less select competing
   for the row, and the arrow reads at a glance. */
.tl-dirtoggle {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 0 8px;
    border: 1px solid #b9c2c9;
    border-radius: 3px;
    background: #fff;
    font: inherit;
    font-size: .85em;
    line-height: 1;
    white-space: nowrap;
    cursor: pointer;
}
.tl-dirtoggle:hover { background: #eef4f8; border-color: #8fa9bb; }
.tl-dirtoggle:focus-visible { outline: 2px solid #0079a8; outline-offset: 1px; }
.tl-dirtoggle-arrow { font-weight: bold; color: #0079a8; }
.tl-dirtoggle[data-dir="DESC"] .tl-dirtoggle-arrow { color: #a35700; }
/* Segmented control for the three-state Status filter. Real radios under the hood — one name, three
   values — so exactly one value posts, keyboard arrow-keys move between options for free, and none of
   the hidden-companion juggling the old two-state switch needed applies. */
.tl-seg {
    display: inline-flex;
    border: 1px solid #b9c2c9;
    border-radius: var(--tl-radius);
    overflow: hidden;          /* clips the segment backgrounds to the rounded ends */
    background: #fff;
}
.tl-seg > input {
    /* Visually hidden, NOT display:none — that would drop them from the tab order and the POST. */
    position: absolute;
    opacity: 0;
    width: 1px;
    height: 1px;
    margin: -1px;
    overflow: hidden;
}
.tl-seg > label {
    padding: 0 9px;
    border-left: 1px solid #dfe3e6;
    font-size: .85em;
    line-height: 22px;
    color: #444;
    white-space: nowrap;
    cursor: pointer;
    user-select: none;
}
.tl-seg > input:first-of-type + label { border-left: 0; }
.tl-seg > label:hover { background: #eef4f8; }
.tl-seg > input:checked + label { background: #0079a8; color: #fff; }
/* Focus lands on the hidden radio, so the ring has to be drawn on its label. */
.tl-seg > input:focus-visible + label { outline: 2px solid #0079a8; outline-offset: -2px; }

/* LINK variant of the same control, for a filter that is plain navigation rather than a form field —
   the client sidebar's active filter re-requests the current page with ?childActive=. Anchors instead
   of radios because there is no form in the sidebar to submit and no JS is needed: each segment is a
   real link, so it works with the keyboard, middle-click and back button for free.
   Selected state is a class here rather than :checked, since there is no input to check. */
.tl-seg-links > a {
    padding: 0 9px;
    border-left: 1px solid #dfe3e6;
    font-size: .85em;
    line-height: 22px;
    color: #444;
    text-decoration: none;
    white-space: nowrap;
    user-select: none;
}
.tl-seg-links > a:first-child { border-left: 0; }
.tl-seg-links > a:hover { background: #eef4f8; color: #444; text-decoration: none; }
.tl-seg-links > a.sel { background: #0079a8; color: #fff; }
.tl-seg-links > a:focus-visible { outline: 2px solid #0079a8; outline-offset: -2px; }

.tl-filterbar-actions {
    display: flex;
    align-items: baseline;
    gap: 12px;
    margin-left: auto;      /* pins the count and the view links to the right edge of the bar */
    /* The bar aligns to flex-START so the label and control rows line up across fields. This block
       has no label, so without an override it would ride up level with the LABELS. flex-end drops it
       onto the control row, where the count sat before the alignment fix. */
    align-self: flex-end;
    height: 24px;
    align-items: center;
}
.tl-filterbar-count { font-weight: bold; white-space: nowrap; }
.tl-filterbar-link { white-space: nowrap; font-size: .9em; }

/* ── Project cards (Views/Project/Cards.cshtml) ───────────────────────────────────────────────
   Card presentation of the project list that Views/Project/Overview.cshtml renders as a table.
   Both views run off the same controller data-prep, so this is styling only — no data or
   permission logic lives here.

   auto-fill + minmax rather than a fixed column count: the grid sits in .tl-grid2-full, whose
   width changes with the sidebar and the viewport, and a card needs ~320px before the two-column
   meta list starts wrapping mid-word. auto-fill (not auto-fit) keeps the track width stable when
   a page returns one or two projects, so a single result renders as one card rather than one card
   stretched across the full width.

   Palette matches .tl-supportbar above (#0079a8 heading blue, #d5d5d5 border, #f7f7f7 panel) so the
   two card treatments in the app read as the same component family. */
/* Loading state for the card grid. The overlay sits ON the grid rather than replacing it, so
   filtering as you type does not collapse the page height and bounce the scroll position on every
   keystroke — the previous cards stay put, dimmed, until the new ones arrive.

   NOTE: reload() was already adding .tl-pcards-busy before this block existed, so the class was
   applied on every fetch and styled by nothing at all — there was no loading feedback whatsoever. */
.tl-cards-region { position: relative; }

/* Only while a FIRST load is in flight: with no cards yet the region has no height, so the overlay
   would have nowhere to sit and the page would look empty rather than busy. */
.tl-cards-region-empty { min-height: 120px; }

.tl-pcards-busy {
    opacity: .45;
    pointer-events: none;   /* stops clicks landing on cards that are about to be replaced */
    transition: opacity .1s ease;
}

.tl-cards-loading {
    position: absolute;
    top: 0;
    left: 50%;
    transform: translateX(-50%);
    z-index: 2;
    display: flex;
    align-items: center;
    gap: 6px;
    margin-top: 24px;       /* sits near the top of the region, visible without scrolling a long grid */
    padding: 6px 14px;
    border: 1px solid #d5d5d5;
    border-radius: 3px;
    background: #fff;
    box-shadow: 0 1px 3px rgba(0, 0, 0, .12);
    font-size: .9em;
    color: #555;
}
.tl-cards-loading[hidden] { display: none; }   /* [hidden] loses to display:flex without this */
.tl-cards-loading img { width: 16px; height: 16px; }

.tl-pcards {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
    gap: 12px;
    list-style: none;
    margin: 0 0 14px;
    padding: 0;
}

.tl-pcard {
    display: flex;
    flex-direction: column;
    min-width: 0;               /* lets long project names ellipsis instead of widening the track */
    border: 1px solid #d5d5d5;
    background: #fff;
    border-radius: 3px;
    box-shadow: 0 1px 2px rgba(0, 0, 0, .06);
}

/* Title and status badge share the header row. The badge never shrinks; the title takes what is left
   and ellipsises, so a long project name cannot squeeze the badge out or push it onto a second line. */
.tl-pcard-head {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 10px;
    border-bottom: 1px solid #e6e6e6;
    background: #f7f7f7;
}

/* .tl-state-pill is the same chip under a name that is not tied to project cards, because the client
   sidebar and the client edit form now use it for active/inactive too. One declaration, two names —
   duplicating the values would let the two drift into slightly different greens. .tl-pcard-badge is
   kept as the original name so the project-card markup and its comments stay accurate. */
.tl-pcard-badge,
.tl-state-pill {
    flex: 0 0 auto;
    padding: 1px 7px;
    border: 1px solid transparent;
    border-radius: 9px;
    font-size: .75em;
    font-weight: bold;
    line-height: 15px;
    text-transform: uppercase;
    letter-spacing: .02em;
}
/* Colour is a reinforcement, not the message — the word itself says which state it is, so this still
   reads for anyone who cannot separate the two hues. */
.tl-pcard-badge.is-active,
.tl-state-pill.is-active    { background: #e3f5e1; border-color: #b9ddb3; color: #2f6b28; }
.tl-pcard-badge.is-inactive,
.tl-state-pill.is-inactive  { background: #ededed; border-color: #d2d2d2; color: #6b6b6b; }

/* Inline in a form row / list item rather than as a flex child of a card header. */
.tl-state-pill { display: inline-block; vertical-align: middle; }

/* SUSPENDED is a THIRD state, not the opposite of active — a suspended user can still be active, so
   this rides alongside the active/inactive pill rather than replacing it. Amber deliberately carries
   over the meaning of the #FFF47F row highlight it replaces on the user list, which shouted at the
   whole row (and fought the odd/even striping) to say one thing about one account. */
.tl-state-pill.is-suspended { background: #fdf3d3; border-color: #e0c67e; color: #7a5a12; }

/* YES / NO — the boolean grid columns, emitted by <status-badge> (StatusTagHelpers.cs). Same chip as
   active/inactive so a row reads as one family rather than two vocabularies; green and red carry the
   sense the old flood-filled cells had, at a fraction of the ink.

   The word is VISIBLE here, which is what makes these safe where a bare coloured dot was not: colour
   alone is unreadable for roughly one man in twelve, red/green being the common case, and a dot left
   a screen reader with an empty cell. "Yes"/"No" needs no hidden text and no legend.

   is-yes reuses is-active's greens exactly rather than restating them — one green in the palette. */
.tl-state-pill.is-yes { background: #e3f5e1; border-color: #b9ddb3; color: #2f6b28; }
.tl-state-pill.is-no  { background: #fdeaea; border-color: #eeb5b8; color: #9a3138; }

/* NAME + PILL ON ONE LINE. The suspended pill is inline-block, so in a narrow name column it wrapped
   under the name instead of sitting beside it. Flex is what .tl-state-pill was built for — it already
   carries flex: 0 0 auto above — so the pill holds its size and the name takes the slack. */
.tl-namecell {
    display: flex;
    align-items: center;
    gap: 6px;
}

/* WIDE TABLE IN A NARROW COLUMN. A grid whose natural width exceeds its container has only two
   honest outcomes: compress until it is unreadable, or scroll. A percentage width picks the first
   silently — and only up to a point, because a table cannot compress below its min-content width,
   which one long unbreakable token (an email address) is enough to set. Past that it simply spills
   over whatever sits to its right.

   Measured on the user-associations view, whose 14 columns want 1291px inside a 953px #column2:
   the table was compressing 338px worth, wrapping names onto three lines at 47px per row, and
   overflowing anyway once the widest row's min-content passed the column width.

   THE WRAPPER IS A SAFETY NET, NOT THE MECHANISM. It guarantees the table can never paint over
   whatever sits to its right, whatever the data does — but it should essentially never scroll, and
   the rules below are what keep it from needing to. A horizontal scrollbar is close to useless on a
   list this shape: it sits at the BOTTOM of the scroll container, which on a 74-row list is over
   800px below the fold, and with overlay scrollbars there is nothing on screen to hint it is there.
   Measured. So do NOT reintroduce min-width: max-content here — it makes the table take its full
   1291px and hands the user an unreachable scrollbar instead of a readable table.

   MAKE IT FIT INSTEAD. A table's floor is its min-content width, and one unbreakable token is enough
   to set it: a single 31-character email address held this list at 853px. Letting those break drops
   the floor to 694px, and shortening three very long headers takes it to 612px against ~953px
   available — enough headroom to compress comfortably instead of overflowing, with rows falling from
   47px to 33px because less has to wrap. */
.tl-tablescroll { overflow-x: auto; }

/* For a cell whose content is one long unbreakable token — an email address, a URL. `anywhere`
   rather than `break-word`: break-word still refuses to break a word that would fit on a line of its
   own, which is exactly the case that sets a table's min-content floor. */
.tl-breakany { overflow-wrap: anywhere; }

/* A principal-client picker and its "Include inactive clients" toggle, sharing ONE viewhelper content
   cell instead of taking two form rows.

   The toggle must sit inside the labeled-* content div, not beside it: default.css floats
   span.viewhelper-label (15em) and div.viewhelper-content (33em) as a pair, so a second bare
   .viewhelper-content with no label span ahead of it floats to the far left and lands under the label
   column — which is exactly how the first cut rendered.

   The select takes the slack and the toggle keeps its width, so the checkbox lines up down the right
   of the cell across every screen that uses this. */
.tl-picker-row {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
}
.tl-picker-row > select { flex: 1 1 12em; min-width: 0; }
.tl-picker-inactive {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    gap: 4px;
    color: #666;
    font-size: .85em;
    white-space: nowrap;
    cursor: pointer;
}
.tl-picker-inactive input { margin: 0; }

/* .project-background supplies the project icon as a background-image with padding-left:30px
   (default.css:1853), so the title only needs sizing and truncation on top of it. */
.tl-pcard-title {
    display: block;
    /* min-width:0 is what makes the ellipsis work now that the header is a flex row: without it a
       flex item refuses to shrink below its content, so the title would push the badge out of the
       card instead of truncating. */
    flex: 1 1 auto;
    min-width: 0;
    font-weight: bold;
    font-size: 1.05em;
    line-height: 20px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Not viewable (Project.IsViewableBy false — typically an inactive project the user is still
   associated with). cardHtml renders a <span> here instead of an <a>, so this only has to stop the
   title reading as a link. Deliberately NOT default.css's global .disabled: its
   `background-image: none !important` would strip the .project-background icon off the title. */
.tl-pcard-title-off { color: #6b6b6b; cursor: default; }

.tl-pcard-meta {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 2px 10px;
    margin: 0;
    padding: 8px 10px;
    font-size: .95em;
}
.tl-pcard-meta dt { color: #777; }
.tl-pcard-meta dd { margin: 0; min-width: 0; overflow-wrap: break-word; }
.tl-pcard-meta dd.tl-pcard-confidential { color: #777; font-style: italic; }

/* Realm and map links. The table showed these as bare 16px icons in two dense columns, which
   relied on hover titles to be readable; a card has room to label them. */
.tl-pcard-realms,
.tl-pcard-maps {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    padding: 0 10px 8px;
}
.tl-pcard-maps { border-top: 1px dotted #e6e6e6; padding-top: 8px; }

.tl-pcard-chip {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    max-width: 100%;
    padding: 2px 7px 2px 5px;
    border: 1px solid #dfe3e6;
    border-radius: 10px;
    background: #f4f7f9;
    font-size: .85em;
    line-height: 18px;
    text-decoration: none;
}
.tl-pcard-chip:hover { background: #e8eff4; border-color: #b9ccd8; text-decoration: none; }
.tl-pcard-chip img { width: 16px; height: 16px; flex: 0 0 16px; }
.tl-pcard-chip span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Inert chip: chip-shaped, but a <span> rather than a link. Greys match .tl-pcard-badge.is-inactive so
   a suppressed card reads as one state, not two competing ones.
   NOTE: this also finally supplies the rule the "Maps disabled" pill has been asking for since it
   shipped — until now that class had no definition anywhere and the pill was styled exactly like a
   live, clickable chip. Its appearance changes with this rule; that is the point. */
.tl-pcard-chip-off,
.tl-pcard-chip-off:hover {
    background: #ededed;
    border-color: #d2d2d2;
    color: #6b6b6b;
    cursor: default;
    pointer-events: none;
}

/* A map is ONE control with up to two destinations: the named chip opens the classic (v1) viewer, the
   joined "v2" segment opens the OpenLayers map. Wrapping the pair keeps them together when the maps row
   wraps, and squaring the touching edges makes them read as one pill rather than two chips.
   Replaces .tl-pcard-beta, which was a bare bold word floating outside the pill with no hit area of its
   own and no tooltip. */
.tl-pcard-mapgrp { display: inline-flex; align-items: stretch; max-width: 100%; }
.tl-pcard-mapgrp .tl-pcard-chip { min-width: 0; }

.tl-pcard-chip-main {
    border-top-right-radius: 0;
    border-bottom-right-radius: 0;
}
.tl-pcard-chip-v2 {
    margin-left: -1px;              /* collapse the shared border so the seam is a single line */
    padding: 2px 7px;
    border-top-left-radius: 0;
    border-bottom-left-radius: 0;
    font-size: .78em;
    font-weight: bold;
    color: #2c5c7a;
}
.tl-pcard-chip-v2:hover { color: #1c3d52; }

/* Map Builder map: no classic renderer exists, so the single chip IS the v2 map and says so inline. */
.tl-pcard-tag {
    flex: 0 0 auto;
    margin-left: 4px;
    padding: 0 4px;
    border-radius: 6px;
    background: #dfe9f0;
    color: #2c5c7a;
    font-size: .72em;
    font-style: normal;
    font-weight: bold;
    line-height: 14px;
}

/* Statistics: only rendered when the Show Overview Stats toggle is on, and only fetched when the
   card's own link is clicked — same lazy AjaxShowStats call the table made, so turning the toggle
   on does not cost one request per card up front. */
.tl-pcard-foot {
    margin-top: auto;           /* pins the control to the bottom on a short card in a tall row */
    padding: 8px 10px;
    border-top: 1px solid #e6e6e6;
    background: #fafafa;
}
.tl-pcard-statsbtn { font-size: .9em; }

/* The animating wrapper stays a plain block: jQuery's slideDown writes display:block inline, which
   would override a `display: grid` here and silently collapse the columns back to one. The grid is a
   separate child so the two concerns cannot fight. */
.tl-pcard-stats { display: none; margin-top: 8px; }

/* Eight figures in COLUMNS rather than one long list — a single column pushed the card to roughly
   double height when opened and made the whole row grow with it. auto-fit with a 120px minimum gives
   two columns in a normal card and drops to one only when the card is genuinely narrow. */
.tl-pcard-stats-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
    gap: 0 14px;
}

.tl-pcard-stat {
    display: flex;
    justify-content: space-between;
    gap: 10px;
    padding: 2px 0;
    border-bottom: 1px dotted #e0e0e0;
    font-size: .9em;
}
.tl-pcard-stat-k { color: #777; }
.tl-pcard-stat-v { font-weight: bold; }
.tl-pcard-stat-err { color: #a33; font-size: .9em; }

.tl-pcard-toolbar {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 10px;
    margin-bottom: 6px;
}
.tl-pcard-count { font-size: medium; font-weight: bold; }

/* One card per row on a phone: 320px minus the page gutters leaves no room for a grid track, and
   auto-fill would otherwise hold a track wider than the screen and scroll the page sideways. */
@media (max-width: 480px) {
    .tl-pcards { grid-template-columns: 1fr; }
}

/* ---------------------------------------------------------------------------
   "Just generated" highlight on the entity Documents pages
   (js/RADPI/BackgroundReports.js -> render()).

   Applied to the whole row, not the badge: the "Request ..." link and the
   generated file's download link live in different cells and both belong to
   "this one is new".

   Deliberately a background tint plus a left edge rather than a border. Those
   pages give every request block its own background colour already (#faffcf,
   #c9c993, #b7c9e2), so an extra border would read as a fourth block type,
   whereas a tint over the top reads as a state.

   The fade is one-shot on purpose. It draws the eye when the page lands after
   the post-generation reload, then settles to a quiet tint that survives as
   long as the server still calls the report recent -- an indefinite pulse would
   be an irritation on a page someone leaves open.
--------------------------------------------------------------------------- */
tr.report-just-generated > td,
div.report-just-generated {
    background-color: #e3f5e1;
    box-shadow: inset 3px 0 0 0 #5FA752;
    animation: report-just-generated-fade 1.6s ease-out 1;
}

@keyframes report-just-generated-fade {
    0%   { background-color: #b7e6b0; }
    100% { background-color: #e3f5e1; }
}

/* Respect a user who has asked the OS for less motion; the tint alone still
   identifies the row. */
@media (prefers-reduced-motion: reduce) {
    tr.report-just-generated > td,
    div.report-just-generated {
        animation: none;
    }
}

/* ---------------------------------------------------------------------------
   A document that FAILED to generate.

   Counterpart to the "just generated" highlight above, and it stays put rather
   than fading: a success is self-evident because the document appears in its
   row, whereas a failure leaves the row looking exactly as it did before the
   button was pressed. Without a mark that persists, the page says nothing
   happened.

   No animation for the same reason — a flash that decays suits an arrival, not
   a problem still waiting to be dealt with.
--------------------------------------------------------------------------- */
tr.report-generation-failed > td,
div.report-generation-failed {
    background-color: #fdecea;
    box-shadow: inset 3px 0 0 0 #b3261e;
}

/* The reason, beneath the badge. Its own line because it is a sentence, not a
   label — and normal-weight so the badge stays the thing the eye lands on. */
.report-generation-error {
    display: block;
    margin-top: .25em;
    color: #8c1d18;
    font-size: 12px;
    font-weight: normal;
    line-height: 1.35;
    white-space: normal;
}

/* What replaces a Cancel button once it has been pressed ("The report is stopping..."). */
.report-cancel-note {
    color: #495057;
    font-size: 12px;
}

/* The same line for a CANCELLED document: it was asked for, so it reads as information, not an error. */
.report-generation-cancelled {
    display: block;
    margin-top: .25em;
    color: #495057;
    font-size: 12px;
    font-weight: normal;
    line-height: 1.35;
    white-space: normal;
}

/* ---------------------------------------------------------------------------
   Generation badge slots on the entity Documents pages
   ("Queued", "Generating…", "Just generated").

   OWN LINE, BELOW THE LINK. These used to sit inline after the "Request ..."
   anchor with a left margin, which is fine until the anchor is long -- report
   display names are client-defined and some are very long -- and then the badge
   wraps to the next line anyway, but ragged and with no space above it. Forcing
   the break makes every row look the same whatever the name's length, instead
   of only the short ones looking right.

   :empty is what keeps that honest. The client empties these slots whenever a
   document is neither in flight nor freshly finished, and without this an empty
   block would still contribute its top margin to every row on the page.
   :empty beats the rule below on specificity (0,2,0 vs 0,1,0) regardless of
   order here.
--------------------------------------------------------------------------- */
[data-generation-status],
[data-generation-status-key] {
    display: block;
    margin-top: 0.45rem;
}

[data-generation-status]:empty,
[data-generation-status-key]:empty {
    display: none;
}

/* ---------------------------------------------------------------------------
   "… is generating" banner on the entity Documents pages
   (js/RADPI/BackgroundReports.js -> setBanner/hideBanner).

   FULL WIDTH of the page's content column. It used to carry max-width:60em,
   which left it visibly narrower than the request tables it sits above -- on a
   wide screen it read as a floating box rather than as a status bar belonging
   to the list below it.

   The four pages had this as an identical inline style repeated four times;
   it lives here instead so the next tweak is one edit rather than four that
   can drift.

   NOTE: visibility is NOT set here. The client swaps the d-none / d-flex
   utility classes, because Bootstrap's display utilities are !important and
   beat both inline styles and jQuery .show()/.hide(). Setting display in this
   rule would be overridden anyway and would only confuse the next reader.
--------------------------------------------------------------------------- */
.report-generation-banner {
    font-size: 1.05em;
    border-left: 6px solid #0d6efd;
}

/* ---------------------------------------------------------------------------
   Busy state for a report control that is generating -- links AND buttons.

   THE CONTENT IS NEVER TOUCHED, which is the whole design. These controls say
   which format they produce and they say it with an ICON: the anywhere-report
   dialog offers Generate PDF, Excel and Word side by side, and the favourites
   list uses icon-only links in a 20px table cell. Swapping a label for a spinner
   took the icon away at the moment the user wants to confirm what they pressed,
   and on the legacy .form-button controls -- whose icon is a background-image
   rather than content -- it left the icon AND added a spinner beside it.

   So the state is a dim, a pointer-events block (the only double-click guard
   these links have ever had) and a spinner drawn OUT OF FLOW on top. Nothing
   reflows, no width needs pinning, and every icon survives.

   ONLY AN ICON-ONLY CONTROL GETS A SPINNER OF ITS OWN. There is no free space
   inside these buttons for one: centred puts it on top of the words, and
   trailing was measured landing 6px into "Generate Excel", 8px into "View
   Report" and 13px into "Download as PDF" -- which is what the collision in the
   anywhere-report dialog was. Widening to make room would reflow the row, which
   is the thing this design exists to avoid.

   So a LABELLED control just dims and stops responding. That already says "this
   is the one you pressed and it is working", the status panel directly below
   carries the animation and the sentence, and the format icon stays readable --
   which on the dialog's PDF/Excel/Word row is the whole point. An icon-only link
   has no words to collide with and may sit far from the panel, so it keeps the
   centred spinner.

   Reuses Bootstrap's spinner-border keyframes rather than declaring another
   identical rotation.
--------------------------------------------------------------------------- */
.report-generating {
    position: relative;
    pointer-events: none;
    opacity: 0.45;
}

.report-generating::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 1.1em;
    height: 1.1em;
    margin: -0.55em 0 0 -0.55em;
    border: 2px solid currentColor;
    border-right-color: transparent;
    border-radius: 50%;
    animation: spinner-border 0.75s linear infinite;
}

.report-generating.report-generating-labelled::after {
    content: none;
}

/* Slowed, not stopped -- which is what Bootstrap does to .spinner-border itself
   under the same query. The status panel below these links carries a real
   Bootstrap spinner, so stopping this one outright would leave two spinners on
   the same page behaving differently. */
@media (prefers-reduced-motion: reduce) {
    .report-generating::after { animation-duration: 1.5s; }
}

/* ---------------------------------------------------------------------------
   Report toolbars and their status panel: one size, one width.

   MEASURED against the real stylesheets, not guessed. The report pages build
   these rows two ways -- the custom-report row is a <div class="report-toolbar">
   and the download row is a <p class="report-toolbar"> -- and default.css sizes
   <p> at 13px while the surrounding context leaves a <div> at 11.68px. So two
   captions in the same stack ("Select Report:" and "Download Results:") rendered
   at different sizes, along with every link beside them. Sizing the toolbar
   itself settles both shapes at once.

   The status panel takes the same size. It was inheriting the smaller 11.68px,
   which is too quiet for the one element on the page that reports whether the
   thing you just asked for worked.
--------------------------------------------------------------------------- */
.report-toolbar,
#downloadReportStatus,
#customReportStatus,
#backgroundReportStatus,
/* The zip panels are addressed by CLASS: BackgroundReports.js assigns them generated ids
   (zipStatus1, zipStatus2, ...) because eight of the nine Create Zip buttons shipped with the same
   id="Submit" and the panels had no id at all -- so there is no stable id to list here. */
.js-zip-status {
    font-size: 13px;
}

/* ---------------------------------------------------------------------------
   Zip status panel: clear the floated Create Zip button.

   .form-button is `display:block; float:left` (default.css:373-377), and in all nine views that
   offer a zip the Create Zip button is the element immediately before .js-zip-status:
   Asset/Files, Client/File/Manage, Contract/Files, File/Manage, Instrument/Files, Party/Files,
   Project/Files, Tract/Files, Units/Files.

   A block box beside a float does not clear it: the BORDER box still spans the full width, so the
   green/blue background started at the container's left edge as expected, but the LINE boxes are
   shortened to avoid the float -- which pushed "Your report is ready: ..." right by the width of
   the button plus its 5px margin, roughly 160px. That read as the panel's text being indented, or
   centred, against the table above it.

   clear:both puts the panel on its own line under the button, left-aligned with everything else.
--------------------------------------------------------------------------- */
.js-zip-status {
    clear: both;
}

/* ---------------------------------------------------------------------------
   Upload progress bar (wwwroot/js/RADPI/UploadProgress.js, <upload-progress>).

   The bar itself is stock Bootstrap .progress/.progress-bar; what needs saying
   here is the row above it and how the panel sits in the legacy forms.

   WIDTH. It replaces the old "#upload-message" spinner line, which was a plain
   block <div> that filled its container. .form-button beside it is
   `display:block; float:left` (default.css:373-377), exactly as with the zip
   status panels further up this file, so the panel needs clear:both or its line
   boxes shorten to dodge the button and the bar appears indented under it.

   SIZE. font-size is pinned for the same reason the report toolbar is: these
   panels land inside both <p> (13px) and <div> (11.68px) contexts across the
   upload views, and the one element reporting whether a 2 GB transfer is moving
   should not change size depending on which view it turned up in.
--------------------------------------------------------------------------- */
.upload-progress {
    clear: both;
    max-width: 46em;
    margin: 8px 0 12px;
    font-size: 13px;
}

.upload-progress-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 4px 12px;
    margin-bottom: 4px;
}

/* The file name takes the room; the byte counter and Cancel stay to the right.
   Long file names wrap rather than push Cancel off the end. */
.upload-progress-label {
    flex: 1 1 12em;
    min-width: 0;
    overflow-wrap: anywhere;
    font-weight: bold;
    color: #346689;
}

.upload-progress-detail {
    flex: 0 0 auto;
    color: #666;
    font-variant-numeric: tabular-nums; /* the byte counter must not jitter as it counts */
}

.upload-progress-cancel {
    flex: 0 0 auto;
}

.upload-progress .progress {
    height: 18px;
    background-color: #eee;
}

.upload-progress .progress-bar {
    background-color: #8CBA4A; /* the site's green, as used by the Administration accent bar */
    font-size: 11px;
    line-height: 18px;
    transition: width .15s linear;
}

/* A failed upload keeps the panel on screen with the reason in it, so the bar
   behind the message must stop reading as success. */
.upload-progress-failed .upload-progress-label {
    color: #c00;
}

.upload-progress-failed .progress-bar {
    background-color: #d9534f;
}

/* prefers-reduced-motion: the indeterminate "server is still working" state is
   a striped bar in motion. Hold it still for anyone who has asked for that,
   rather than leaving them a permanently animated stripe. */
@media (prefers-reduced-motion: reduce) {
    .upload-progress .progress-bar-animated {
        animation: none;
    }
    .upload-progress .progress-bar {
        transition: none;
    }
}

/* ── TAB STRIPS MUST NOT PILE UP WHEN THEY WRAP ───────────────────────────────────────────────
   Reported on Edit Project in a narrow window: the Main / Brokers / Locations / … strip rendered as
   overlapping boxes with the labels clipped, unreadable and barely clickable.

   default.css:1358 styles `div.content_tabset a` with a border and vertical padding but never gives
   it a display, so it stays INLINE — and an inline box contributes neither its padding nor its
   border to line height. On one line that is invisible. The moment the strip wraps, every row's
   boxes bleed into the rows above and below.

   Measured at 375px on Edit Project's two strips, before this rule:

       display=inline   4 visual rows   3 of 3 row boundaries overlapping   strip height 61px

   61px of strip for four rows of ~20px boxes is the overlap stated as a number.

   inline-block is the whole fix: the box then occupies real line height and the rows sit clear of
   each other. Deliberately NOT applied only under the phone media query — the trigger is WRAPPING,
   not viewport width, and this strip wraps on a desktop too as soon as a project has enough tabs
   (Edit Project alone can render fifteen). vertical-align keeps a single-row strip sitting exactly
   where it does today.

   Global on purpose: ~40 views render a .content_tabset, every one of them can wrap, and every one
   of them has the same defect. */
html div.content_tabset a {
    display: inline-block;
    vertical-align: bottom;
}

/* ── THE LEGACY COLUMNS NEED THE SAME BOX MODEL AS THE GRID ONES ──────────────────────────────
   defaultResponsive.css's phone block gives #column2 (and its siblings) `width: 98%`, written for a
   float layout in a document that is content-box (legacy-theme.css:77). default.css then adds 10px
   of padding-right and 2x1px of borders on top of that 98%, so the border box lands PAST the
   viewport -- measured 1.77px over at 320px, 0.67px at 375px.

   Sub-pixel, and invisible on the page, but it is the entire reason a legacy two-column screen
   reports a horizontal overflow at all once its real offenders are fixed: on the Client Invoice
   screen it is what kept scrollWidth at 376 rather than 375. Worth closing so "does this page
   overflow" stays a question with a clean yes/no answer.

   Identical in kind to the box-sizing already applied to .tl-grid3's columns, and applied here for
   the same reason -- these are the same three ids on the layouts that never moved to a grid. Scoped
   to the phone block because that is where the percentage comes from; above it the columns are sized
   by rules that already account for their chrome. */
@media (max-width: 800px) {
    html div#column1,
    html div#column2,
    html div#column3 {
        box-sizing: border-box;
    }
}
