/* Brand tokens for the application, sharing a vocabulary with the marketing
   site's Web/css/site.css.

   WHY THIS IS A SEPARATE FILE AND NOT AN EDIT TO iob.css. The application is
   heading for a Bootstrap upgrade. Tokens are not coupled to a Bootstrap
   version, so everything in the first block below survives that upgrade
   untouched; only the thin mapping layer at the bottom, which reaches into
   Bootstrap's own class names, would need revisiting. Keeping the two apart
   means the upgrade has one small surface to review instead of a restyle
   tangled through it.

   HOW IT IS ADOPTED. Nothing here applies until a page puts `iob-brand` on
   <body>. That opt-in was how this shipped one page at a time; the whole
   application takes it now. Common/Site.master carries the class, which covers
   the 76 pages that inherit it, and the six standalone pages that load iob.css
   carry it themselves: Login, ForgotPassword, ResetPassword, ErrorPage,
   Unsubscribe, ImpersonateLogin. SignUp was the pilot and already had it.
   FrontDesk was a seventh until the page was deleted on 22 September 2026.

   The opt-in is kept rather than deleted, for two reasons: the twelve report
   PRINT pages deliberately load neither this file nor iob.css and should stay
   that way, and a page that needs to be looked at outside the brand can still
   drop one class rather than unpick a stylesheet.

   THE TOKEN NAMES MATCH Web/css/site.css ON PURPOSE. Same names, same values.
   Two codebases, one palette, and a future change to the brand is an edit in
   two files rather than an archaeology exercise. */

:root {
    /* Identical to the marketing site's values. --brand is the logo cyan. */
    --brand: #00aeef;
    --brand-dark: #0090c7;
    --brand-darker: #00719c;
    --brand-tint: #e6f7fe;

    --navy: #1b232f;
    --navy-deep: #151a26;
    --navy-light: #2d384a;

    /* Bootstrap 5's own $primary and its hover/active shades (shift-color 15%
       and 20%), which is what this application loads: bootstrap.bundle.min.js
       is v5.3.8. Taking the framework's values verbatim rather than mixing a
       blue by hand means the primary action matches every Bootstrap control on
       the page that this layer has not skinned. */
    --blue: #0d6efd;
    --blue-dark: #0b5ed7;
    --blue-darker: #0a58ca;
    --blue-darkest: #084298;

    /* Inter first, then the same fallback stack Bootstrap 4 already used. Both
       faces are self-hosted now - see the @font-face block below - so the
       fallbacks are what shows for the moment before the file arrives, and on
       the characters outside the latin subset. */
    --font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;

    /* The handwritten display face, same stack as site.css. */
    --font-display: "Caveat", "Segoe Print", "Comic Sans MS", cursive;

    --radius-sm: 8px;
    --radius: 12px;
    --radius-lg: 16px;

    /* Neutrals and surfaces, same names and values as site.css. --gray-300 is
       the input border the contact form uses; --gray-200 is the hairline round
       its cards.

       KEEP THIS RAMP IN STEP WITH THE RULES BELOW. A var() that names a token
       nobody defined does not fall back and does not warn -- the whole
       declaration is dropped at computed-value time, and a shorthand takes its
       siblings with it. That is not hypothetical: --gray-200 and --gray-600
       were used by the card and label rules before they were added here, and
       the symptom was cards rendering with no border at all rather than
       anything that looked like a mistake. */
    --white: #ffffff;
    --text: #000000;
    --gray-700: #344054;
    --gray-600: #475467;
    --gray-500: #667085;
    --gray-300: #d0d5dd;
    --gray-200: #eaecf0;
    --gray-100: #f2f4f7;

    /* The tint the marketing site's .section--tint bands use. */
    --gray-50: #f9fafb;

    /* The focus indicator is deliberately NOT --brand. site.css records why:
       "#00aeef is 2.53:1 on white and 2.42:1 on the --gray-50 tint", which is
       why the marketing site moved its ring off brand while leaving buttons on
       it. The same reasoning applies here, so the ring is navy from the start. */
    --focus-ring: var(--navy);

    /* ----------------------------------------------------------------------
       PURPOSE TOKENS. Everything above is a primitive -- a colour with a name
       from the marketing site. Everything below says what a primitive is FOR,
       and it is what the rules in this file should reference.

       The distinction is not bookkeeping. A rule that says
       "background: var(--gray-50)" has to be re-read and re-reasoned every time
       the palette moves; one that says "background: var(--surface-page)" does
       not. It is also the precondition for a dark mode: Bootstrap 5.3 ships
       data-bs-theme, and a purpose layer is the difference between re-pointing
       twenty tokens and auditing four hundred declarations. Dark mode is NOT in
       scope here -- it is a second design and a second accessibility pass -- but
       naming things this way now is what keeps it a week rather than a rewrite.
       ---------------------------------------------------------------------- */

    /* THE PAGE IS DARKER THAN THE GREY RAMP GOES, AND THAT IS THE POINT.
       --gray-50 #f9fafb was 1.045 against a white card: a step you had to be
       told was there. 386 cards, a white sidebar and a white card header all
       sat on it, so the application read as one flat sheet with hairlines
       drawn on it rather than as cards resting on a page.

       #eef1f5 is 1.133 against the card. It is a literal rather than a
       primitive on purpose: every colour in the ramp above is shared by name
       AND value with the marketing site, and this one is not on that site --
       adding it as --gray-150 would put a value in the shared vocabulary that
       only one of the two codebases has, which is the thing the primitive
       block exists to prevent. If the marketing site ever wants this tint,
       promote it then, in both files.

       What it is measured against: --text-body 9.23:1, --link 5.68:1, and the
       card's --border-hairline at 1.044, so the border still draws rather than
       dissolving into the page the way it would at --gray-200. Nothing in the
       application puts --text-muted on this surface; if something ever does,
       that pairing is 4.39:1 and fails.

       --surface-sunken stays --gray-100, which is now LIGHTER than the page.
       That is not a mistake: every sunken surface is a hover or active state
       on a white component -- the sidebar, the mail folders, a message row, a
       button -- so it is judged against white and never against the page. */
    --surface-page: #eef1f5;
    --surface-card: var(--white);
    --surface-sunken: var(--gray-100);
    --surface-nav: var(--white);

    --border-hairline: var(--gray-200);

    /* A CONTROL'S BORDER IS NOT DECORATION, AND 1.4.11 IS A DIFFERENT
       CRITERION FROM 1.4.3.

       This was --gray-300, which is 1.47:1 on a card and 1.30:1 on the page.
       For a .btn-light or a .form-control the border is the ONLY thing that
       says where the control is -- both fill with --surface-card, the same
       white the card behind them is -- so it is "visual information required
       to identify a user interface component" and 1.4.11 asks for 3:1. The
       September audit closed 1.4.3 on these values and 1.4.3 is about TEXT;
       nothing in it looks at a boundary, which is how a border a third of the
       way to the threshold survived it.

       --gray-500 is 4.97:1 on a card, 4.51:1 on a hover surface and 4.39:1 on
       the page. The page matters: three controls on the status board sit
       directly on --surface-page rather than in a card, so the token has to
       clear 3:1 against the DARKEST surface a control can land on, not just
       against white. An interpolated grey at #7d8b9d clears it at 3.06:1 and
       was rejected -- no headroom, and it invents a rung this palette does
       not have.

       Every hover and active border moved down to --gray-600 to stay apart
       from the new resting state. That gap is narrower than it was, 1.55:1
       against the old 3.37:1, and it is not carrying hover on its own: those
       states change the background to --surface-sunken as well. Hover is also
       not what 1.4.11 is asking about. */
    --border-control: var(--gray-500);

    --text-strong: var(--text);
    --text-body: var(--gray-700);
    --text-muted: var(--gray-500);

    /* THE ACTION COLOUR IS BOOTSTRAP BLUE, AND THE BRAND CYAN IS AN ACCENT.
       White on --blue is 4.54:1, on --blue-dark 5.84:1, on --blue-darker
       6.47:1. The cyan cannot carry white text at all (2.53:1), which is the
       constraint that has shaped every colour decision in this product -- so
       rather than keep hunting for a cyan dark enough to survive, the primary
       action is the framework blue and the cyan does the job it is good at:
       the active marker in the navigation, the selected state, the accent on a
       focused control. One loud colour per screen.

       THE HEADROOM HERE IS THIN. 4.54:1 clears 1.4.3's 4.5:1 by 0.04, where
       the navy this replaced had 15.81:1. Anything that lightens --blue -- a
       tint, an opacity, a gradient, a disabled state that keeps white text --
       drops it under the floor. Check the figure, not the eye, before touching
       this token. */
    --action-primary: var(--blue);
    --action-primary-hover: var(--blue-dark);
    --action-primary-active: var(--blue-darker);

    /* LINKS ARE THE SAME BLUE AS THE BUTTON, ONE SHADE DOWN, AND THE SHADE IS
       NOT A MATTER OF TASTE. A link is body text: it sits on the page tint and
       on sunken surfaces as often as it sits on a white card, and it is the
       most-repeated colour in the product -- every name on the status board is
       one. Measured on all three: --blue is 4.50 / 4.31 / 4.08, so the exact
       Bootstrap link colour passes only on a white card and only by rounding.
       --blue-darker is 6.44 / 6.16 / 5.84 and passes everywhere with room.

       That is why the fill and the ink differ. A button brings its own
       background, so --blue is judged against the white sitting on it; a link
       is judged against whatever is behind it, which is the surface with the
       least contrast to spare. Same family, same hue, one step apart. */
    --link: var(--blue-darker);
    --link-hover: var(--blue-darkest);

    --accent: var(--brand);
    --accent-ink: var(--brand-darker);

    /* The one place cyan is allowed to be a fill: behind --accent-ink, never
       behind white. #00719c on #e6f7fe is 4.97:1, which clears 1.4.3 for the
       small text that uses it. */
    --accent-wash: var(--brand-tint);

    /* Type. The scale is small on purpose: this is a board somebody checks
       fifty times a day, not a landing page. Body stays at Bootstrap's 1rem --
       shrinking the base is the fastest way to make an application feel cramped
       and to annoy anyone who already zooms -- and the smaller steps are for
       dense contexts only: table cells, meta lines, chips. */
    --font-size-title: 1.25rem;
    --font-size-section: 1rem;
    --font-size-dense: 0.875rem;
    --font-size-meta: 0.8125rem;

    /* A 4px grid. Named by step rather than by pixel so the rhythm can be
       tuned in one place. */
    --space-1: 4px;
    --space-2: 8px;
    --space-3: 12px;
    --space-4: 16px;
    --space-5: 20px;
    --space-6: 24px;
    --space-7: 32px;
    --space-8: 40px;

    /* Surfaces get ONE hairline and no resting shadow. A shadow now means the
       thing genuinely floats -- a menu, a dialog -- which is the whole reason
       an elevation is worth having. Cards carrying both a border and a shadow
       is most of why the old surfaces read as dated. */
    --radius-control: 8px;
    --radius-card: 10px;
    --shadow-overlay: 0 8px 24px rgba(16, 24, 40, .12), 0 2px 6px rgba(16, 24, 40, .08);

    /* MEASURED, NOT CHOSEN. Lisa read the rail and the content padding as too
       much space, 21 September 2026, and the call was to keep the content
       padding and take it out of the rail instead.

       The floor is 153px: the widest label, "Status Board", needs about 137px of
       line and .iob-nav adds 8px of padding each side plus the 1px border.
       Below that it wraps, and 148px is where it actually does. The logo
       wants 140px -- 108px of mark inside 16px of brand padding -- so the
       label is what binds, not the mark.

       176px leaves 23px past the longest label, which is the room for a
       label longer than "Status Board" to appear later without this
       becoming a two-line menu. Do not take it to the floor.

       Desktop only. Below lg the rail is an offcanvas panel with its own
       280px, further down this file, and that one overlays the content
       rather than taking width from it, so it is not part of this. */
    --sidebar-width: 176px;

    /* Icons only, no labels. 64px is the floor an icon row actually needs: the
       rail keeps its 8px side padding and a nav link centers a single ~24px
       icon inside it, same as the expanded row's leading icon box. Desktop
       only, same as --sidebar-width above -- below lg the rail is an
       offcanvas panel you opened on purpose, and collapsing it to icons
       would defeat opening it. */
    --sidebar-width-collapsed: 64px;
    --focus-ring-inner: #ffffff;
}

/* ---------------------------------------------------------------------------
   DARK MODE. Only the PURPOSE layer gets a second set of values here -- the
   primitives above (the brand palette, the grey ramp, the Bootstrap blues)
   stay exactly as they are and are reused BY the tokens below, the same way
   Bootstrap's own [data-bs-theme=dark] reuses its base palette rather than
   inventing a parallel one. That reuse is also why --navy/--navy-deep/
   --navy-light exist as primitives already: they were the sidebar's own
   colour before it went light, so putting them back here for the page and
   card surfaces is not a new idea, just an old one with a second use.

   app.min.css is an unmodified Bootstrap 5.3 build, which already ships a
   complete [data-bs-theme=dark] variable set of its own -- every unskinned
   Bootstrap control (buttons, form controls, modals, dropdowns) re-themes
   from that the moment the attribute is set, with nothing to do here. This
   block only has to cover what THIS file draws on top of Bootstrap: cards,
   the sidebar, borders, body text and links.

   NOT AUDITED THE WAY THE LIGHT VALUES ABOVE WERE. Those went through a
   measured contrast pass against specific WCAG criteria; these did not --
   this is a first pass, chosen to read correctly rather than measured. A
   proper audit is follow-up work, not a blocker for shipping a toggle. */
:root[data-bs-theme="dark"] {
    --surface-page: var(--navy-deep);
    --surface-card: var(--navy);
    --surface-sunken: var(--navy-light);
    --surface-nav: var(--navy);

    /* A hairline drawn in the grey ramp reads as a light STROKE on a dark
       surface, the opposite of what it does on a light one -- rgba white is
       what keeps it a subtle seam instead of a bright line. */
    --border-hairline: rgba(255, 255, 255, .12);
    --border-control: rgba(255, 255, 255, .32);

    --text-strong: var(--gray-100);
    --text-body: var(--gray-300);

    /* Not --gray-500: that grey is tuned to sit on white (4.97:1) and on the
       light page tint, and it goes muddy rather than muted against navy. A
       lighter, page-specific value the way --surface-page above is one. */
    --text-muted: #98a2b3;

    /* --link was --blue-darker, tuned for a light background (6.44:1 on a
       white card) -- a dark blue on a dark card is close to unreadable.
       Bootstrap's own [data-bs-theme=dark] link colour, so a themed link and
       an unskinned Bootstrap one still match. */
    --link: #6ea8fe;
    --link-hover: #8bb9fe;

    /* --focus-ring was navy, chosen because navy carries on a light page.
       On a dark one it would sit almost on top of --surface-page instead of
       outlining it, so the ring becomes the brand cyan instead -- saturated
       enough to read against every dark surface in this token set. The
       white inner ring is untouched: its job is separating the ring from
       whatever is directly behind the control, which does not depend on
       page theme. */
    --focus-ring: var(--brand);

    --shadow-overlay: 0 8px 24px rgba(0, 0, 0, .36), 0 2px 6px rgba(0, 0, 0, .24);

    /* --accent-wash was --brand-tint, #e6f7fe -- a near-white fill chosen to
       sit under --accent-ink (#00719c), a dark ink that goes invisible on
       its own dark-mode background. Left unset here it fell through to that
       light value everywhere, including the calendar's "today" cell, which
       is filled with ordinary --text-body/--text-muted rather than
       --accent-ink: light-on-light where the rest of the page is dark.

       A translucent cyan over the card colour keeps the highlight readable
       -- the existing light text tokens stay legible on it because it is
       still close to --surface-card, not a swap to a light panel. --accent-ink
       moves to the same light blue --link uses, for the one place (the badge
       at .iob-sg__swatch--wash, line ~1804) that pairs ink with wash directly. */
    --accent-wash: rgba(0, 174, 239, .18);
    --accent-ink: #6ea8fe;
}

/* ---------------------------------------------------------------------------
   THE TYPEFACES, SELF-HOSTED.

   The marketing site pulled Inter and Caveat from Google Fonts on every page
   until Oct 2026; it now self-hosts these same two files in Web/fonts/, so
   keep the copies identical. This application is behind a login, so the
   Google arrangement would have added a third-party request to every
   authenticated page view, on a product sold to organisations that often run
   locked-down networks. Two files answer that:
   the variable Latin subsets, 48KB and 74KB, covering every weight.

   Both are the files @fontsource-variable publishes (v5.2.5), which are the
   upstream Google Fonts builds; both are SIL Open Font License 1.1 and the
   licences travel with them in App/fonts/. The family names here are plain
   "Inter" and "Caveat" rather than fontsource's "Inter Variable", so
   --font-sans and --font-display keep the same spelling as Web/css/site.css.

   No italic face is shipped, which matches what the marketing site requests.
   Italic text synthesises an oblique; there is very little of it.

   .woff2 already has a MIME map in web.config -- see the comment there, which
   records that IIS 404s extensions it does not know and that this exact trap
   has been hit once already.
   --------------------------------------------------------------------------- */

@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-display: swap;
    font-weight: 100 900;
    src: url('../fonts/inter-latin-wght-normal.woff2') format('woff2-variations');
    unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}

@font-face {
    font-family: 'Caveat';
    font-style: normal;
    font-display: swap;
    font-weight: 400 700;
    src: url('../fonts/caveat-latin-wght-normal.woff2') format('woff2-variations');
    unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}

/* ---------------------------------------------------------------------------
   Mapping layer: tokens onto the classes this application already uses.
   Scoped to .iob-brand so it is opt-in. This is the part a Bootstrap upgrade
   would touch.

   ON !important IN THIS FILE. There is exactly one, on body.iob-brand, and its
   reason is given where it sits. The four that used to be here -- two on the
   navbar, two on .btn-primary -- existed only because iob.css marked those same
   properties !important, and an !important declaration can only be beaten by
   another one however specific the selector. They are gone: the reconciliation
   pass deleted the iob.css rules they were fighting, so every rule below now
   wins on specificity alone. Keep it that way.
   --------------------------------------------------------------------------- */

.iob-brand {
    font-family: var(--font-sans);
}

/* ON THE BUTTON COLOURS, WHICH THIS FILE HAS CHANGED ITS MIND ABOUT ONCE.

   An earlier version of this layer painted .btn-blue with --brand-darker, then
   stopped, because the sign-up page was the only adopter and the application's
   own #1976d2 (4.60:1) was preferred. Now that every page adopts the layer,
   .btn-primary -- 107 of them, against 14 .btn-blue -- is the button that says
   what colour this product is, and both families sit on --action-primary so
   that the two never drift apart again. The rule and the figures are under
   "Buttons" in the application skin below.

   IF YOU ARE TEMPTED TO "FINISH THE JOB" BY POINTING EITHER AT --brand, DO NOT.
   White on #00aeef is 2.53:1 and fails at both the normal and the large-text
   threshold. The marketing site carries that knowingly - site.css records the
   figure, and a source comment on Web/Pages/Accessibility.aspx notes the ACR's
   contrast method covered headings, body copy, list items and links but not
   buttons. The application cannot inherit it: AccessibilityBacklog closed
   1.4.3 on "every colour surface in App/ now meets 4.5:1". The brand cyan
   belongs in the logo and the marker swipe, not under white text. */

/* Inputs sized to match the marketing site's contact form, which is the page a
   visitor most often sees just before this one. Measured there: 52.8px tall,
   17px type, 12px radius. Bootstrap's default here is about 38px with a 4px
   radius, and that jump is the thing you notice on the hop from /contact-us/.
   The declarations below are lifted from site.css's own rule rather than
   reverse-engineered from computed values, so the two cannot drift apart
   silently.

   height:auto is needed because Bootstrap gives a select a fixed height that
   the padding would otherwise fight.

   SCOPED TO .signup-form, AND THAT SCOPE IS LOAD-BEARING. When this layer was
   written the sign-up page was its only adopter, so the selector could be bare
   .form-control. The whole application adopts it now, and 53px fields are the
   right answer on a two-field marketing form and the wrong one on a status
   board filter row or a 40-field user editor. The application's own field
   treatment is further down: Bootstrap's density, this file's border, radius
   and focus ring.

   .form-select IS LISTED HERE BECAUSE OF A DEFECT THE BOOTSTRAP 5 SWAP LEFT
   BEHIND. This rule used to say "select.form-control, .custom-select", which
   are Bootstrap 4 spellings; the swap rewrote every select in the application
   to .form-select and nothing re-pointed this rule at it. Measured on the live
   sign-up page before this change: the text inputs were 53px tall with a 12px
   radius and the three dropdowns beside them 38px with a 4px radius. The
   min-height below was written to stop exactly that, and had quietly stopped
   applying to the controls it was written for.

   The focus treatment is copied with its reasoning intact: the ring is
   --brand-darker, not --brand, because in site.css's words "cyan at 2.53:1 on
   the white field was too faint" to show where you landed. Same conclusion the
   button above reached independently. Bootstrap's own glow is turned off so
   the two do not stack. */
.iob-brand .signup-form .form-control,
.iob-brand .signup-form .form-select {
    height: auto;
    padding: 12px 14px;
    font-size: 1.0625rem;
    line-height: 1.6;
    color: var(--text);
    background-color: var(--white);
    border: 1px solid var(--border-control);
    border-radius: var(--radius);

    /* On BOTH, not just the select. A <select> builds a shorter line box than
       an <input> from identical font-size and line-height - 46.4px against
       52.8px measured here - so the three dropdowns would sit visibly shorter
       than the three text fields beside them. Declaring one min-height for
       both makes them equal by construction rather than by two numbers that
       have to be kept in step. */
    min-height: calc(1.6em + 24px + 2px);
}

    .iob-brand .signup-form .form-control:focus,
    .iob-brand .signup-form .form-select:focus {
        outline: 2px solid var(--brand-darker);
        outline-offset: 1px;
        border-color: var(--brand);
        box-shadow: none;
    }

/* ---------------------------------------------------------------------------
   PAGE FRAME: a header band, then the two-column body /contact-us/ uses -
   rebuilt on the classes this application already has rather than imported.
   --------------------------------------------------------------------------- */

/* The tint is on the page rather than on a band sized to the form, so it
   reaches the bottom of the viewport however short the form is; the header
   paints white over the top of it. body.iob-brand needs !important because
   iob.css sets the page colour with one, and an !important declaration beats a
   more specific rule that lacks it - with both marked, the more specific
   selector wins, which is this one.

   This names --surface-page rather than the primitive it used to name. It was
   the only rule painting the application's page colour and it reached past the
   purpose token to --gray-50 directly, so --surface-page was declared, read as
   the page colour by anyone scanning the token block, and used by nothing. */
body.iob-brand {
    background-color: var(--surface-page) !important;
}

.iob-brand .signup-header {
    background-color: var(--white);
}

/* Bootstrap's .container is 1140px with 15px gutters; the marketing site's is
   1200px with 24px. Matched on the two wrappers this page uses rather than by
   redefining .container, which would move every page that adopts this layer
   later. */
.iob-brand .signup-header__inner,
.iob-brand .signup-main > .container {
    max-width: 1200px;
    padding-right: 24px;
    padding-left: 24px;
}

.iob-brand .signup-header__inner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 32px;
    min-height: 72px;
}

.iob-brand .signup-logo {
    flex: none;
    display: flex;
    align-items: center;
}

    .iob-brand .signup-logo img {
        display: block;
        width: 190px;
        height: auto;
    }

/* Built from tokens rather than given Bootstrap's .btn. iob.css has already
   reshaped .btn for the application - an 18px bottom margin among other things
   - which would push this off centre in a flex row. */
.iob-brand .signup-header__login {
    flex: none;
    display: inline-block;
    padding: 10px 20px;
    font-size: 0.9375rem;
    font-weight: 600;
    line-height: 1.5;
    color: var(--text);
    background-color: var(--white);
    border: 1px solid var(--border-control);
    border-radius: var(--radius-sm);
}

    .iob-brand .signup-header__login:hover,
    .iob-brand .signup-header__login:focus {
        color: var(--text);
        background-color: var(--gray-50);
        text-decoration: none;
    }

/* The header row is 24 + 190 + 32 + 86 + 24 = 356px, so below that the Log in
   button ran off the right edge and the whole page scrolled sideways (12px at
   320px, measured 26 September 2026). Tighter gutters and a smaller mark bring
   it to 284px. Both wrappers, so the logo still lines up with the form. */
@media (max-width: 379.98px) {
    .iob-brand .signup-header__inner,
    .iob-brand .signup-main > .container {
        padding-right: 16px;
        padding-left: 16px;
    }

    .iob-brand .signup-header__inner {
        gap: 16px;
    }

    .iob-brand .signup-logo img {
        width: 150px;
    }
}

/* The same vertical rhythm as the marketing site's opening section: pulled up
   toward the header, full section padding underneath. */
.iob-brand .signup-main {
    padding: clamp(32px, 4vw, 56px) 0 clamp(56px, 8vw, 96px);
}

/* Two equal columns collapsing to one at the same 860px /contact-us/ uses.
   minmax(0, 1fr) rather than 1fr so a long unbroken string in either column
   cannot push the grid wider than its container. No order property: the form
   is first in the DOM, so it lands left on desktop and on top on a phone. */
.iob-brand .signup-split {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: clamp(28px, 4vw, 60px);
    align-items: start;
}

@media (max-width: 860px) {
    .iob-brand .signup-split {
        grid-template-columns: 1fr;
    }
}

.iob-brand .signup-form__title {
    margin: 0 0 6px;
    font-size: clamp(1.875rem, 4vw, 2.5rem);
    font-weight: 700;
    line-height: 1.2;
    color: var(--text);
}

/* THE GAP BELOW THE TITLE IS SET BY THE FORM, NOT BY THE TITLE. Adjacent
   siblings collapse to the larger of the two margins, so the 20px here is what
   shows under a bare title, and anything taller slotted between them - a
   subtitle, an alert - sets its own gap without this rule having to know.
   Both steps run bare today; the title keeps a small margin of its own so that
   stays true if one gains a line again. */
.iob-brand .signup-form {
    margin-top: 20px;
}

/* THE DISPLAY COLUMN. Script faces read smaller than sans at the same size,
   which is why this scale runs larger than the surrounding headings - the same
   reasoning, and the same clamp, as .section-head h1 in site.css. */
.iob-brand .signup-split__intro h2 {
    margin: 0 0 1rem;
    font-family: var(--font-display);
    font-size: clamp(1.75rem, 6.1vw, 4rem);
    font-weight: 700;
    line-height: 1.15;
    letter-spacing: 0;
    color: var(--text);
    text-wrap: balance;
}

.iob-brand .signup-split__intro p {
    margin: 0;
    font-size: calc(1.275rem + 0.3vw);
    line-height: 1.6;
    color: var(--text);
}

/* The marker swipe, lifted from site.css so both sites draw the same ink:
   three stacked semi-transparent passes in one SVG, so the overlaps darken the
   way real marker does. box-decoration-break keeps the band intact when the
   phrase wraps onto a second line. */
.iob-brand .marker-highlight {
    padding: .02em .22em .06em;
    margin-inline: -.22em;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 100' preserveAspectRatio='none'%3E%3Cg opacity='.75' fill='%23fbb945' transform='rotate(-3 150 50)'%3E%3Cpath d='M52 20 C130 17 230 16 280 19 C294 20 294 43 280 44 C210 47 130 48 52 46 C38 45 38 21 52 20 Z'/%3E%3Cpath d='M20 34 C110 31 240 30 284 33 C298 34 298 57 284 58 C230 61 110 62 20 60 C6 59 6 35 20 34 Z'/%3E%3Cpath d='M30 54 C100 51 210 50 268 53 C282 54 282 77 268 78 C200 81 100 82 30 80 C16 79 16 55 30 54 Z'/%3E%3C/g%3E%3C/svg%3E");
    background-size: 100% 74%;
    background-position: 0 46%;
    background-repeat: no-repeat;
    box-decoration-break: clone;
    -webkit-box-decoration-break: clone;
}

/* Step 2's button row. Back keeps its natural width; the primary grows into
   what is left. flex-wrap lets the pair stack rather than squeeze on a narrow
   screen, and the primary keeps its full width when it does. */
.iob-brand .signup-form__actions {
    display: flex;
    flex-wrap: wrap;
}

    .iob-brand .signup-form__actions .btn-blue {
        flex: 1 1 14rem;
    }

/* The step counter closing each form. */
.iob-brand .signup-form__step {
    margin: 18px 0 0;
    font-size: 0.9375rem;
    color: var(--gray-500);
}

/* FIELD RHYTHM: the contact form's `.form__row { margin-bottom: 18px }` said in
   Bootstrap's grid, so the gap between one field and the next label reads the
   same 18px on both pages.

   The margin goes on the columns rather than the rows. First and Last name are
   two col-sm-6 inside one row; a margin on the row would separate that pair from
   what follows but leave the two of them flush against each other the moment
   they stack below 576px - which is the one place the gap matters most. On the
   columns it comes out right in both directions with no media query.

   Scoped to .signup-form so it reaches only the field rows, not the header or
   the display column, which set their own spacing.

   Rows that already carry an mb-3 keep it and so land on a wider gap. That is
   intended rather than an oversight: those are the breaks before the buttons,
   and a break between sections should read as larger than the gap between two
   fields. The button rows are the exception - their mb-3 came off when this
   went in, because iob.css already gives .btn an 18px bottom margin of its own
   and those rows were stacking three spacings where one was wanted.

   The final row of each step is zeroed, so the step counter that follows it
   sets the whole gap with its own top margin instead of adding to one. It is
   :last-of-type rather than :last-child because the counter, a <p>, is the
   real last child - :last-child would quietly match nothing. */
.iob-brand .signup-form .row > [class*="col-"] {
    margin-bottom: 18px;
}

.iob-brand .signup-form .row:last-of-type > [class*="col-"] {
    margin-bottom: 0;
}

/* Field labels match the contact form - 16px at 600 - instead of the app's
   .dataLabel at 80%, which is 12.8px and read as a thin caption beside a 53px
   field. The class is removed from this page's labels rather than redefined,
   because .dataLabel is used across seventeen other pages that have not
   adopted this layer and should not shift underneath it. */
/* The "All fields are required" line: quiet, but not as quiet as an error. */
.iob-brand .form-required-note {
    margin: 0 0 1.5rem;
    color: var(--gray-500);
    font-size: 0.9375rem;
}

/* ASP.NET validators render as CssClass="text-danger small" on this page.
   Bootstrap's .small is 80%, which is 13.6px beside a 17px field - the same
   thinness the labels had before. Sized up here rather than by editing the
   validators: there are sixty-four of them across seventeen pages and only this
   one has adopted the layer, so changing them at source would move pages that
   have not been looked at.

   The colour is deliberately untouched, and it is worth being precise about
   which red it is. iob.css sets .text-danger to #c62828 - NOT Bootstrap's
   #dc3545, which this product uses as a danger *background* and which iob.css
   warns is not interchangeable with it.

   That distinction matters here because this layer changed the surface these
   messages sit on, from the old #e9ebee page to the #f9fafb tint. Re-checked
   against the new background: #c62828 is 5.38:1 and passes. #dc3545 would be
   4.33:1 and fail - so "correcting" this to Bootstrap's red, which looks like
   the obvious tidy-up, would break 1.4.3 on every validation message on the
   page.

   Both selectors are listed so the rule beats .small on specificity rather
   than relying on source order. */
.iob-brand .signup-form .text-danger,
.iob-brand .signup-form .text-danger.small {
    display: block;
    margin-top: 6px;
    font-size: 0.9375rem;
}

.iob-brand .signup-form label {
    display: block;
    margin-bottom: 6px;
    font-size: 1rem;
    font-weight: 600;
    color: var(--text);
}

/* Keyboard focus, on the navy ring rather than brand - see the token note. The
   inner white line keeps the ring visible on navy and dark surfaces too.

   3px and 2px rather than the 2px and 1px this started with, so the indicator
   is the one the marketing site draws. site.css: "The box-shadow spread matches
   outline-offset exactly, so the gap is filled and the two bands read as one
   ring." One of the two bands always clears 3:1 against whatever sits behind,
   which a single band cannot do across this palette (WCAG 2.2 SC 1.4.11). */
.iob-brand :focus-visible {
    outline: 3px solid var(--focus-ring);
    outline-offset: 2px;
    box-shadow: 0 0 0 2px var(--focus-ring-inner);
}

/* Except the page's <main>. It takes focus only from a script or the skip link
   (tabindex="-1", never in the Tab order), as a place to continue from, and it
   is not a control, so a ring round it says nothing. It drew one anyway: the
   browser counts a script's focus() as keyboard focus when the element before
   it had a keyboard ring. The welcome guide's buttons do, so closing the guide
   left a navy line down the edge of the content beside the rail (Chris,
   9 Oct 2026). */
.iob-brand #main-content:focus-visible {
    outline: none;
    box-shadow: none;
}

/* ===========================================================================
   THE APPLICATION SKIN
   ===========================================================================

   Everything above this line predates the whole-application adoption: tokens,
   and the sign-up page that piloted them. Everything below dresses the
   components the application is actually built from -- measured, not guessed:
   386 cards, 196 form-controls and 195 form-selects, 107 btn-primary and 77
   btn-light, 32 modals, one navbar, four paginations.

   WHAT THIS LAYER DELIBERATELY DOES NOT DO. It does not touch the button and
   background colour variants in iob.css. Those carry contrast ratios in their
   comments, WCAG 1.4.3 was closed on those exact values, and re-deriving them
   here would be the single easiest way to reopen it. This layer changes the
   SHAPE of things -- type, radius, border, shadow, density, case -- and changes
   colour in three places: the navbar, .btn-primary, and links, each checked
   below.

   Two more colours did change, but in iob.css rather than here, because they
   were failing 1.4.3 before this work started and the fix belongs with the
   values it corrects: .grey-text and .red-text. Both are text colours that
   declare no background of their own, which is the blind spot that let them
   through the September audit -- that audit read CSS rules, and a rule with no
   background in it has nothing to pair. The link colour below was the third.
   =========================================================================== */

/* --- Type -----------------------------------------------------------------

   The face changes; the scale does not. The marketing site sets body copy at
   1.25rem, which is right for a landing page read once and wrong for a board
   read fifty times a day, so the application keeps Bootstrap's 1rem. What
   closes the seam is Inter itself, and headings that match the marketing
   site's weight and tracking rather than iob.css's font-weight: 300, which is
   the thin, wide look the rest of this work is retiring. */
.iob-brand h1,
.iob-brand h2,
.iob-brand h3,
.iob-brand h4,
.iob-brand h5,
.iob-brand h6 {
    font-weight: 700;
    line-height: 1.2;
    letter-spacing: -0.02em;
}

/* LINKS ARE BLUE AGAIN, BUT NOT THE BLUE THAT WAS TAKEN OUT. iob.css sets
   a { color: #007bff }, which is 3.98 on a white card, 3.81 on the page tint
   and 3.61 on a sunken surface: three failures of WCAG 1.4.3's 4.5:1, on the
   most-repeated colour in the product. That is what moved links onto the
   brand cyan --brand-darker, which passed at 5.47 / 5.23 / 4.96.

   They are blue once more because the primary action is, and a link and a
   button pointing at two different hues made the product look like it had two
   minds about what "clickable" is. The colour is --link, which is
   --blue-darker #0a58ca -- one shade down from the button's --blue, for the
   reason set out at the token.

   #0d6efd IS THE VALUE TO RESIST HERE, AND IT IS THE OBVIOUS ONE, because it
   is what Bootstrap itself sets $link-color to and what .btn-primary already
   uses on this page. On a white card it is 4.50:1 -- a pass that survives no
   rounding at all -- and on the page tint and sunken surfaces it is 4.31 and
   4.08, which are not passes. Buttons can carry it because a button brings its
   own background; a link cannot, because it does not.

   Hover goes darker, never lighter: --blue-darkest #084298 is Bootstrap's own
   $blue-700, at 9.36 / 8.96 / 8.50. */
.iob-brand a {
    color: var(--link);
}

    .iob-brand a:hover,
    .iob-brand a:focus {
        color: var(--link-hover);
    }

/* The sort links and the status board's own controls inherit their colour on
   purpose -- see iob.css. Left as they are. */
.iob-brand .sort-link,
.iob-brand .sort-link:hover,
.iob-brand .sort-link:focus {
    color: inherit;
}

/* --- The shell ------------------------------------------------------------

   A light sidebar rather than a dark bar. The bar was #4267b2, then --navy, and
   the second was better than the first but still the largest block of colour on
   every page in service of six links. A light rail spends that weight on the
   content instead, and it has room for the section menus that currently need a
   pattern of their own.

   The sidebar markup is .offcanvas-lg, so Bootstrap makes it a static column at
   lg and up and a sliding panel below -- no duplicate markup, no JS of ours,
   and the same mechanism the Setup and Reports menus already use. */
.iob-brand .iob-shell {
    display: flex;
    min-height: 100vh;
    align-items: stretch;
}

.iob-brand .iob-sidebar {
    flex: 0 0 var(--sidebar-width);
    width: var(--sidebar-width);
    background-color: var(--surface-nav);
    border-right: 1px solid var(--border-hairline);
    display: flex;
    flex-direction: column;
    padding: var(--space-3) 0;
}

/* THE RAIL STAYS ON SCREEN. As a plain flex child the sidebar stretched to the
   height of the page, which put the profile and settings links at the bottom of
   a 3,000px board rather than at the bottom of the window -- they scrolled away
   and, on the longest pages, were never seen at all.

   Sticky rather than fixed: fixed would take the rail out of flow and the
   content column would slide under it, which then needs a matching margin and
   two numbers that have to be kept in step. Sticky keeps the layout honest and
   the flex row does the spacing.

   Only at lg and up. Below that the rail is an offcanvas panel, which Bootstrap
   already positions for us. */
@media (min-width: 992px) {
    .iob-brand .iob-sidebar {
        position: sticky;
        top: 0;
        height: 100vh;

        /* THE RAIL HAS TO OUTRANK THE CONTENT, and it is `position: sticky`
           above that makes this necessary. A sticky element creates a stacking
           context, so the account menu's own z-index: 1000 is sealed inside the
           rail and counts for nothing outside it. The rail's context then sorts
           against its siblings at z-index: auto -- by document order -- and
           .iob-main comes after it. Every Bootstrap .card is position: relative,
           so the dashboard cards painted straight over the open menu.

           1020 is Bootstrap's own tier for a sticky element ($zindex-sticky).
           Above the content, and below the offcanvas (1045) and the modal
           (1055), both of which still have to cover the rail.

           Inside this media query on purpose. Below lg the rail IS the
           offcanvas and Bootstrap gives it 1045; .iob-brand .iob-sidebar would
           beat .offcanvas on specificity and drop the open panel behind its own
           backdrop at 1040. */
        z-index: 1020;
    }
}

/* THE COLLAPSED RAIL. State is a class on <body>, not on .iob-sidebar itself --
   see SidebarCollapseToggle's script near the top of this file and the inline
   script right after <body> in Site.master that reads it back from
   localStorage before the rail exists. Desktop only, same reasoning as the
   sticky rail above: below lg the rail is an offcanvas panel, and there is no
   icons-only state for a panel you deliberately opened. */
@media (min-width: 992px) {
    /* .iob-brand is not a separate ancestor to chain onto here -- it is the
       class on <body> itself (see <body class="iob-brand"> in Site.master),
       so body.iob-sidebar-collapsed already carries it. A selector reading
       "body.iob-sidebar-collapsed .iob-brand X" would ask for a second,
       different .iob-brand element nested inside body, which does not exist,
       and every rule in this block would silently never match. */
    body.iob-sidebar-collapsed .iob-sidebar {
        flex-basis: var(--sidebar-width-collapsed);
        width: var(--sidebar-width-collapsed);
    }

    body.iob-sidebar-collapsed .iob-sidebar__brand {
        justify-content: center;
        padding-left: var(--space-2);
        padding-right: var(--space-2);
    }

    /* Both variants of the full wordmark, light and dark, carry the shared
       .iob-sidebar__logo-full class precisely so this one rule hides either
       of them when the rail collapses -- see the theme swap on
       .iob-sidebar__logo-full--light/--dark further down, which this always
       outranks regardless of source order: the type selector on body here
       is worth more than the extra rule those add, (0,2,1) beating (0,2,0). */
    body.iob-sidebar-collapsed .iob-sidebar__logo-full {
        display: none;
    }

    body.iob-sidebar-collapsed .iob-sidebar__logo-mark {
        display: block;
    }

    /* The nav list's own side padding comes off so a centered icon has the
       full rail width to center inside, same as the ul above it did not need
       to when a label filled the row instead. */
    body.iob-sidebar-collapsed .iob-nav {
        padding-left: 0;
        padding-right: 0;
        align-items: center;
    }

    /* Centred by shrinking to its content and letting the ul's own
       align-items: center place it, not by pinning a width and zeroing the
       link's own padding -- that put the icon flush against the 3px cyan
       .active bar (an inset box-shadow, so it always sits at the link's own
       left edge regardless of padding) with no breathing room on either
       side. Keeping the base padding is what the full-width rail already
       relies on for that same gap. */
    body.iob-sidebar-collapsed .iob-nav__link {
        justify-content: center;
    }

    /* visually-hidden, not display: none -- Bootstrap's own formula, copied
       rather than referenced because it has to survive independently of
       whatever order the utility classes load in. The row's accessible name
       (the label text, and the account button's actual name) has to stay
       readable to a screen reader; only the caret is pure decoration and
       already carries aria-hidden, so it is the one exception. */
    body.iob-sidebar-collapsed .iob-nav__label,
    body.iob-sidebar-collapsed .iob-nav__name {
        position: absolute;
        width: 1px;
        height: 1px;
        padding: 0;
        margin: -1px;
        overflow: hidden;
        clip: rect(0, 0, 0, 0);
        white-space: nowrap;
        border: 0;
    }

    /* The account button and the collapse toggle no longer fit side by side
       in a 64px rail, so the row that held them stacks instead. */
    body.iob-sidebar-collapsed .iob-sidebar__footrow {
        flex-direction: column;
        gap: var(--space-2);
    }

    body.iob-sidebar-collapsed .iob-nav__link--toggle {
        width: auto;
        justify-content: center;
    }
}

/* The middle scrolls, so a long section menu -- Setup runs to sixteen links and
   three headings -- can overflow without pushing the foot off the bottom.
   min-height: 0 is what allows a flex child to shrink below its content and
   actually scroll; without it the child keeps its full height and the overflow
   silently moves to the page instead. */
.iob-brand .iob-sidebar__scroll {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
}

.iob-brand .iob-sidebar__brand {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: var(--space-2) var(--space-4) var(--space-4);
}

/* The mark's own size, in the stylesheet rather than only in the img's height
   attribute -- the attribute stays as the pre-CSS fallback and to reserve the
   box before the SVG paints, but this is the number to change. The logo is a
   3:1 wordmark, so 36px of height is ~108px of width inside a 280px rail and
   has room to spare.

   Below lg the rail IS the menu -- an offcanvas panel you deliberately opened --
   and there is nothing else competing for the top of it, so the mark goes up
   again. At 26px it read as a favicon on a phone. */
.iob-brand .iob-sidebar__logo img {
    height: 36px;
    width: auto;
}

@media (max-width: 991.98px) {
    .iob-brand .iob-sidebar__logo img {
        height: 44px;
    }
}

/* The white wordmark is the one shown by default in markup order, so it has
   to be the one hidden by default here -- otherwise a page with the dark
   attribute not yet applied (there is a brief window: see the pre-paint
   script in Site.master, which sets it before first paint but is still
   JS that has to run) would flash both logos stacked. */
.iob-sidebar__logo-full--dark {
    display: none;
}

[data-bs-theme="dark"] .iob-sidebar__logo-full--light {
    display: none;
}

[data-bs-theme="dark"] .iob-sidebar__logo-full--dark {
    display: inline;
}

.iob-brand .iob-nav {
    list-style: none;
    margin: 0;
    padding: 0 var(--space-2);
    display: flex;
    flex-direction: column;
    gap: 2px;
}

/* The tools, below the six sections: Settings and Help. The rule is on the <ul>
   rather than on a first <li>, which is what lets Settings be rights-gated
   without leaving a hairline in the wrong place. Not an empty <li> separator
   either: a list item with no content is a thing a screen reader still counts.

   This was two groups until 22 September 2026 -- Front desk had one of its own
   above Settings, with an .iob-nav--deskonly rule hiding it below lg because it
   is a reception display and not a page you read on a phone. Nobody was using
   it, so the item, that rule and the page itself were all deleted; see the note
   where the route used to be in Global.asax.cs. The two-group pattern is still
   worth knowing, because Settings is rights-gated the same way. */
.iob-brand .iob-nav--tools {
    margin-top: var(--space-3);
    padding-top: var(--space-3);
    border-top: 1px solid var(--border-hairline);
}

.iob-brand .iob-nav__link {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-control);
    color: var(--text-body);
    font-size: var(--font-size-dense);
    line-height: 1.4;
}

    .iob-brand .iob-nav__link .fa,
    .iob-brand .iob-nav__link .fas,
    .iob-brand .iob-nav__link .fal {
        width: 1.1em;
        text-align: center;
        color: var(--text-muted);
    }

    .iob-brand .iob-nav__link:hover,
    .iob-brand .iob-nav__link:focus {
        background-color: var(--surface-sunken);
        color: var(--text-strong);
    }

    /* THE ONE LABEL IN THE RAIL THAT IS NOT OURS. Every other item here is a
       fixed string we picked and measured the rail against; this one is
       whatever the account's first name happens to be.

       The row is a nowrap flex line, and a name is a single word with nothing
       to break on, so it cannot wrap -- it overflows, and .iob-nav__link is
       overflow: visible, so it draws straight across the page beside it. That
       was already true at the old width, just further away: a name had to pass
       about twenty characters. At 176px it is about fifteen, which is still
       past any ordinary first name but close enough to be worth closing.

       min-width: 0 is the part that does the work. A flex item will not shrink
       below its content by default, so without it the ellipsis never gets the
       chance to happen and the name overflows exactly as before. */
    .iob-brand .iob-nav__name {
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

/* THE ACTIVE MARKER IS THE BRAND CYAN, and this is the job the cyan is actually
   good at. It cannot carry white text (2.53:1) so it was never going to be the
   button; as a 3px rail against a tinted row it is unmistakable and costs the
   page nothing. .active is written by SiteMaster.SetActiveNav, which replaces
   the li's class attribute wholesale -- hooking .active rather than a class of
   our own is what keeps that working. */
.iob-brand .iob-nav > .nav-item.active > .iob-nav__link {
    background-color: var(--surface-sunken);
    color: var(--text-strong);
    font-weight: 500;
    box-shadow: inset 3px 0 0 var(--accent);
}

/* The mail toolbar, at the top of the three /messages/ pages. It replaces the
   .iob-subnav block, which styled the Setup, Reports and Inbox menus in the
   sidebar rail and had no users left once all three moved out.

   A row, not a rail: the New Message button, then the folders as pills. Below
   sm the button goes full width and the folders wrap under it, which is the
   only thing that changes -- two folders and one button need no disclosure. */
.iob-brand .iob-mailbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-3);
    margin-bottom: var(--space-4);
}

    .iob-brand .iob-mailbar__folders {
        display: flex;
        flex-wrap: wrap;
        gap: var(--space-2);
    }

    .iob-brand .iob-mailbar__folder > a {
        display: inline-flex;
        align-items: center;
        gap: var(--space-2);
        padding: var(--space-2) var(--space-3);
        border-radius: var(--radius-control);
        color: var(--text-body);
        font-size: var(--font-size-dense);
        line-height: 1.5;
        text-decoration: none;
        /* 44px, so a folder is a comfortable target on a phone. */
        min-height: 2.75rem;
    }

        .iob-brand .iob-mailbar__folder > a:hover,
        .iob-brand .iob-mailbar__folder > a:focus-visible {
            background-color: var(--surface-sunken);
            color: var(--text-strong);
        }

    /* The folder you are in: an outlined pill in the accent ink, rather than the
       accent rail the sidebar uses. A pill in a row of pills reads as selected
       the way a tab does, and there is no rail here to hang a marker off.

       It used to be --surface-sunken with a hairline border, and that could not
       be seen: this toolbar sits on the page background, not on a card, and
       #f2f4f7 against #eef1f5 is 1.03:1 (25 September 2026). No pale fill can fix
       that, since the accent wash is the same 1.03:1 there, so the outline and the
       ink carry the selection. The 2px inset ring in --accent-ink is about 4.6:1
       against the page in light mode and 7:1 in dark. The label takes the same
       ink, at 4.97:1 on the wash in light and 5.33:1 in dark. The wash stays as a
       supporting cue, and inset keeps the pill the same size as its neighbour. */
    .iob-brand .iob-mailbar__folder.active > a {
        background-color: var(--accent-wash);
        color: var(--accent-ink);
        font-weight: 600;
        box-shadow: inset 0 0 0 2px var(--accent-ink);
    }

    .iob-brand .iob-mailbar__folder i {
        color: var(--text-muted);
    }

    .iob-brand .iob-mailbar__folder.active i {
        color: var(--accent-ink);
    }

@media (max-width: 575.98px) {
    .iob-brand .iob-mailbar__new {
        width: 100%;
    }
}

/* The dashboard status strip. A row at desktop width -- who you are, what your
   status is, the facts about it, and the one button that changes it -- and a
   stack below md. It replaces a full-height card in a column of its own, which
   spent a third of the dashboard telling you your own name. */
.iob-brand .iob-statusbar .card-body {
    padding: var(--space-3) var(--space-4);
}

/* Two rows and two columns from md up: the strip and the Update Status button
   on the first, the comment spanning both on the second. A grid rather than a
   wrapping flex row because the comment has to take a whole line, and in one
   flex container that pushed the button onto a third one -- 188px of strip for
   four facts. Below md it is a plain block, so the DOM order stands: who,
   status, facts, comment, then the button as the last thing you reach. */
@media (min-width: 768px) {
    .iob-brand .iob-statusbar .card-body {
        display: grid;
        grid-template-columns: 1fr auto;
        align-items: center;
        column-gap: var(--space-5);
    }

    /* Placed, not auto-flowed: the button is the third child in the DOM so that
       a phone reaches it last, and auto-placement would put it on a third row
       under the comment. */
    .iob-brand .iob-statusbar__action {
        grid-column: 2;
        grid-row: 1;
        justify-self: end;
    }

    .iob-brand .iob-statusbar__comment {
        grid-column: 1 / -1;
        grid-row: 2;
    }
}

/* The first line: who you are, what your status is, and the facts about it. */
.iob-brand .iob-statusbar__main {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-3) var(--space-5);
}

.iob-brand .iob-statusbar__who {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    color: var(--text-strong);
    text-decoration: none;
    min-width: 0;
}

    .iob-brand .iob-statusbar__who:hover .iob-statusbar__name {
        text-decoration: underline;
    }

    .iob-brand .iob-statusbar__name {
        display: block;
        font-weight: 600;
        line-height: 1.3;
    }

    .iob-brand .iob-statusbar__title {
        display: block;
        font-size: var(--font-size-meta);
        color: var(--text-muted);
        line-height: 1.3;
    }

/* The chip keeps its inline colours: they come from dbo.SystemColors per status
   and are the user's own choice, so nothing here touches colour. */
.iob-brand .iob-statusbar__chip {
    padding: var(--space-1) var(--space-3);
    font-weight: 500;
    white-space: nowrap;
}

.iob-brand .iob-statusbar__facts {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--space-1) var(--space-4);
    margin: 0;
    min-width: 0;
}

    .iob-brand .iob-statusbar__facts > div {
        display: flex;
        align-items: baseline;
        gap: var(--space-2);
        min-width: 0;
    }

    .iob-brand .iob-statusbar__facts dt {
        font-size: var(--font-size-meta);
        font-weight: 400;
        color: var(--text-muted);
        white-space: nowrap;
    }

    .iob-brand .iob-statusbar__facts dd {
        margin: 0;
        font-size: var(--font-size-dense);
    }

/* The comment line, under the strip. A comment is free text and can run to a
   sentence, so it is a sibling of the strip rather than an item inside it. */
.iob-brand .iob-statusbar__comment {
    display: flex;
    align-items: baseline;
    gap: var(--space-2);
    margin: var(--space-2) 0 0;
}

    .iob-brand .iob-statusbar__comment dt {
        font-size: var(--font-size-meta);
        font-weight: 400;
        color: var(--text-muted);
        white-space: nowrap;
    }

    .iob-brand .iob-statusbar__comment dd {
        margin: 0;
        font-size: var(--font-size-dense);
    }

/* The grid places it at the end of the first row from md up. Below that it is
   a block in the flow, full width, a proper target on a phone rather than a
   button floating mid-row. */
.iob-brand .iob-statusbar__action {
    white-space: nowrap;
}

@media (max-width: 767.98px) {
    .iob-brand .iob-statusbar__main {
        align-items: flex-start;
    }

    .iob-brand .iob-statusbar__action {
        display: block;
        margin-top: var(--space-3);
        width: 100%;
    }
}

/* The toolbar at the top of a card body: the two or three controls for the
   thing below it. Recent Messages, Upcoming Events and the calendar all have
   one, and all three used to disagree -- a bare <div> of buttons, a d-flex, and
   a row of two cols -- so this is the shape, once.

   .form-select is width:100% out of the box, which none of those layouts could
   talk it out of: every select took a whole line to itself. */
.iob-brand .iob-cardtools {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2) var(--space-4);
    margin-bottom: var(--space-3);
}

    /* iob.css still gives every .btn margin: 0.375rem, from the MDB era. Inside a
       flex row that lands on top of the gap -- 28px between two buttons where the
       token says 16 -- and the 6px on the left pushes them out of line with the
       column headings underneath. Zeroed here rather than globally: 0.375rem is
       load-bearing for the many places that still lay buttons out by margin. */
    .iob-brand .iob-cardtools .btn {
        margin: 0;
    }

    .iob-brand .iob-cardtools__filter {
        display: flex;
        align-items: center;
        gap: var(--space-2);
    }

        .iob-brand .iob-cardtools__filter label {
            margin: 0;
            font-size: var(--font-size-meta);
            color: var(--text-muted);
            white-space: nowrap;
        }

        /* Sized to its widest option, but capped: a company can name a calendar
           anything, and one long name should not push the control off the card. */
        .iob-brand .iob-cardtools__filter .form-select {
            width: auto;
            max-width: 16rem;
        }

/* --- Feed lists -----------------------------------------------------------

   THE DASHBOARD'S TWO LISTS, AS LISTS. Recent Messages and Upcoming Events
   were both a .row of three col-12 col-md-4 cells under a bold header row.
   That is a table drawn in grid classes, and it had the failure mode tables
   drawn in grid classes always have: below md the cells stack, the header
   stays behind at the top of the card, and by the second row nothing on
   screen says which block is the sender and which is the message. Read the
   dashboard on a phone and Recent Messages was nine anonymous lines.

   So: no header row, no columns, one stacked block per record at every width.
   The title is the thing you scan for and the only link; everything else is
   meta under it, in the order you would say it out loud.

   Only two dashboard sections use this so far. It is named for the shape
   rather than for either of them because the shape is the reusable part --
   Announcements is the obvious third, but it renders notes through
   DisplayNotes and that is a separate decision about markup in free text. */
.iob-brand .iob-feed {
    list-style: none;
    margin: 0;
    padding: 0;
}

.iob-brand .iob-feed__item {
    padding: var(--space-3) 0;
}

    /* The separator is a border between items rather than a border on every
       item plus a rule cancelling the last one. First and last lose their
       outer padding so the list sits flush in the card body. */
    .iob-brand .iob-feed__item + .iob-feed__item {
        border-top: 1px solid var(--border-hairline);
    }

    .iob-brand .iob-feed__item:first-child {
        padding-top: 0;
    }

    .iob-brand .iob-feed__item:last-child {
        padding-bottom: 0;
    }

.iob-brand .iob-feed__title {
    font-size: var(--font-size-dense);
    font-weight: 500;
    line-height: 1.4;
}

/* A title with something anchored to its right -- the time on a message. The
   time belongs at the end of the line rather than above the name: it is an
   index, not content, and leading with it put the quietest thing in the
   loudest position while the name, which is what you actually scan an inbox
   for, sat underneath.

   align-items: baseline so the time sits on the name's baseline rather than
   being centred against a name that has wrapped. min-width: 0 on the title is
   what lets a long name wrap instead of forcing the time off the edge -- a
   flex item defaults to min-width: auto and will not shrink below its content.
   nowrap on the time keeps "5:50 PM" and its badge together. */
.iob-brand .iob-feed__line {
    display: flex;
    align-items: baseline;
    gap: var(--space-2);
}

    .iob-brand .iob-feed__line .iob-feed__title {
        flex: 1 1 auto;
        min-width: 0;
    }

.iob-brand .iob-feed__when {
    flex: 0 0 auto;
    font-size: var(--font-size-meta);
    color: var(--text-muted);
    white-space: nowrap;
}

/* The muted tail on a line -- the location on an announcement. A span rather
   than more text in the title, so a title that is a link keeps the thing you
   would name as its accessible name.

   It carried the caller's company on a message for a while; that is a row of
   its own now, because a company under a name reads better than a company
   after one and it cannot push a long name onto a second line.

   :empty is doing real work here: the value is optional, and without this every
   record without one would carry a dangling separator. display:none takes the
   ::before with it, which is the whole trick. It only holds because the markup
   writes the span's contents with no whitespace inside the tags, and sets the
   span flush against whatever precedes it -- the ::before carries its own
   margin, so a newline in front would read as a double space. */
.iob-brand .iob-feed__sub {
    color: var(--text-muted);
    font-weight: 400;
}

    .iob-brand .iob-feed__sub::before {
        content: "\00b7";
        margin: 0 0.4em;
        color: var(--text-muted);
    }

    .iob-brand .iob-feed__sub:empty {
        display: none;
    }

/* One fact per line, all of them quieter than the title. Same :empty rule --
   a message with no phone number and an event with no location should cost
   nothing rather than leave a blank line in the middle of the block. */
.iob-brand .iob-feed__meta {
    font-size: var(--font-size-meta);
    color: var(--text-muted);
    line-height: 1.5;
}

    .iob-brand .iob-feed__meta:empty {
        display: none;
    }

/* A meta line with a fact at each end: an event's location on the left and
   its calendar on the right, under the time on the line above (Home.aspx,
   Upcoming Events). The location takes the room and wraps; the calendar is
   pushed right with margin-left: auto rather than by the location's width, so
   when an event has no location (:empty, above) the calendar still sits at the
   right edge instead of sliding left into its place. */
.iob-brand .iob-feed__line--meta > .iob-feed__meta:first-child {
    flex: 1 1 auto;
    min-width: 0;
}

.iob-brand .iob-feed__meta--end {
    flex: 0 1 auto;
    margin-left: auto;
    text-align: right;
}

/* The message body. pre-wrap is what pays for encoding the text instead of
   writing it to the page raw: the newlines a caller typed still break lines,
   but they do it as whitespace rather than as <br> we injected into their
   words.

   Not truncated. A long note makes a long card, and that is the honest
   rendering -- if these previews ever need a cap, clamp it here rather than
   cutting the string, so the full text stays in the page for search. */
.iob-brand .iob-feed__body {
    margin: var(--space-1) 0 0;
    font-size: var(--font-size-dense);
    color: var(--text-body);
    white-space: pre-wrap;
}

    .iob-brand .iob-feed__body:empty {
        display: none;
    }

        /* Mark as read sits above the stretched link. .stretched-link::after covers
           the whole item at z-index 1 to make the row a target, so a control inside
           the item is beneath it and unclickable until it is lifted; position:relative
           is what gives z-index something to act on.

           The dashboard's rows have no stretched link and need none of that, but they
           use this class too, so the control lands in the same place on both screens.
           Mark all as read is not here -- it is .iob-cardtools, outside the list. */
        .iob-brand .iob-feed__tools {
            display: flex;
            justify-content: flex-end;
            position: relative;
            z-index: 2;
        }

/* Unread, in the dashboard feed. The whole item, which is what
   .iob-maillist__row does on the Messages page, whose rows are now feed items
   too -- the two screens that show messages bold the same amount of the same
   thing, from one class.

   .iob-feed__title is named a second time because it declares its own
   font-weight: 500, and a declaration beats an inherited value however
   specific the ancestor. Everything else in the item -- the time, the
   company and phone line, the body -- has no weight of its own and inherits.

   The "Unread" badge is what carries this to a screen reader; weight alone is
   a visual-only signal and would leave one out. Neither half is on its own. */
.iob-brand .iob-feed__item--unread,
.iob-brand .iob-feed__item--unread .iob-feed__title {
    font-weight: 600;
}

/* The way out of a card, under the data. The counterpart to .iob-cardtools,
   which is the row of controls ABOVE it -- tools act on what you are looking
   at, the foot sends you somewhere else, and the two had been sharing the top
   of the card where the difference was invisible.

   A link, not a .btn. Announcements already ends this way with "View more
   announcements"; a card that ends in a filled button reads as though leaving
   were the point of the card. The hairline is the same one that separates
   feed items, so the foot looks like the row after the last one. */
.iob-brand .iob-cardfoot {
    margin-top: var(--space-3);
    padding-top: var(--space-3);
    border-top: 1px solid var(--border-hairline);
    text-align: center;
    font-size: var(--font-size-dense);
}

    /* Two feet in a row draw one line, not two -- without this they each bring
       a hairline and the card ends in a ladder.

       The Home empty states (divNoMessages, divNoAnnouncements) used to carry
       this class too, which is what this rule was written for. They no longer
       do, as of 26 September 2026: in an empty card the "nothing here" line is
       the first thing in the body, so its hairline drew a rule under a blank
       band with nothing above it to separate from. They are plain text-center
       font-small now, the same as divNoEvents, and a following foot draws the
       one line between them. */
    .iob-brand .iob-cardfoot + .iob-cardfoot {
        margin-top: var(--space-2);
        padding-top: 0;
        border-top: 0;
    }


/* --- A form's actions, pinned to the bottom on a phone ---------------------

   TRIAL, 21 September 2026, on /profile/edit/ only. If it stays, the class
   comes off and this hangs on .card-footer directly -- 52 pages carry one.

   Sticky rather than fixed. Fixed pins to the glass and is the more native
   feel, but it leaves the document, so the last field needs matching bottom
   padding or it hides underneath, and it holds its place when the keyboard is
   up and the room is scarcest. Sticky stays in flow: it rides the bottom of
   the screen while the card is in view and settles at the end of the card
   when you reach it, which on a page that is one long form is the same thing
   for the whole scroll.

   No safe-area padding of its own. Below md this bar always rests on the tab
   bar, which carries the home indicator's inset; while the bars are away,
   --tabbar-offset becomes that inset, so the bar stops above the swipe-home
   line rather than on it. (Until 18 Oct 2026 this padded itself with
   env(safe-area-inset-bottom), which was 0 then: no viewport-fit=cover. With
   cover it would have put 34pt of empty bar between Save and the tab bar.)

   The footer becomes a flex row because .btn still carries margin: 0.375rem
   from the MDB era, and in a bar that margin is what sets the gap between the
   buttons and the space above them. Zeroed and replaced with a gap token, the
   same trade .iob-cardtools and the split card header already make.

   Order is unchanged: Save then Cancel, primary first. That is a settled
   decision and moving the bar is not a reason to revisit it. */
@media (max-width: 767.98px) {
    .iob-brand .card-footer.iob-actionbar {
        position: sticky;
        bottom: var(--tabbar-offset, 0px);
        z-index: 3;

        display: flex;
        flex-wrap: wrap;
        justify-content: center;
        /* Without this the row stretches every item to the height of the tallest,
           which on a footer with five buttons made each one a 115px column of
           wrapped text. */
        align-items: center;
        gap: var(--space-2);

        background-color: var(--surface-card);
        border-top: 1px solid var(--border-hairline);
        /* The card's own corners, so a pinned bar does not read as pasted on. */
        border-bottom-left-radius: var(--radius-card);
        border-bottom-right-radius: var(--radius-card);
        padding-bottom: var(--space-3);
    }

        .iob-brand .card-footer.iob-actionbar .btn {
            margin: 0;
        }

        /* A SERVER-SIDE VISIBILITY WRAPPER IS NOT LAYOUT.

           Several of these footers group their buttons in a <span runat="server">
           so the code-behind can show or hide a set at once -- divMessages,
           divPrivateLinks, divChangePassword on the profile. As a flex item that
           span becomes a column of its own and the buttons inside it stack
           vertically instead of joining the row, which is what turned the profile
           footer into five ragged columns of wrapped text.

           display: contents dissolves the box and leaves the buttons as siblings
           of the ones beside them. Safe on these: they are plain spans with no
           role and nothing but buttons inside, so there is no semantics to lose,
           and a span the code-behind sets Visible="false" is not rendered at all
           rather than being display: none. */
        .iob-brand .card-footer.iob-actionbar > span,
        .iob-brand .card-footer.iob-actionbar > div {
            display: contents;
        }

    /* PINNING IS FOR FORM ACTIONS. THE LAYOUT IS FOR EVERY FOOTER.

       Five pages carry an action bar and no field to edit: the profile, a
       resource, a message, a calendar event and the payment centre. Their
       buttons navigate away or act on the record -- they are not a Save that
       has to stay reachable while you work down a long form, which is the
       whole argument for pinning. Pinned they charge for it anyway, and the
       profile charged the most: six buttons wrap to 146px, 18% of an 812px
       screen, held for the length of the page.

       So they keep the flex row, the wrapping, the gap and the rest of the
       layout above, and give up only the sticky. Chris's call, 22 September
       2026, after seeing the profile on a real phone.

       The test was "does the page have an input, select or textarea the user
       edits" rather than a judgement about what each page is for -- /profile/
       photo/ has a single file input and keeps its pin on that basis. */
    .iob-brand .card-footer.iob-actionbar--static {
        position: static;
    }
}


/* --- Something you can hit with a thumb ------------------------------------

   Measured at 375px across the five screens that matter most on a phone: the
   dashboard, Change Status and the three boards. The links in them render 17px
   tall -- "Go to messages", "View all announcements", a sender's name, a
   person's name on a board, the column sort links -- and the status chips in
   the dashboard legend 20px. 24x24 CSS px is the floor a touch target should
   meet; a 17px line of text is a third of a fingertip.

   PADDING, NOT HEIGHT, AND THAT IS THE WHOLE TRICK. These are inline anchors
   inside sentences and list rows. min-height does nothing to an inline box, and
   making them inline-block or inline-flex changes how they wrap and pushes the
   lines around them apart -- on the feed lists it moved every row. Vertical
   padding on an inline element does NOT add to the line box, so nothing on the
   page moves, but it does enlarge the hit area and the focus ring. The rows are
   38px apart at the tightest, so the padded boxes come close but never overlap.

   Below md only. On a desktop these are mouse targets and 17px is fine; the
   padding would only inflate focus rings for no gain.

   Not a conformance item: 24x24 is WCAG 2.2 SC 2.5.8 and this product claims
   Section 508 / WCAG 2.0, which has no target-size criterion. See the note on
   --border-control in Docs/AccessibilityBacklog.md. This is here because the
   application is used on a phone, not because a report asks for it. */
@media (max-width: 767.98px) {
    .iob-brand .iob-cardfoot a,
    .iob-brand .iob-feed__title > a,
    .iob-brand .sort-link,
    .iob-brand .sb-board [role="cell"] > a,
    .iob-brand .sb-card-meta .sb-card-action a,
    .iob-brand .iob-text-button,
    /* Filter and Columns, the two controls above every board. Matched on the
       attributes the markup already carries rather than on a class: the link is
       runat="server", so its rendered id is a mangled ClientID and a plain
       #FilterOptionLink never matches -- the [id$=] rule this repository learned
       the hard way, written up in Docs/AppUiDecisions.md. The button's data-bs-target is the
       modal it opens, which is as stable as a class and already there. Columns
       is absent from Card view, which has no columns to pick; the selector
       simply finds nothing there. */
    .iob-brand [id$="FilterOptionLink"],
    .iob-brand [data-bs-target="#columnModal"],
    /* .sb-card-identity > a used to be in this list too, sharing 767.98 with
       everything else here. It does not belong at that width: the flex it
       needs is only true once .sb-card-identity itself is a grid, which
       happens below 575.98, not below 767.98. From 576 up to 767.98 the card
       is still the centred stack from CardView.aspx's text-sm-center, and a
       flex container ignores an ancestor's text-align in favour of its own
       justify-content (default flex-start) -- so the name in that 192px gap
       rendered left rather than centred. Moved to its own rule below, at the
       breakpoint the comment on it already claimed. */
    /* The Messages screens. A value in a .iob-datalist that happens to be a link
       -- "Created by" on a message -- and the two helpers on the new-message
       form: the link to the team list, which sits at the end of a sentence of
       help text, and the "(me)" shortcut beside the From label. All three are
       inline in prose, which is exactly the case padding is for. */
    .iob-brand .iob-datalist dd a,
    .iob-brand .font-small a,
    .iob-brand .btn-unstyled.small,
    /* The breadcrumb, which is on fifty pages -- every Setup and Reports screen
       and most of the edit pages. Its links render 20px, and on those pages the
       breadcrumb is the way back: the section menus were deleted when the
       sidebar arrived and the crumb took over the job. */
    .iob-brand .breadcrumb a,
    /* The Setup and Reports hubs: 16 tiles and 12, each an icon, a link and a
       line of description, and between them the ONLY way into those two
       sections -- the crumb gets you back out, these get you in. Bare anchors
       with no class of their own, rendering 19px; .iob-setting is a hook added
       to the tile div in Setup.aspx and Reports.aspx for exactly this.

       Descendant rather than child: one tile wraps its link in a runat="server"
       div so the whole thing can be hidden by permission, and a child
       combinator missed it -- User Report stayed 19px while the other eleven
       grew. A tile holds one link and a line of description, so there is
       nothing else for a descendant selector to catch. */
    .iob-brand .iob-setting a {
        padding-block: 0.25rem;
    }

    /* The dashboard's chart legend: nine status chips, each a link to that
       status on the board, and the only way to reach a filtered board from the
       dashboard. 20px tall.

       min-height rather than padding, and not by preference. The chips carry
       Bootstrap's .p-1, and Bootstrap 5 generates its spacing utilities with
       !important, so a padding declaration here loses however specific the
       selector is -- measured: the rule matched, and the computed padding was
       still .p-1's 4px. Nothing sets min-height on them, so it applies
       cleanly. inline-flex to centre the label in the taller box; they are
       inline-block already, so nothing else about the row changes. */
    .iob-brand #StatusChartLegend a.badge {
        min-height: 24px;
        display: inline-flex;
        align-items: center;
    }

}

/* The name on a card takes min-height instead, and this is the exception
   that proves why padding is the tool everywhere else. .sb-card-identity is
   a grid below sm, so its children are grid items and a grid item is
   blockified -- padding on it is real layout, not just hit area. Padding
   here put 16px on every card and took the view from 100px a card back to
   116px, undoing most of what the grid had just won. min-height costs the
   5px between the line box and 24 and nothing more.

   575.98, matching .sb-card-identity's own breakpoint in iob.css -- not the
   767.98 above. display: flex only reads as "taller hit area" while the grid
   makes .sb-card-identity's children grid items already blockified by
   layout; above 575.98 the card is CardView.aspx's centred stack, and a flex
   container's own justify-content (default flex-start) would win over the
   ancestor's text-align: center on the single text node inside. */
@media (max-width: 575.98px) {
    .iob-brand .sb-card-identity > a {
        min-height: 24px;
        display: flex;
        align-items: center;
    }
}


/* The soft state badge: "how many, is it new, has it been read". One rule,
   because it is one decision -- the unread count beside a mail folder, the
   "new" marker on the dashboard and "Not Read" in the sent list all say some
   version of "nobody has looked at this yet", and they had better not argue
   about it. Each of the three arrived wearing something different: the
   dashboard marker was <span class="orange white-text">, white on #ff9800 at
   2.16:1, unseen for as long as the dev database had no unread message; "Not
   Read" was .text-warning, which resolves to #856404 -- dark olive, 5.49:1, so
   it passed contrast and still read as ordinary text in a column of ordinary
   text, which is the opposite of what a state wants.

   Accent ink on the accent wash rather than Bootstrap's .bg-info or MDB's
   .orange, both of which are fills that cannot hold white text. #00719c on
   #e6f7fe measures 4.97:1 in the renderer. */
.iob-brand .iob-badge-soft {
    background-color: var(--accent-wash);
    color: var(--accent-ink);
    font-size: var(--font-size-meta);
    font-weight: 600;
    padding: 0.15em 0.5em;
    border-radius: 10rem;
}

/* Messages: the folder's search (From on the Inbox, Sent to on Sent) above the
   list, divided from it by a hairline the full width of the card, as the card
   header is from the body. The negative margin takes the line to the card's
   edges; the padding puts the controls back where the card body has them.
   Stronger than the row separators below only by running edge to edge, which
   is what makes it read as the end of the controls rather than one more row
   (8 Oct 2026). */
.iob-brand .iob-mailsearch {
    margin: 0 calc(-1 * var(--space-4)) var(--space-3);
    padding: 0 var(--space-4) var(--space-1);
    border-bottom: 1px solid var(--border-hairline);
}

/* The message list. Each row is one link, stretched over the row by Bootstrap's
   .stretched-link, so the whole thing is the target and there is one tab stop
   per message. The row therefore has to LOOK like a target: without a hover
   state, a clickable row is a clickable row nobody knows about. */
.iob-brand .iob-maillist__row:hover {
    background-color: var(--surface-sunken);
}

    /* .stretched-link's ::after covers the row, so :focus-visible on the anchor
       draws a ring around the anchor's own text and nothing else. Put the ring
       on the row instead -- a keyboard user needs to see the thing that is
       about to activate, which is the row. */
    .iob-brand .iob-maillist__row:focus-within {
        background-color: var(--surface-sunken);
        outline: 2px solid var(--accent-ink);
        outline-offset: -2px;
    }

    .iob-brand .iob-maillist__row a.stretched-link {
        color: var(--text-body);
        text-decoration: none;
    }

        /* Underline on hover only. Fifty rows of underlined text is a hedge, and
           the row already says it is clickable by lighting up. */
        .iob-brand .iob-maillist__row:hover a.stretched-link {
            text-decoration: underline;
        }

    /* Unread. Weight lives on .iob-feed__item--unread now, shared with the
       dashboard so one class means one thing; this is just the link colour,
       which only applies where a row is a stretched target. A background is
       how the row says "you are pointing at me", and two backgrounds fighting
       over the same row is how you end up unable to tell which is which. */
    .iob-brand .iob-feed__item--unread a.stretched-link {
        color: var(--text-strong);
    }

.iob-brand .iob-sidebar__foot {
    margin-top: auto;
    padding-top: var(--space-3);
    border-top: 1px solid var(--border-hairline);
}

/* The account menu and the collapse toggle share the foot's bottom row. The
   account menu is the item people actually reach for, so it gets the space
   that is left; the toggle is a fixed 32px square next to it. */
.iob-brand .iob-sidebar__footrow {
    display: flex;
    align-items: center;
    gap: var(--space-1);
}

    .iob-brand .iob-sidebar__footrow .iob-nav {
        flex: 1 1 auto;
        min-width: 0;
    }

/* Hidden by default -- see the @media (min-width: 992px) block further down.
   Below lg the rail is an offcanvas panel you opened on purpose; collapsing
   it to icons would defeat opening it. */
.iob-brand .iob-sidebar__collapse {
    display: none;
    flex: 0 0 auto;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    padding: 0;
    background: none;
    border: 0;
    border-radius: var(--radius-control);
    color: var(--text-muted);
}

    .iob-brand .iob-sidebar__collapse:hover,
    .iob-brand .iob-sidebar__collapse:focus {
        background-color: var(--surface-sunken);
        color: var(--text-strong);
    }

/* The compact mark, swapped in for the full wordmark only once the rail is
   narrow enough that the wordmark would not fit. Hidden here so a page with
   JS disabled -- the toggle does nothing without it -- never shows two
   marks stacked in the brand row. */
.iob-brand .iob-sidebar__logo-mark {
    display: none;
}

/* The mark is a cyan doorway with a near-black arrow going in, transparent
   everywhere else -- fine on the light rail, but the arrow all but
   disappears against the dark rail's navy. There is no separate dark asset
   for it the way the full wordmark has inoutboard-logo-white.svg, and
   recolouring an SVG's fill from a plain <img> tag needs either a second
   file or a filter that would also shift the doorway's own cyan off-brand.
   A small light disc behind it is the cheaper fix: the doorway still reads
   as itself, and the arrow gets a light backdrop to sit on instead of navy. */
[data-bs-theme="dark"] .iob-brand .iob-sidebar__logo-mark {
    background-color: var(--gray-100);
    border-radius: 50%;
    padding: 2px;
}

/* Smaller than the wordmark it replaces -- the wordmark's 36px is sized to a
   3:1 label, but this mark is square and reads oversized at the same
   height next to icons that are themselves about 17px. The extra "img" in
   the selector is what beats .iob-sidebar__logo img's own 36px above without
   resorting to !important: two classes plus a type selector outranks two
   classes alone regardless of which comes first in the file. */
.iob-brand .iob-sidebar__logo img.iob-sidebar__logo-mark {
    height: 20px;
    width: auto;
}

/* The override that actually shows the toggle, at the same breakpoint the
   sticky rail switches on. Kept below the base rule above rather than beside
   it -- both are unguarded-vs-guarded rules of equal specificity on the same
   selector, so source order is what decides, and the guarded one has to be
   the LATER of the two to win at lg and up. */
@media (min-width: 992px) {
    .iob-brand .iob-sidebar__collapse {
        display: flex;
    }
}

/* THE ACCOUNT MENU. The foot row is a <button> now rather than a link to the
   profile, so the UA button styles have to come off before .iob-nav__link above
   can do its job: a button draws a border and a background, centres its text,
   and shrinks to its content instead of filling the row. width: 100% is what
   makes the whole row the target rather than just the name.

   NO `font` SHORTHAND HERE, and this cost a measurement to find. `font: inherit`
   looks like the tidy way to undo the UA font, but the shorthand also sets
   line-height, and this rule comes after .iob-nav__link with the same
   specificity -- so it quietly won, the row rendered at 1.5 instead of 1.4, and
   the foot sat 37px against the 36px of every other item in the rail. Bootstrap
   reboot already gives buttons font-family/size/line-height: inherit, and
   .iob-nav__link overrides the last two, so there is nothing left to declare. */
.iob-brand .iob-nav__link--toggle {
    width: 100%;
    background: none;
    border: 0;
    text-align: left;
}

/* The rail is 176px and "Profile and settings" does not fit in it. Letting the
   menu be wider than the rail is deliberate -- it overlays the content, which
   is what a menu is for, and the alternative is wrapping every second label.
   Nothing clips it: .iob-sidebar sets no overflow, and the sticky positioning
   at lg only makes it the containing block. */
.iob-brand .iob-accountmenu {
    min-width: 13rem;
    font-size: var(--font-size-dense);
}

    /* The menu is part of the rail, so its rows are rail rows: the same padding,
       the same leading-icon box, the same body colour. Two defaults had to go to
       get there, both measured against the items above rather than guessed --
       Bootstrap gives .dropdown-item 4px of vertical padding, which made a 29px
       row against the rail's 36px, and an <a> in this codebase is link-blue
       (#0a58ca), which put four blue labels under seven grey ones. */
    .iob-brand .iob-accountmenu .dropdown-item {
        display: flex;
        align-items: center;
        gap: var(--space-3);
        padding: var(--space-2) var(--space-3);
        color: var(--text-body);
    }

        .iob-brand .iob-accountmenu .dropdown-item:hover,
        .iob-brand .iob-accountmenu .dropdown-item:focus {
            background-color: var(--surface-sunken);
            color: var(--text-strong);
        }

        .iob-brand .iob-accountmenu .dropdown-item .fa,
        .iob-brand .iob-accountmenu .dropdown-item .fas {
            width: 1.1em;
            text-align: center;
            color: var(--text-muted);
        }

/* THE ACCOUNT MENU AS A LIST, below lg. In the phone panel the menu's items
   are listed under the name rather than behind it -- opening the panel and
   then a menu inside it was two taps to reach four things. Same markup as the
   pop-up at lg, restyled: the name button goes, .iob-sidebar__who says whose
   items these are, and the list sits in the flow. The script in Site.master
   takes .dropdown-menu off the <ul> at this width so Bootstrap's keyboard
   handling leaves it alone; the !important lines are for the one case the
   class is still on, a desktop window with the menu open narrowed past lg,
   where Popper's inline position would otherwise win. */
.iob-brand .iob-sidebar__who {
    display: none;
}

@media (max-width: 991.98px) {
    .iob-brand .iob-sidebar__who {
        display: flex;
        align-items: center;
        gap: var(--space-3);
        margin: 0 var(--space-2);
        padding: var(--space-2) var(--space-3);
        color: var(--text-strong);
        font-size: var(--font-size-dense);
        font-weight: 500;
        line-height: 1.4;
    }

    .iob-brand .iob-sidebar__foot .iob-nav__link--toggle {
        display: none;
    }

    .iob-brand .iob-sidebar__foot .iob-accountmenu {
        display: block !important;
        position: static !important;
        inset: auto !important;
        transform: none !important;
        margin: 0 !important;
        min-width: 0;
        padding: 0;
        list-style: none;
        background: none;
        border: 0;
        border-radius: 0;
        box-shadow: none;
    }

        .iob-brand .iob-sidebar__foot .iob-accountmenu .dropdown-item {
            border-radius: var(--radius-control);
            line-height: 1.4;
        }

        .iob-brand .iob-sidebar__foot .iob-accountmenu .dropdown-divider {
            margin: var(--space-1) var(--space-3);
            border-top: 1px solid var(--border-hairline);
            opacity: 1;
        }

    /* On your own profile the rail marks the name button as the active item.
       Below lg that button is hidden, so the mark moves to Profile and settings,
       which is where it goes. */
    .iob-brand .iob-sidebar__foot .nav-item.active .iob-accountmenu > li:first-child > .dropdown-item {
        background-color: var(--surface-sunken);
        color: var(--text-strong);
        font-weight: 500;
        box-shadow: inset 3px 0 0 var(--accent);
    }
}

/* .iob-nav__sep was here: a hairline above Log out when Log out was a second
   item in the foot. It moved inside the account menu on 22 September 2026 and
   the rule is Bootstrap's own .dropdown-divider now, which is the right tool
   once the thing being divided is a menu. */

.iob-brand .iob-main {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
}

.iob-brand .iob-content {
    padding: var(--space-5) var(--space-6);
    flex: 1 1 auto;
}

/* The slim bar that exists only below lg, to open the panel. */
.iob-brand .iob-topbar {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-2) var(--space-3);
    background-color: var(--surface-nav);
    border-bottom: 1px solid var(--border-hairline);
}

/* Same story as the rail's mark: the size lives here, the img's height attribute
   is the fallback and the box the browser reserves before the SVG paints.

   Smaller than the 44px in the open panel, and deliberately so. This bar is
   chrome you look past on your way to the page -- it sits above every screen --
   whereas the panel's mark only exists while the menu is open. 30px is ~90px of
   width next to the 30px hamburger, which it now exactly matches -- the bar takes
   its height from the taller of the two, so this is as far as the mark goes
   before the bar starts growing with it. Measured: bar 59px, unchanged. */
.iob-brand .iob-topbar__logo img {
    height: 30px;
    width: auto;
}

/* Same swap as the rail's own brand row, and the same reason: the topbar's
   background is --surface-nav too, so it goes navy right along with the
   rail in dark mode and the black wordmark needs the same white asset. */
.iob-topbar__logo-img--dark {
    display: none;
}

[data-bs-theme="dark"] .iob-topbar__logo-img--light {
    display: none;
}

[data-bs-theme="dark"] .iob-topbar__logo-img--dark {
    display: inline;
}

/* THE TOP BAR COMES BACK ON SCROLL UP. Sticky, and slid out of the way while
   the page is scrolling down; the script in Site.master adds and removes
   .iob-bars-away on <body>, which moves the tab bar the other way at the same
   moment (see the tab bar block below). Sticky rather than fixed so it keeps
   its place in the flow at the top of the page and nothing below needs a
   spacer for it.

   1020 is Bootstrap's sticky layer: under the tab bar's 1030 and under the
   offcanvas panel, so opening the menu from it covers it like everything else.

   scroll-margin keeps focus and anchor jumps out from under both bars. A
   field moved into view by focus -- validation-a11y.js does this on a failed
   save -- would otherwise land behind the top bar, or behind the tab bar at
   the bottom. 60px is the top bar; --tabbar-offset is the tab bar.

   On the content, NOT scroll-padding on <html>. Measured 26 September 2026:
   scroll-padding covers the whole viewport, so the top bar itself sits inside
   its 60px band, and focusing the hamburger or the logo made Chrome scroll the
   page up about 300px to "clear" a sticky element that cannot move. A
   scroll-margin belongs to the element being revealed, so the bar is left out. */
@media (max-width: 991.98px) {
    .iob-brand .iob-content * {
        scroll-margin-top: 60px;
        scroll-margin-bottom: var(--tabbar-offset);
    }

    .iob-brand .iob-topbar {
        position: sticky;
        top: 0;
        z-index: 1020;
        transition: transform .2s ease;
    }

    .iob-brand.iob-bars-away .iob-topbar {
        transform: translateY(-100%);
    }
}

@media (max-width: 991.98px) {
    .iob-brand .iob-sidebar {
        flex-basis: auto;
        width: 280px;
        border-right: 0;
    }

    .iob-brand .iob-content {
        padding: var(--space-4) var(--space-3);
    }
}

/* THE PHONE TAB BAR (Site.master, after <main>). Four sections along the bottom
   below lg, where the sidebar is a panel behind the hamburger.

   --tabbar-offset is how much of the bottom of the viewport the bar takes. It is
   0 at lg and up, so the two sticky bars that pin to bottom: 0 below md -- the
   form action bar (.iob-actionbar) and the calendar's view switcher -- can sit
   on top of it at every width with one declaration and no media query of their
   own. Without it both were pinned underneath the tab bar, out of reach.

   THE HOME INDICATOR. The bar pads itself with env(safe-area-inset-bottom),
   34pt on a Face ID iPhone, so its icons and labels sit above the swipe-home
   line the way LinkedIn's and Amazon's do. That needs viewport-fit=cover in
   Site.master: without it the home-screen app still laid the page out to the
   bottom edge but reported an inset of 0, and the labels ended 5pt above the
   line (iPhone 15 Pro Max, 18 Oct 2026). 54px of row plus the inset is about
   89pt, against Apple's own 49 + 34; it was 66 + 0.

   THE BAR SLIDES DOWN WHILE YOU SCROLL DOWN, with the top bar (.iob-bars-away,
   set by the script in Site.master). Two variables because two things follow
   it and one must not: --tabbar-offset goes to the home indicator's inset while the bar is away, so
   the sticky bars resting on it drop to just above it instead of floating over a
   gap; --tabbar-space does not, because it is the page's bottom padding,
   and changing it mid-scroll changes the page's length under your finger --
   which near the end of a page moves the scroll position, which toggles the
   bar again. */
.iob-brand {
    --tabbar-height: 54px;
    --tabbar-offset: 0px;
    --tabbar-space: 0px;
}

.iob-brand .iob-tabbar {
    display: none;
}

@media (max-width: 991.98px) {
    /* + 1px for the bar's border-top, which sits outside the links' height. */
    .iob-brand {
        --tabbar-space: calc(var(--tabbar-height) + 1px + env(safe-area-inset-bottom, 0px));
        --tabbar-offset: var(--tabbar-space);
    }

    /* THE OTHER EDGES. viewport-fit=cover lets the page run under the camera
       cutout as well, which on an iPhone held sideways (932pt wide, still below
       lg) is the left or right edge: env() gives about 59pt there. The bars
       keep their background to the edge and move only what is in them; the
       content and the panel move in by the same amount. The top inset is 0
       from the home screen (the status bar keeps its own strip) and in Safari,
       and is written in for any case where it is not. All four are 0 on
       anything without a cutout, so none of this changes a desktop or iPad. */
    .iob-brand .iob-topbar {
        padding-top: calc(var(--space-2) + env(safe-area-inset-top, 0px));
        padding-left: calc(var(--space-3) + env(safe-area-inset-left, 0px));
        padding-right: calc(var(--space-3) + env(safe-area-inset-right, 0px));
    }

    .iob-brand .iob-tabbar {
        padding-left: env(safe-area-inset-left, 0px);
        padding-right: env(safe-area-inset-right, 0px);
    }

    .iob-brand .iob-content {
        padding-left: calc(var(--space-3) + env(safe-area-inset-left, 0px));
        padding-right: calc(var(--space-3) + env(safe-area-inset-right, 0px));
    }

    .iob-brand .iob-sidebar {
        padding-top: calc(var(--space-3) + env(safe-area-inset-top, 0px));
        padding-left: env(safe-area-inset-left, 0px);
        padding-bottom: calc(var(--space-3) + env(safe-area-inset-bottom, 0px));
    }

    /* The inset rather than 0, so a sticky bar left on screen (Save, the
       calendar's views) stops above the home indicator, not on it. */
    .iob-brand.iob-bars-away {
        --tabbar-offset: env(safe-area-inset-bottom, 0px);
    }

    /* 1030 is Bootstrap's own fixed-element layer: under the offcanvas panel
       and its backdrop (1045/1040), under modals, so an open menu or dialog
       covers the bar rather than the other way round. */
    .iob-brand .iob-tabbar {
        display: block;
        position: fixed;
        left: 0;
        right: 0;
        bottom: 0;
        z-index: 1030;
        padding-bottom: env(safe-area-inset-bottom, 0px);
        background-color: var(--surface-nav);
        border-top: 1px solid var(--border-hairline);
        transition: transform .2s ease;
    }

    /* 100% of the bar's own box, which includes the safe-area padding, so it
       goes all the way off the bottom edge. */
    .iob-brand.iob-bars-away .iob-tabbar {
        transform: translateY(100%);
    }

    /* The sticky bars follow --tabbar-offset down and back up; the same
       duration as the bars so the three move as one. */
    .iob-brand .card-footer.iob-actionbar,
    .iob-brand .fc.fc-theme-bootstrap .fc-footer-toolbar {
        transition: bottom .2s ease;
    }

    /* On .iob-main, not .iob-content, so the gap is added once below whatever
       the page puts at the end, including its own bottom padding. The fixed
       --tabbar-space, not --tabbar-offset: see the note at the top. */
    .iob-brand .iob-main {
        padding-bottom: var(--tabbar-space);
    }

    /* The four sidebar items the tab bar already carries, out of the panel so
       it does not list them a second time. Same breakpoint as the bar itself,
       so there is no width at which an item is in neither place. */
    .iob-brand .iob-sidebar .iob-nav__tabbed {
        display: none;
    }

    /* With those gone the first group is Resources, Calendar and Reports (fewer
       where a module is off), and the rule above Settings and Help divided a
       short list from one or two items. Up to five short items read as one
       list without it. The rail at lg keeps the rule, where it still
       separates seven sections from the tools. */
    .iob-brand .iob-sidebar .iob-nav--tools {
        margin-top: 2px;
        padding-top: 0;
        border-top: 0;
    }
}

.iob-brand .iob-tabbar__list {
    display: flex;
    list-style: none;
    margin: 0;
    padding: 0;
}

    /* Equal shares whatever is showing. Messages is a module switch and Status
       board is hidden from guests, so the bar can be anything from two items
       to four; flex: 1 1 0 spreads what is left rather than leaving holes. min-width: 0 lets a label ellipsize instead
       of pushing its neighbours. */
    .iob-brand .iob-tabbar__list > li {
        flex: 1 1 0;
        min-width: 0;
    }

/* 11px labels, not the 13px meta size. iOS's own tab bar labels are 10px.
   Four items get 80px each at 320px, the narrowest phone still in use, and
   the longest label, "Status Board", is about 62px at 11px (measured 18 October
   2026; with six items it had to be "Board", and Resources and Messages
   still ellipsized at 320px). The icon is what people scan for; the label is there to confirm it. */
.iob-brand .iob-tabbar__link {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 4px;
    min-height: var(--tabbar-height);
    padding: var(--space-1) 2px;
    color: var(--text-muted);
    font-size: 0.6875rem;
    line-height: 1.2;
    text-decoration: none;
    -webkit-tap-highlight-color: transparent;
}

    .iob-brand .iob-tabbar__link .fa {
        font-size: 1.5rem;
    }

    .iob-brand .iob-tabbar__label {
        max-width: 100%;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    .iob-brand .iob-tabbar__link:active {
        background-color: var(--surface-sunken);
    }

/* The rail's active marker turned on its side: the same cyan edge, along the
   top of the item instead of down its left. The text goes to --text-strong
   because the cyan cannot carry text (2.53:1), same as in the rail. */
.iob-brand .iob-tabbar__list > li.active > .iob-tabbar__link {
    color: var(--text-strong);
    font-weight: 500;
    box-shadow: inset 0 3px 0 var(--accent);
}

/* LOADING. js/nav-busy.js puts .is-active on this when a tab bar or sidebar
   link is tapped, so the tap shows at once even when the next page takes a
   moment on the server. A 3px strip along the very top of the screen, above
   the top bar, the offcanvas panel and its backdrop (a tap in the panel shows
   it too). The moving segment is a transform, which keeps running while the
   browser waits for the next page; left or width would freeze. */
.iob-brand .iob-navprogress {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    height: 3px;
    z-index: 1070;
    overflow: hidden;
    pointer-events: none;
    visibility: hidden;
}

    .iob-brand .iob-navprogress.is-active {
        visibility: visible;
    }

.iob-brand .iob-navprogress__track {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    width: 40%;
    background-color: var(--accent);
    transform: translateX(-100%);
}

    .iob-brand .iob-navprogress.is-active .iob-navprogress__track {
        animation: iob-navprogress 1.1s ease-in-out infinite;
    }

@keyframes iob-navprogress {
    from {
        transform: translateX(-100%);
    }

    to {
        transform: translateX(250%);
    }
}

/* Less motion: a still, full-width strip. The spinner on the tapped item
   stops as well (Font Awesome's own .fa-spin rule), leaving a still ring. */
@media (prefers-reduced-motion: reduce) {
    .iob-brand .iob-navprogress__track {
        width: 100%;
        transform: none;
    }

    .iob-brand .iob-navprogress.is-active .iob-navprogress__track {
        animation: none;
    }
}

/* The button that sent a post, while it is on its way (nav-busy.js). Dimmed
   and deaf to another tap rather than disabled: a disabled button is left out
   of the post, and WebForms needs it there to know which one was clicked. */
.iob-brand .btn.is-busy {
    pointer-events: none;
    opacity: .75;
    cursor: progress;
}

@media print {
    .iob-brand .iob-navprogress {
        display: none;
    }
}

/* NO ZOOM ON FOCUS. iOS Safari zooms the page in when you tap into a field
   whose text is under 16px, and leaves it zoomed until you pinch back out.
   The small controls are 14px (0.875rem): the Sort, Rows and Refresh selects
   on the three status board views and Resources, the Calendar and Category
   filters on Calendar and Home, and the confirmation code on text-alert
   setup. 16px from lg down, the same line the tab bar uses; the control grows
   about 3px taller, which is no loss at thumb size. Desktop keeps the compact
   size. Not user-scalable=no on the viewport, which would stop the zoom by
   stopping everyone from zooming at all (WCAG 1.4.4). */
@media (max-width: 991.98px) {
    .iob-brand .form-control-sm,
    .iob-brand .form-select-sm {
        font-size: 1rem;
    }
}

/* No slide for anyone who has asked for less motion, and none for the one
   change the script needs to be instant (the Tab key bringing the bars back:
   see showNow in Site.master). */
@media (max-width: 991.98px) and (prefers-reduced-motion: reduce) {
    .iob-brand .iob-topbar,
    .iob-brand .iob-tabbar,
    .iob-brand .card-footer.iob-actionbar,
    .iob-brand .fc.fc-theme-bootstrap .fc-footer-toolbar {
        transition: none;
    }
}

.iob-brand.iob-bars-instant .iob-topbar,
.iob-brand.iob-bars-instant .iob-tabbar,
.iob-brand.iob-bars-instant .card-footer.iob-actionbar,
.iob-brand.iob-bars-instant .fc.fc-theme-bootstrap .fc-footer-toolbar {
    transition: none !important;
}

@media print {
    .iob-brand .iob-tabbar {
        display: none;
    }

    .iob-brand .iob-main {
        padding-bottom: 0;
    }
}

/* --- Surfaces -------------------------------------------------------------

   ONE HAIRLINE, NO RESTING SHADOW. The previous version of this rule gave every
   card a border AND a soft shadow, copied from the marketing site where a card
   sits alone in a section band. In an application where 386 of them sit in
   grids and inside one another, two elevation cues at once is most of why the
   surfaces read as heavy and slightly old. The border does the work; the shadow
   is now reserved for things that genuinely float, which is what makes it mean
   something when you see one.

   10px rather than 12px for the same reason: a dense grid of small cards with a
   large radius reads as a pile of pills. */
.iob-brand .card {
    border: 1px solid var(--border-hairline);
    border-radius: var(--radius-card);
    box-shadow: none;
}

/* The card header was #f5f6f7, uppercase, and 500 weight -- a grey band with
   shouting in it, on 85 pages. Now it is part of the card: white, a hairline
   under it, and the words as they are written in the markup. 500 rather than
   600, because at this size the heavier weight was doing the shouting the
   uppercase used to do. */
.iob-brand .card-header {
    background-color: var(--surface-card);
    color: var(--text-strong);
    border-bottom: 1px solid var(--border-hairline);
    font-size: var(--font-size-section);
    font-weight: 500;
    text-transform: none;
    letter-spacing: 0;
    padding: var(--space-3) var(--space-4);
}

.iob-brand .card-footer {
    background-color: var(--surface-card);
    border-top: 1px solid var(--border-hairline);
}

.iob-brand .card-body {
    padding: var(--space-4);
}

/* Numbers that sit in a column -- every time, count and date on a board -- are
   lined up by figure rather than by luck. Inter ships tabular figures; without
   this they are proportional, so a column of clock times jitters as the minutes
   change and the eye has to re-find the left edge on every scan. */
.iob-brand .sb-board,
.iob-brand table,
.iob-brand .pagination {
    font-variant-numeric: tabular-nums;
}

/* --- The board, at the width a sidebar leaves it --------------------------

   The shell costs the content 232px, and the board spends its width on ten
   equal columns. Bootstrap's default gutter is 24px per column, which on ten
   columns is 240px of nothing -- more than the sidebar took. Halving it gives
   most of that back, and is why the columns stopped wrapping mid-name. */
.iob-brand .sb-board > [role="row"] {
    --bs-gutter-x: 0.75rem;
}

/* Column widths, and why the board is a CSS table at md and up.

   It is markup with role="table" laid out as a stack of independent flex rows,
   and that is exactly the problem: a flex row sizes its columns from its own
   content and knows nothing about the row above it. Every cell was .col-md --
   flex: 1 0 0% -- but a flex item will not shrink below its min-content width,
   so one long job title or a nowrap phone number widened that cell and squeezed
   its neighbours, in that row only. Measured across the rows of a board with
   every column on, the left edge of a column drifted by up to 49px, and the
   drift accumulated left to right: by Last Update nothing lined up with
   anything. A table is a table.

   min-width: 0 was the cheaper fix and is the wrong one -- it takes the drift
   down to 5px but leaves 19 cells whose nowrap content then spills out of the
   box it was given. display: table costs nothing and is exact: 0px of drift,
   nothing spilling, and the browser doing the column balancing it was built to
   do.

   The percentages below are hints, not a grid. Under table-layout: auto the
   browser treats them as preferences and normalises whatever it is given, so
   they hold their proportions for any subset the Columns modal leaves on --
   which is the same reason these were flex weights before, and the reason
   neither can be col-md-N. Comments takes the largest share because it is the
   only column holding a sentence. */
@media (min-width: 768px) {
    /* :not(.sb-board--grid) -- an opt-out from the TABLE, not from the board.
       .sb-board carries two things: the phone layout further up this file, and
       this desktop table. The status history on a resource wants the first and
       not the second: its desktop grid was already right, and it has a
       full-width comments row that a table turns into a narrow extra column
       instead of the line of its own it has always had.

       Excluding it here rather than reverting it there, because revert rolls
       back to the user agent and not to Bootstrap -- it took .row from flex to
       block and stacked every cell. Never matching is cleaner than undoing. */
    .iob-brand .sb-board:not(.sb-board--grid) {
        display: table;
        width: 100%;
    }

    .iob-brand .sb-board:not(.sb-board--grid) > [role="row"] {
        display: table-row;
        margin: 0;
    }

        /* :not(.sb-disclosure) so this rule does not claim the disclosure cell.
           It is hidden on desktop by the rule below, and adding :not() above
           lifted this selector to five classes -- enough to outweigh that rule
           and leave the board an empty extra column. Excluding it here is the
           fix that does not depend on counting specificity twice. */
        .iob-brand .sb-board:not(.sb-board--grid) > [role="row"] > [role="cell"]:not(.sb-disclosure),
        .iob-brand .sb-board:not(.sb-board--grid) > [role="row"] > [role="columnheader"]:not(.sb-disclosure) {
            display: table-cell;
            vertical-align: top;
            /* .col-12 sets width: 100%. On a table-cell that is a 100% width
               hint from every column at once, which is no hint at all. */
            width: auto;
            flex: none;
            padding: var(--space-1) var(--space-2);
        }

        /* The disclosure button belongs to the phone layout only. */
        .iob-brand .sb-board > [role="row"] > .sb-disclosure {
            display: none;
        }

        /* THE ROW LINES WENT WHEN THE BOARD BECAME A TABLE. Every row still
           carries .border-top (the header .border-bottom), but a table-row in
           the separate border model -- the default -- paints no border at all,
           so List and People have shown one unbroken block since. Resource
           Search hit the same thing and moved its lines onto the cells (see
           .rs-board below); this is that fix for the board, so the three
           boards rule their rows the same way.

           The row's own border is left alone. Bootstrap's .border-top is
           !important, so a border: 0 here would lose -- the one .rs-board
           carries does -- and in the separate model it paints nothing to
           remove. Measured in Chrome: one hairline per row, not two. */
        .iob-brand .sb-board:not(.sb-board--grid) > [role="row"].border-bottom > [role] {
            border-bottom: 1px solid var(--border-hairline);
        }

        .iob-brand .sb-board:not(.sb-board--grid) > [role="row"].border-top > [role] {
            border-top: 1px solid var(--border-hairline);
        }

        /* Same specificity as the width: auto above, and after it, or the base
           rule wins and every hint here is silently ignored.

           :not(.sb-board--grid), matching the opt-out two rules up: these are
           table-layout column-width HINTS (see the note above them), meaningless
           outside table-layout, but the selector was never given the same
           exclusion. .sb-col-name also names the Name column on the three
           --grid Setup boards (User Status Codes, Resource Status Codes,
           Calendar Categories), and (0,4,0) beats a bare .col-md-N's (0,1,0) --
           so this 15% hint was silently overruling whatever column width those
           pages actually asked for, on a board where "hint, not a grid" was
           never true to begin with. */
        .iob-brand .sb-board:not(.sb-board--grid) > [role="row"] > .sb-col-comments {
            width: 20%;
        }

        .iob-brand .sb-board:not(.sb-board--grid) > [role="row"] > .sb-col-name {
            width: 15%;
        }

        .iob-brand .sb-board:not(.sb-board--grid) > [role="row"] > .sb-col-status,
        .iob-brand .sb-board:not(.sb-board--grid) > [role="row"] > .sb-col-title {
            width: 12%;
        }

        .iob-brand .sb-board:not(.sb-board--grid) > [role="row"] > .sb-col-time {
            width: 10%;
        }

        .iob-brand .sb-board:not(.sb-board--grid) > [role="row"] > .sb-col-phone {
            width: 8%;
        }

        /* The photograph, on /statusboard/people/ only. Same 1% trick as the
           action column: it carried no hint at all, and under table-layout: auto
           an un-hinted column is handed everything the hinted ones leave. With
           the default column set that was about a third of the board -- a 75px
           image in a 320px cell, and a gap between it and the name wide enough
           to read as a missing column.

           :not(.sb-board--grid) for the same reason as the other column-width
           hints above: ResourceTypes.aspx used to give its photo cell this
           same class on a grid board, with a real width of its own from
           col-1 -- 1% overruled that, squeezing the cell to a sliver and
           leaving the 75px photo overflowing across the Name column next to
           it. That page has since moved its photo to a plain col-md-3 cell
           of its own instead of reusing this class, but the guard stays:
           the next grid board to add a photo column would hit the exact
           same bug without it. */
        .iob-brand .sb-board:not(.sb-board--grid) > [role="row"] > .sb-col-photo {
            width: 1%;
        }

        /* Two icons wide. 1% plus nowrap is how you ask a TABLE for "as narrow
           as the content and no narrower" -- the browser resolves it against
           the other columns' demands. Say it to a flex item and you get 1% of
           the row and nothing else, which is what a --grid board is: /setup/
           history/ opts out of the table above, so its action cell came out
           12px and the Edit button inside it lay across the Status column.

           Hence :not(.sb-board--grid), the same opt-out the table rules carry,
           and for the same reason: this is a table idiom. A grid board's action
           cell keeps its col-md-* width, which is a real width. */
        .iob-brand .sb-board:not(.sb-board--grid) > [role="row"] > .sb-col-action {
            width: 1%;
            white-space: nowrap;
        }
}

/* The status chip fills its cell, at every width.

   It was inline-block below md until 22 September, and the reasoning for that
   was sound when it was written and has since expired. The original fault was a
   block chip in a row that sized itself from its own content: "In" became a
   slab as wide as the longest status on screen, and two lines tall when the
   cell was squeezed. inline-block fixed the slab by giving up on the column,
   and the note here said there was no column to fill below md because the cells
   size to their content.

   THAT IS NO LONGER TRUE OF LIST. Its status cell is a fixed 7rem -- see
   .sb-col-status:first-child in iob.css, which pins it so the names start at
   the same x on every row. So there was a column, 112px of it, and the chip was
   hugging its label inside it: a ragged left edge of coloured blocks against a
   straight one on the desktop table. Reported from a phone. A block chip makes
   it the same column of identical bands the desktop has, and cannot grow taller
   than the text already makes it, because the wrapping is decided by the cell.

   On People and on the resource board the cells still DO size to their content,
   so this changes nothing there -- measured, the page is the same height to the
   pixel. Their chips stay ragged because their cells are, and that is the trade
   iob.css records: a fixed status column on People takes wrapped names from 2
   of 20 to 8 and the page from 1,682px to 1,761px, because a 20px "In" would
   reserve the same room as "Working From Home" on every row.

   The colour is still the customer's own setting from dbo.SystemColors, inline,
   untouched. */
.iob-brand .sb-board [role="cell"] > .rounded {
    display: block;
}

/* A phone number is one token; tabular figures make it wide enough to wrap at
   the hyphen, which reads as two numbers. Same for a timestamp, which wraps
   after the date and leaves "AM" alone on the next line.

   .sb-col-time covers Return Time and Last Update. It used to be
   [role="cell"]:last-child:not([class*="col-md-"]) -- Last Update has no element
   of its own, it is text in the cell, so the only hook was its position. Now
   that the columns carry weight classes it has a name, and the rule says what
   it means. */
.iob-brand .sb-board a[href^="tel:"],
.iob-brand .sb-board .sb-col-time {
    white-space: nowrap;
}

/* --- Buttons --------------------------------------------------------------

   SHAPE ONLY, WITH ONE EXCEPTION BELOW. iob.css gives .btn 0.84rem/2.14rem of
   padding, 0.81rem type, a 2px radius and UPPERCASE -- the Material-era button,
   and the reason a row of eight calendar controls could not fit a phone. This
   is the marketing site's button at application density: 8px radius, sentence
   case, 0.9375rem at 600, and padding that leaves a 38px control rather than a
   52px one.

   The margin iob.css puts on .btn is deliberately NOT reset. Layouts across
   the application lean on that 6px for the gap between adjacent buttons, and
   removing it here would close up button rows on pages nobody has looked at. */
/* THE TRANSPARENT BORDER IS LOAD-BEARING. iob.css sets .btn { border: 0 },
   which is why a filled button and a bordered one in the same row were never
   the same height: the bordered families below add 1px and the filled ones had
   nothing, so every Apply/Clear, Save/Cancel and Primary/Secondary pair sat 2px
   apart. Bootstrap's own .btn carries a transparent border for exactly this
   reason and iob.css zeroed it. Restoring it here makes every button in the
   application one box model, and the families below change only the colour of
   a border that is already there. */
.iob-brand .btn {
    padding: 0.4375rem 0.875rem;
    font-size: var(--font-size-dense);
    font-weight: 500;
    line-height: 1.5;
    text-transform: none;
    border: 1px solid transparent;
    border-radius: var(--radius-control);
}

.iob-brand .btn.btn-sm {
    padding: 0.3rem 0.7rem;
    font-size: 0.8125rem;
}

.iob-brand .btn.btn-lg {
    padding: 0.7rem 1.5rem;
    font-size: 1.0625rem;
}

/* THE PRIMARY ACTION IS BOOTSTRAP BLUE. 107 buttons.

   It has now been four colours: #4267b2 (Facebook blue, inherited), then
   --brand-darker #00719c, then --navy, now Bootstrap 5's own $primary #0d6efd.
   Navy was chosen because a saturated mid-tone cyan on every primary action, on
   a dense screen that already carries customer-chosen status colours, competed
   with the data; it solved that by receding so far that the one action on a
   page stopped reading as an action at all. The framework blue is the
   conventional answer -- it is what an ASP.NET application on Bootstrap 5 looks
   like, and it is what every un-skinned control on these pages already uses --
   and it still leaves the cyan free to be the accent: the active marker in the
   sidebar, the selected row, the focused control.

   IT IS NOT --brand AND MUST NOT BECOME --brand. White on #00aeef is 2.53:1.
   The marketing site carries that on its own buttons knowingly -- its own
   accessibility page records the figure -- and this application cannot, because
   AccessibilityBacklog closed 1.4.3 on "every colour surface in App/ now meets
   4.5:1".

   #0d6efd IS 4.54:1 AND THAT IS THE WHOLE MARGIN. It clears the 4.5:1 floor by
   0.04, so this button no longer has the slack navy gave it. Do not lighten it,
   do not put it behind an opacity, and do not reach for Bootstrap 4's #007bff,
   which looks near-identical and is 3.98:1 -- a fail. The token comment on
   --action-primary says the same thing; both are there because the difference
   is invisible by eye.

   Hover and active go darker, never lighter: --blue-dark is 5.84:1 and
   --blue-darker 6.47:1. Every button family in iob.css was corrected the same
   way after every one of them lightened on hover and so failed hardest at the
   moment of interaction. Keep the direction.

   No !important anywhere: iob.css's .btn-primary colour rules were deleted in
   the reconciliation, so these win on specificity over Bootstrap's .btn-primary
   (0,2,0 against 0,1,0) and its .btn:hover (0,3,0 against 0,2,0). */
.iob-brand .btn-primary,
.iob-brand .btn-primary.dropdown-toggle {
    background-color: var(--action-primary);
    color: var(--white);
}

    .iob-brand .btn-primary:hover,
    .iob-brand .btn-primary.dropdown-toggle:hover {
        background-color: var(--action-primary-hover);
    }

    .iob-brand .btn-primary:focus,
    .iob-brand .btn-primary:active,
    .iob-brand .btn-primary.active {
        background-color: var(--action-primary-active);
    }

/* --- Four roles, mapped onto the families the markup already uses ----------

   The complaint that started this was "the button colours are questionable",
   and the honest diagnosis was that there was no system: the Users page alone
   showed five action colours -- teal, Material green, a third blue, magenta and
   teal pills -- with nothing saying which mattered. Colour was decorating the
   buttons instead of ranking them.

   There are now four roles. Rather than rewrite 200+ markup sites, the existing
   families are re-pointed onto them here, which is reversible and keeps the
   diff readable:

     primary    blue      .btn-primary, .btn-blue        the one action
     secondary  white     .btn-light, .btn-white,
                          .btn-green, .btn-secondary      everything ordinary
     destructive red      .btn-danger, .btn-red           deletes
     quiet      text      .btn-unstyled, .btn-link        inline actions

   .btn-green going white is deliberate and is the change most likely to be
   questioned. It is "Export to CSV" on fifteen report pages and "Import users"
   -- ordinary actions that were wearing a colour that means "go". Nothing about
   exporting a CSV needs to be the loudest thing on the page. */
.iob-brand .btn-light,
.iob-brand .btn-white,
.iob-brand .btn-green,
.iob-brand .btn-secondary {
    background-color: var(--surface-card);
    border: 1px solid var(--border-control);
    color: var(--text-strong);
}

    .iob-brand .btn-light:hover,
    .iob-brand .btn-white:hover,
    .iob-brand .btn-green:hover,
    .iob-brand .btn-secondary:hover {
        background-color: var(--surface-sunken);
        border-color: var(--gray-600);
        color: var(--text-strong);
    }

/* Secondary, but the way into a row's own data: Members on Settings > Teams
   and User and Team access on Company Calendars. As white buttons they were
   lost down a column of rows (Chris, 7 Oct 2026), so they are outlined in the
   primary blue -- clearly a button, still below the page's one solid blue
   action. Blue on white is 4.5:1. Not .btn-outline-primary: the theme's
   version of it is not part of the four roles above. */
.iob-brand .iob-btn-outline {
    background-color: var(--surface-card);
    border: 1px solid var(--action-primary);
    color: var(--action-primary);
}

    .iob-brand .iob-btn-outline:hover,
    .iob-brand .iob-btn-outline:focus {
        background-color: var(--action-primary);
        border-color: var(--action-primary);
        color: #fff;
    }

/* The two controls in the board footer: Records to display, and Auto-refresh.
   Both are a label over a select. They used to be d-flex flex-wrap
   align-items-center, which stacks the label only when the container happens
   to be too narrow for one line -- so Auto-refresh stacked and Records to
   display did not, and which one you got depended on the viewport. Stacked on
   purpose now, so the pair reads as a pair at every width. */
.iob-brand .sb-footctl {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    align-items: flex-start;
}

    .iob-brand .sb-footctl label {
        margin: 0;
    }

/* The board's view switcher: People / List / Card. All three links have
   carried .active on the one you are looking at since long before this pass,
   but nothing in the brand layer drew it, so the view you were on looked
   exactly like the two you were not.

   A fill and the weight, plus the accent as an inset underline -- the same
   "you are here" the sidebar uses, in the shape a segmented control wants.
   The fill alone would not do: .btn-white:hover already uses it, so hovering
   a sibling would make two buttons look selected at once.

   .active on an <a> is a class and nothing more, so the markup also carries
   aria-current="page". Without it the state is only visible to people who
   can see it. */
.iob-brand .btn-group > .btn-white.active,
.iob-brand .btn-group > .btn-light.active {
    background-color: var(--surface-sunken);
    border-color: var(--gray-600);
    color: var(--text-strong);
    font-weight: 600;
    box-shadow: inset 0 -2px 0 var(--accent);
    z-index: 1;
}

.iob-brand .btn-blue {
    background-color: var(--action-primary);
    color: var(--white);
}

    .iob-brand .btn-blue:hover {
        background-color: var(--action-primary-hover);
    }

/* Destructive. #b42318 is 6.05:1 with white -- the marketing site's error red,
   and darker than the #dc3545 this application used, which passed at 4.53:1
   with 0.03 of headroom. Hover darker again. */
.iob-brand .btn-danger,
.iob-brand .btn-red {
    background-color: #b42318;
    color: var(--white);
}

    .iob-brand .btn-danger:hover,
    .iob-brand .btn-red:hover {
        background-color: #912018;
    }

/* A BUTTON THAT IS AN ANCHOR IS STILL A BUTTON.

   asp:LinkButton renders an <a>, and so does every <a class="btn"> in the
   markup -- the Delete in the message dialog, Update Status and Edit on a
   resource, and around ninety others. The resting state was fine, because
   .iob-brand .btn-danger (0,2,0) beats .iob-brand a (0,1,1). Hover was not:
   .iob-brand a:hover is (0,2,1), the filled families set only a background on
   hover, and so the label was repainted the LINK colour on top of the button's
   own fill. On the Delete dialog that is #084298 on #912018 -- 1.08:1, a label
   you cannot read.

   The three bordered families were never affected: .btn-light and the outline
   and icon rules already name a colour in their hover rules, at (0,3,0), which
   outranks the link. These selectors do the same thing for the filled ones.

   WHY NOT EXCLUDE BUTTONS FROM THE LINK RULE INSTEAD, which is the obvious
   fix: .iob-brand a:not(.btn) is (0,2,1) at rest and (0,3,1) on hover, and
   both outrank .iob-brand .sort-link (0,2,0) and .sort-link:hover (0,3,0).
   The sort links inherit their colour on purpose and would have turned blue.
   Raising the link rule's specificity breaks quieter things than it fixes.

   Nothing here changes a background. Each selector restates the white the
   resting rule already sets, so hover and focus keep the colour they always
   should have had: 8.66:1 on the red, 5.84 and 6.44 on the two blues. */
.iob-brand .btn-primary:hover,
.iob-brand .btn-primary:focus,
.iob-brand .btn-primary:active,
.iob-brand .btn-primary.active,
.iob-brand .btn-primary.dropdown-toggle:hover,
.iob-brand .btn-blue:hover,
.iob-brand .btn-blue:focus,
.iob-brand .btn-danger:hover,
.iob-brand .btn-danger:focus,
.iob-brand .btn-red:hover,
.iob-brand .btn-red:focus {
    color: var(--white);
}

/* The outline family becomes the quiet bordered button rather than a 2px slab.

   IT SETS NO VERTICAL PADDING, AND MUST NOT. It used to pin padding-top and
   padding-bottom to 0.4375rem, the full-size value. iob.css sizes outline
   buttons with a four-rung ladder -- base, lg, md, sm -- and this rule replaced
   only the base rung, so every .btn-sm outline button silently took full-size
   padding: same specificity as .iob-brand .btn.btn-sm, further down the file,
   so it won. That is what made Clear 5.75px taller than Apply in the status
   board filters. With the padding gone, the ordinary .btn / .btn-sm / .btn-lg
   rules above apply and the sizes line up on their own. */
.iob-brand .btn[class*="btn-outline-"] {
    border-color: var(--border-control);
    background-color: transparent;
    color: var(--text-body);
}

    .iob-brand .btn[class*="btn-outline-"]:hover {
        background-color: var(--surface-sunken);
        border-color: var(--gray-600);
        color: var(--text-strong);
    }

/* Icon-only row actions. These were filled magenta and teal pills, one pair per
   row, 22 rows deep -- the loudest thing on a page whose job is a list of
   people. A quiet square that colours on hover reads as an action without
   competing with the row it belongs to. */
.iob-brand .btn.iob-btn-icon {
    background-color: transparent;
    border: 1px solid transparent;
    color: var(--text-muted);
    padding: var(--space-1) var(--space-2);
}

    .iob-brand .btn.iob-btn-icon:hover {
        background-color: var(--surface-sunken);
        border-color: var(--border-hairline);
        color: var(--text-strong);
    }

/* --- Form controls --------------------------------------------------------

   Bootstrap's density, this file's border, radius and focus ring. The ring is
   --brand-darker for the reason recorded on the sign-up fields above: cyan at
   2.53:1 on a white field is too faint to show where you landed. */
.iob-brand .form-control,
.iob-brand .form-select {
    border-color: var(--border-control);
    border-radius: var(--radius-sm);
}

    .iob-brand .form-control:focus,
    .iob-brand .form-select:focus {
        outline: 2px solid var(--brand-darker);
        outline-offset: 1px;
        border-color: var(--brand);
        box-shadow: none;
    }

/* A ticked box is Bootstrap's checked blue, which is --action-primary: the
   same value the framework sets and the same one .btn-primary uses, so the
   Save button and the boxes above it in a dialog agree. It was --brand-darker,
   the cyan, from when the cyan was carrying every interactive state; the tick
   itself is Bootstrap's white SVG, at 4.50:1 on the fill and comfortably past
   the 3:1 that 1.4.11 asks of a graphical object. */
.iob-brand .form-check-input:checked {
    background-color: var(--action-primary);
    border-color: var(--action-primary);
}

/* An empty box needs its edge to be seen as well (1.4.11, 3:1 for a control's
   boundary). Bootstrap draws it in --bs-border-color, #dee2e6, which measured
   1.30:1 on white, so an unticked box was close to invisible; this showed on the
   Users list's row checkboxes (7 Oct 2026) and was true of every checkbox and
   radio in the app. The text fields' --border-control instead. */
.iob-brand .form-check-input:not(:checked) {
    border-color: var(--border-control);
}

/* Field labels. .form-label is the Bootstrap hook and Bootstrap gives it
   margin-bottom: .5rem and nothing else -- no size, no colour, no weight. The
   typography is decided here rather than inherited, because inheriting means
   16px body text, which is not a label, it is a sentence.

   14px at 500 is the size these were reaching for. They were .dataLabel at 80%,
   which is 12.8px, and the sign-up page had already written off that size in
   these words: "read as a thin caption beside a 53px field". This is a real
   label without becoming body copy.

   4px below, not Bootstrap's 8. The forms in this product are dense and were
   built with the label sitting on the field; 8px on several hundred of them
   loosens every form in the app by a line. */
.iob-brand .form-label {
    font-size: var(--font-size-dense);
    font-weight: 500;
    color: var(--text-body);
    margin-bottom: var(--space-1);
}


/* THE BAR OVER A GROUP OF FIELDS. "Company Information", "Where are you?",
   "Message Details" -- 46 of them across 17 pages, every one written as
   <div class="blue lighten-5 rounded p-1">, three MDB-era utilities doing the
   job of one name. .blue.lighten-5 was a shim in iob.css carrying Material's
   blue 50 (#e3f2fd) behind an !important.

   NOT .alert.alert-info, which is the other way this could have gone. Three
   reasons it did not:

   .alert already means something in this product -- a rejected sign-in, a
   throttled attempt, a failed reCAPTCHA -- and some of those are wired as live
   regions. Teaching it to also mean "section heading" in 45 places leaves a
   genuine info alert on ApplicationSettings or UserEdit looking exactly like a
   heading on the same page, and those are the pages that would want both.

   Bootstrap's .alert is padding 1rem on both axes, margin-bottom 1rem and a
   1px border. These are dense strips with none of that. Overriding padding,
   margin and border leaves only the background colour coming from Bootstrap,
   which is a custom class wearing Bootstrap's name.

   Several of these wrap an asp:Label with AssociatedControlID, so the element
   contains a real <label for>. Calling it an alert invites someone to add
   role="alert" later and announce a form label as a live region.

   The COLOUR and now the whole box come from Bootstrap. This rule is exactly
   what "alert alert-info rounded p-1" computes to, written out longhand:

     .alert            position, padding 1rem, margin-bottom 1rem, border,
                       border-radius, and the four --bs-alert-* variables
     .alert-info       points those variables at the info tokens
     .rounded          border-radius: var(--bs-border-radius) !important
     .p-1              padding: .25rem !important  -- beats .alert's 1rem

   Every value below is a Bootstrap variable rather than a number, so this
   tracks .alert-info through theme changes and picks up its dark-theme values
   without a second rule here. Verified by computed-style diff against a live
   <div class="alert alert-info rounded p-1">: all properties equal. */
.iob-brand .iob-formsection {
    position: relative;
    padding: 0.25rem;
    margin-bottom: 1rem;
    color: var(--bs-info-text-emphasis);
    background-color: var(--bs-info-bg-subtle);
    border: var(--bs-border-width) solid var(--bs-info-border-subtle);
    border-radius: var(--bs-border-radius);
}

/* THE ASIDE THAT TALKS YOU OUT OF SOMETHING. "You may want to consider
   changing the user's Account Status to Inactive instead", "Privacy: this is
   only visible to managers", the Return Time explanations, Default Calendar
   Access, Subscribe Now. Seven of them, and they were written six different
   ways: .yellow.lighten-4 five times, .yellow.lighten-3 once -- a second shade
   doing the identical job -- with rounded, p-1, p-2, small and mt-3 sprinkled
   on in whatever order. Both shades were MDB shims in iob.css sitting behind
   an !important, and both are deleted now.

   The colour comes from Bootstrap's warning tokens, the same ones .alert-warning
   uses, so it tracks the theme and has a dark-theme value already. #fff3cd
   rather than Material's #fff9c4: warmer, and one value instead of two.

   NOT .alert.alert-warning, though the case for it is better here than it was
   for .iob-formsection -- these really are notices, and nothing about calling
   them alerts would be a lie. It is density that decides it: .alert is padding
   1rem and margin-bottom 1rem, where these are 8px asides tucked against the
   field they qualify. Using it would have re-spaced all seven rather than
   renamed them.

   The text stays body colour rather than --bs-warning-text-emphasis. These are
   paragraphs with a <strong> lead, not badges; dark amber prose reads as a
   warning about itself. */
.iob-brand .iob-note {
    background-color: var(--bs-warning-bg-subtle);
    color: var(--text-body);
    border-radius: var(--radius-control);
    padding: var(--space-2);
}

/* --- Label and value, on the pages that show a record ---------------------

   These were <span class="dataLabel">Company</span><br /><span>value</span>:
   no programmatic relationship between the two, and a <br> holding them apart
   by the value's own line-height. Measured on /messages/view/, a label sat 19px
   from its value and 48px from the next pair -- fine until a value is empty,
   and then the orphan label sits 24px from the NEXT label. 24 against 19 is not
   a grouping, so "Company" read as belonging to "Priority" below it rather than
   to the nothing above it.

   A grid fixes the part CSS can fix. Two columns from sm up, so every value
   starts on the same line as its label and on the same left edge as every other
   value -- including a long one, which wraps inside its own column instead of
   running back to the margin, which is what an inline-block label could not do.
   One column below sm, where there is no room for two.

   The em dash is the part CSS cannot fix on its own: an asp:Label with no text
   still renders <span></span>, so the dd is not :empty. The inner element is,
   which is what the last two selectors catch. */
.iob-brand .iob-datalist {
    display: grid;
    grid-template-columns: 8.5rem 1fr;
    gap: var(--space-2) var(--space-3);
    margin: 0;
}

    /* HTML5 allows a <div> inside <dl> to group one dt with its dd, which is how
       a pair that the server shows or hides stays together -- hide the wrapper
       and both halves go. display: contents keeps the grid seeing dt and dd as
       its own items, and drops the wrapper from the accessibility tree, which is
       right: it carries no meaning of its own. */
    .iob-brand .iob-datalist > div {
        display: contents;
    }

    .iob-brand .iob-datalist dt {
        font-size: var(--font-size-dense);
        font-weight: 400;
        color: var(--text-muted);
        line-height: 1.55;
    }

    .iob-brand .iob-datalist dd {
        margin: 0;
        line-height: 1.55;
        min-width: 0;
    }

        /* "There is nothing here", rather than a gap you have to interpret. */
        .iob-brand .iob-datalist dd:empty::after,
        .iob-brand .iob-datalist dd > span:empty::after,
        .iob-brand .iob-datalist dd > a:empty::after {
            content: "\2014";
            color: var(--gray-300);
        }

@media (max-width: 575.98px) {
    .iob-brand .iob-datalist {
        grid-template-columns: 1fr;
        gap: 0;
    }

        /* Stacked, the pair has to hold together by spacing alone: nothing
           between a label and its value, a clear step before the next label. */
        .iob-brand .iob-datalist dt + dd {
            margin-bottom: var(--space-3);
        }
}

/* A label column sized to its own longest label, for a list in a card that is only
   ~350px wide (two cards side by side on the Payment Center at iPad width). The fixed
   8.5rem left "lisayale@hotmail.com" too little room and broke it mid-word, and a smaller
   fixed width wrapped "Billing frequency" onto two lines. The phone layout below sm is
   unchanged: one column. */
@media (min-width: 576px) {
    .iob-brand .iob-datalist--compact {
        grid-template-columns: max-content 1fr;
    }
}

/* Two label/value pairs per row, for a long list on a wide screen (the Details card on
   /resources/view/). Label columns size to their own longest label, as --compact does.
   A dd with .iob-datalist__wide takes the rest of the row, for a value that is prose --
   it must start a row, so put it after an even number of pairs. One column below lg. */
@media (min-width: 992px) {
    .iob-brand .iob-datalist--twocol {
        grid-template-columns: max-content 1fr max-content 1fr;
        column-gap: 2rem;
    }

        .iob-brand .iob-datalist--twocol dd.iob-datalist__wide {
            grid-column: 2 / -1;
        }
}

/* .dataLabel keeps the hundred places that are NOT form labels: the caption
   above a read-only value -- "Subject", "Manager", "Start" on the view pages.
   The name was always right for those; it was wrong for the three hundred
   labels that belonged to an input, and they are .form-label now. */
.iob-brand .dataLabel {
    color: var(--gray-600);
}

/* --gray-600 is a primitive, so it does not move with the theme the way a
   purpose token does -- left alone, a caption in dark mode would stay this
   same dark slate against a now-dark card. Overridden here rather than
   switching the rule above to --text-muted outright, which would also
   change the light-mode value this was measured against. */
[data-bs-theme="dark"] .iob-brand .dataLabel {
    color: var(--text-muted);
}

/* --- Modals and pagination ------------------------------------------------

   The modal header colours are left exactly as they are. Seven of them are
   contrast-tuned in iob.css, one of them with 0.02 of headroom, and there is
   nothing to gain by re-deriving that here. Only the shape changes. */
.iob-brand .modal-dialog .modal-content {
    border-radius: var(--radius);
}

    .iob-brand .modal-dialog .modal-content .modal-header {
        border-top-left-radius: var(--radius);
        border-top-right-radius: var(--radius);
    }

/* The roles and rights table in Setup > Users > edit. Each row heading is a
   sentence, so it is set at body weight rather than bold -- twelve bold
   sentences down the left would outweigh the answers. The four role columns
   hold a tick, a dash or two words; centred, each column reads straight down. */
.iob-brand .iob-roles-table th[scope="row"] {
    font-weight: normal;
}

.iob-brand .iob-roles-table td,
.iob-brand .iob-roles-table thead th:not(:first-child) {
    text-align: center;
}

/* iob.css paints every .fa inside a .modal-primary #4267b2. That is 5.5:1 on
   the light card but 2.8:1 on the dark one, under the 3:1 a meaningful graphic
   needs, and here the tick IS the answer. --accent-ink moves with the theme.
   Four classes, so it outranks that rule's four by coming later in the cascade. */
.iob-brand .iob-roles-modal .iob-roles-table .fa {
    color: var(--accent-ink);
}

/* Below sm the five columns do not fit: at body size the table measured 396px
   in a 311px body at 375px wide, so the Guest column sat behind a sideways
   scroll. Smaller type, tighter cells and a narrower body gutter bring it to
   335px in 335px. .table-responsive stays on the wrapper as the fallback for
   anything narrower. */
@media (max-width: 575.98px) {
    .iob-brand .iob-roles-modal .modal-body {
        padding-left: .75rem;
        padding-right: .75rem;
    }

    .iob-brand .iob-roles-table {
        font-size: .8125rem;
    }

        .iob-brand .iob-roles-table > :not(caption) > * > * {
            padding: .25rem .2rem;
        }
}

/* The unselected page numbers follow the links. They sit directly beside the
   active pill, which is --action-primary, so leaving them cyan put the two
   blues of one control in different families. --accent-ink stays what it was
   for everything that is not a link: the soft badge and the focus outline. */
.iob-brand .page-link {
    color: var(--link);
}

/* THE CURRENT PAGE HAD ITS NUMBER PAINTED IN ITS OWN BACKGROUND COLOUR: both
   were --brand-darker, so the digit was invisible at 1:1. It survived a
   fortnight of contrast audits because the audit skipped any text shorter than
   two characters, and a page number is one. The audit floor is fixed; this is
   now the primary fill with white on it, 4.54:1, and it matches the selected
   state everywhere else. */
.iob-brand .page-item.active .page-link {
    background-color: var(--action-primary);
    border-color: var(--action-primary);
    color: var(--white);
}

/* --- The style guide's own layout ------------------------------------------

   Only what the guide needs to lay itself out: swatch tiles and the spacing
   ruler. Everything the guide DEMONSTRATES is styled by the rules above, which
   is the point of the page -- if these rules grew to style the buttons or the
   cards on it, it would stop being evidence. */
.iob-brand .iob-sg {
    max-width: 62rem;
}

.iob-brand .iob-sg__note {
    color: var(--text-muted);
    font-size: var(--font-size-dense);
}

.iob-brand .iob-sg__swatches {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
    gap: var(--space-3);
}

.iob-brand .iob-sg__swatch {
    font-size: var(--font-size-meta);
    color: var(--text-muted);
}

    .iob-brand .iob-sg__swatch span {
        display: block;
        height: 40px;
        border: 1px solid var(--border-hairline);
        border-radius: var(--radius-control);
        margin-bottom: var(--space-1);
    }

.iob-brand .iob-sg__spacing div {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    font-size: var(--font-size-meta);
    color: var(--text-muted);
    margin-bottom: var(--space-1);
}

    .iob-brand .iob-sg__spacing span {
        display: block;
        height: 12px;
        background-color: var(--accent);
        border-radius: 2px;
    }

/* ---------------------------------------------------------------- calendar */

/* FullCalendar draws no grid lines at all -- not in month, week, day or list.
   The cause is the Bootstrap 4 to 5 migration, one step removed. The calendar
   runs themeSystem: 'bootstrap', FullCalendar's Bootstrap 4 theme plugin, and
   that theme delegates borders by putting .table-bordered on the scrollgrid.
   In Bootstrap 4 that was a descendant selector --

       .table-bordered th, .table-bordered td { border: 1px solid #dee2e6 }

   -- which reached the day cells inside FullCalendar's nested tables. Bootstrap
   5 rewrote it as a child chain:

       .table-bordered > :not(caption) > * > * { border-width: 0 1px }

   which stops at the outer table's own cells and never reaches the inner ones.
   Meanwhile Bootstrap's reboot sets border-width: 0 on every td and th, so what
   is left is a grid with no lines in it.

   Swapping to FullCalendar's standard theme would fix it and cost more: under a
   theme the toolbar controls render as .btn rather than .fc-button, and iob.css
   sizes them that way for narrow viewports. So this restates the border rules
   the standard theme would have applied, scoped to the Bootstrap theme, and
   points FullCalendar's variables at the design tokens.

   This is also where the calendar's off-palette colours came from: today was
   rgba(255,220,40,.15), FullCalendar's default yellow wash, which belongs to no
   part of this design. */
.iob-brand .fc-theme-bootstrap {
    --fc-border-color: var(--border-hairline);
    --fc-page-bg-color: var(--surface-card);
    --fc-neutral-bg-color: var(--surface-sunken);
    --fc-today-bg-color: var(--accent-wash);
}

    /* Bootstrap's reboot sets border-color: inherit on thead, tbody, tr, td and
       th -- but not on table, where the chain starts, so it resolves to
       currentColor and the rows Bootstrap DOES reach through .table-bordered
       drew in the body text colour. That is the near-black line above each date
       in list view. Set on the table so the inherit chain has something to
       inherit, and on the rest so nothing has to rely on it. */
    .iob-brand .fc-theme-bootstrap table,
    .iob-brand .fc-theme-bootstrap thead,
    .iob-brand .fc-theme-bootstrap tbody,
    .iob-brand .fc-theme-bootstrap tr {
        border-color: var(--fc-border-color);
    }

    /* Every cell in every view, the way .fc-theme-standard does it. */
    .iob-brand .fc-theme-bootstrap td,
    .iob-brand .fc-theme-bootstrap th {
        border: 1px solid var(--fc-border-color);
    }

    /* The outer frame. Right and bottom are dropped because the cells' own
       borders already draw those edges; leaving them on doubles the line. */
    .iob-brand .fc-theme-bootstrap .fc-scrollgrid {
        border: 1px solid var(--fc-border-color);
        border-right-width: 0;
        border-bottom-width: 0;
    }

    /* List view is its own table and gets no frame from the scrollgrid rule. */
    .iob-brand .fc-theme-bootstrap .fc-list {
        border: 1px solid var(--fc-border-color);
    }

    /* The date header rows in list view. Without a fill they read as another
       event row, which is the whole reason the view has them. */
    .iob-brand .fc-theme-bootstrap .fc-list-day-cushion {
        background-color: var(--fc-neutral-bg-color);
    }

    /* Column headings: the days of the week, and the date column in list view.
       Same sunken fill as a table header elsewhere in the app. */
    .iob-brand .fc-theme-bootstrap .fc-col-header-cell {
        background-color: var(--surface-sunken);
        font-weight: 600;
    }

    /* Today, in a view with only one day in it, is distinguished from nothing --
       and the title already says the date. Tinting the whole grid just makes the
       day view a different colour from every other screen. */
    /* :not the header cell -- in a one-day view the column heading is also
       .fc-day-today, and clearing it there takes the heading row's fill with it. */
    .iob-brand .fc-theme-bootstrap .fc-timeGridDay-view .fc-day-today:not(.fc-col-header-cell),
    .iob-brand .fc-theme-bootstrap .fc-dayGridDay-view .fc-day-today:not(.fc-col-header-cell) {
        background-color: transparent;
    }

    /* Half-hour lines are the ones you read a duration against, so they stay,
       but dotted and only inside the hour -- a solid line every 30 minutes
       makes a day view look like graph paper. */
    .iob-brand .fc-theme-bootstrap .fc-timegrid-slot-minor {
        border-top-style: dotted;
    }

/* --- The sign-in card -----------------------------------------------------

   WHY A MAX-WIDTH AND NOT A COLUMN. This card held
   `offset-lg-3 col-lg-6`, which is half the container -- and the container
   grows to 1320px on a wide screen, so the card grew with it, to 636px for two
   single-line fields. A login form has no content that benefits from width:
   wider only means a longer mouse trip from the field to the button and a
   measure that reads as an empty box.

   Half a container is also three widths, not one. 456px at lg, 570px at xl,
   636px at xxl -- the card resized twice on the way up for no reason a person
   could name. One max-width is one width from 400px up, and below that the
   column is simply full-bleed, which is exactly what `col-12` already did on a
   phone. Nothing about the small end changes.

   400px is the column width, not the card width: 376px of card inside it once
   the gutters are taken off. The narrowest thing in the card is the pair of
   half-columns in the footer -- "Forgot your password?" against "Don't have an
   account?" -- which measure 171px each at a 375px viewport and do not wrap.
   That is the floor; going narrower breaks that row first. */
.iob-brand .iob-auth-col {
    max-width: 25rem;
    margin-inline: auto;
}

/* The rule inside the sign-in card, under the logo -- one on each of Login,
   ForgotPassword (both steps) and ResetPassword.

   Bootstrap's hr is `color: inherit` at `opacity: .25`, and the body text here
   is black, so the line lands on roughly #bfbfbf -- a mid grey, darker than the
   card's own border, which makes a divider inside the card heavier than the
   edge of the card itself. It reads as a rule that is separating two things
   rather than one that is resting between them.

   --border-hairline is the colour the card, its header and its footer already
   draw their own edges in, so the divider now matches them instead of
   competing. THE OPACITY RESET IS NOT OPTIONAL: #eaecf0 at Bootstrap's .25
   computes to about #fafbfc on white, which is invisible. Setting the colour
   without setting this back to 1 deletes the line rather than lightening it. */
.iob-brand .iob-auth-col hr {
    border-top-color: var(--border-hairline);
    opacity: 1;
}

/* A button with no border and no fill: the words alone, with a button's hit
   area behind them. The reset link on the sign-in card is the first of these.

   IT IS STILL AN <a>, AND THAT IS NOT AN OVERSIGHT. It goes to another page,
   so it is a link wearing button clothes, which is the job .btn-link exists
   to do. A real <button> would have to be scripted to navigate, would lose
   middle-click and "open in a new tab", and would tell a screen reader it
   does something on this page rather than leaving it.

   NO UNDERLINE. Bootstrap's .btn-link underlines, because its .btn-link is a
   link that looks like a link. This one is a control that looks like a
   control, and an underline next to a field label reads as the second half of
   a sentence.

   THE TWO ZEROS ARE WHAT KEEP IT ALIGNED. .btn brings 14px of side padding
   from the rule above and a 6px margin from iob.css, and together they would
   hold the text 20px clear of the field's right edge -- the alignment this
   line was built for, undone by the class that restyles it. Done here rather
   than with p-0/m-0 in the markup because that 6px is deliberate everywhere
   else: iob.css's own comment says layouts lean on it for the gap between
   adjacent buttons, so it is wrong to reach for a utility that looks like it
   is correcting a mistake.

   VERTICAL PADDING STAYS, AND --space-1 IS THE ONLY VALUE THAT WORKS. It buys
   a 29px target instead of a 21px one, which on a phone is the difference
   between hitting this and hitting the password field underneath it. The size
   is not a preference: on a baseline-aligned line, a button padded by p sits
   its box bottom at p below the label's text, and the label's own margin is
   --space-1, so p = --space-1 is what lands the two boxes level and keeps the
   gap down to the field the same 4px the Username field has. Bootstrap's 7px
   put the button's box 3px past the label's and closed that gap to nothing.

   Negative block margins would buy a taller target while keeping the layout
   honest, and they are the wrong tool here: they would leave a clickable box
   overlapping the top of the password field, so a tap at the field's top-right
   corner would navigate away instead of focusing it. */
.iob-brand .iob-text-button {
    padding-block: var(--space-1);
    padding-inline: 0;
    margin: 0;
    text-decoration: none;
}

/* A FULL-WIDTH BUTTON IS 6px OFF ITS CONTAINER, AND HAS BEEN ALL ALONG.

   iob.css gives every .btn `margin: 0.375rem`. On a button sized by its own
   content that margin is the gap to the next button, which is why the rule
   above deliberately leaves it alone. On a .w-100 button it is neither: the
   width is already 100% of the container, so 6px of side margin pushes the
   button off the left edge of the field above it and hangs the same 6px past
   the card's padding on the right. Measured on Login before this rule: input
   340.8 -> 683.2, button 346.8 -> 689.2. Same width, moved sideways.

   w-100 is an unambiguous statement that the button should be exactly as wide
   as what contains it, so the side margins are dropped wherever it appears.
   The vertical margin stays -- it is spacing between stacked controls, and it
   is doing its job.

   SignUp.aspx already carries mx-0 on its Continue button, which is this same
   bug found once and patched in markup; that attribute is now redundant but
   harmless, and the five buttons on Login, ForgotPassword and ResetPassword
   that did NOT get the patch are what this fixes. */
.iob-brand .btn.w-100 {
    margin-inline: 0;
}

/* --- The server's error message, on the three sign-in pages ---------------

   These are the failures no field validator knows about: a rejected sign-in,
   a throttled attempt, a failed reCAPTCHA, an inactive account, an unknown
   email, an expired reset link. All three pages report them through a single
   asp:Label, and all three now render it as an alert at the top of the form
   rather than as a line of red text below the button.

   IT IS AN asp:Label, SO IT IS A <span>, SO IT NEEDS display. Bootstrap's
   .alert sets padding, border and margin but never display, which is fine on
   the <div> it is normally written on and leaves this one inline -- padding
   that overlaps its neighbours instead of pushing them apart.

   :empty IS WHAT KEEPS AN EMPTY RED BOX OFF A CLEAN PAGE LOAD, and it is the
   reason none of this needed a wrapper div, a Visible="false", or a single
   line of code-behind. The label renders as <span ...></span> with no child
   nodes at all when the server has nothing to say, which is exactly what
   :empty matches; the moment the server -- or Login's own iobFailSubmit --
   puts text in it, the rule stops matching and the alert appears.

   The element stays in the DOM either way, which is what lets
   validation-a11y.js give it role="alert" on load and have the later
   insertion announce. */
.iob-brand .iob-form-error {
    display: block;
}

    .iob-brand .iob-form-error:empty {
        display: none;
    }

/* --- The comments column, when full comments is off ------------------------

   .text-truncate is overflow:hidden + text-overflow:ellipsis + white-space:
   nowrap, and it stopped truncating when the board became a CSS table at md
   and up. Under table-layout: auto a cell asks for its content width, and
   nowrap makes that the whole comment: the column takes what it asks for, the
   table runs past 100%, and the page gets a horizontal scrollbar. Nothing ever
   overflows the cell, so the ellipsis never appears. Measured on a 1,100px
   viewport with one 130-character comment: the board rendered 1,233px wide,
   the comments cell 1,037px, and scrollWidth equalled clientWidth.

   THE FIX FOR THAT WAS max-width: 0, AND IT OVERSHOT. Reported 22 September:
   the column was a 20px sliver with the header reading "C...". max-width on a
   table-cell is undefined in CSS 2.1 and Chrome resolves it by capping the
   COLUMN, not just the box -- so the cell asked the table for nothing, width:
   20% above was never honoured, and the free space it should have had went to
   the auto columns beside it. Measured at 1,440px with the default column set:
   comments 20px against a 93px header, while Status took 209px and Last Update
   198px for content that needs about 110px.

   nowrap was the real fault, not the cell's width, so this undoes nowrap and
   moves the one-line clamp onto the link inside. The cell then sizes like any
   other text cell -- min-content is the longest word, not the whole comment,
   which is what forced the table past 100% -- and -webkit-line-clamp: 1 keeps
   the visible result to a single line with an ellipsis. Same viewport after:
   comments 348px, header drawn in full, no page overflow.

   This makes the truncated column size exactly like the "Display full
   comments" column, which has always been correct; the only difference left is
   the clamp. Measured side by side at 1,100px with a long comment on every
   row, both give a 90px column -- the crowded case where every column is at
   its minimum -- but the clamped row is 89px tall against 310px.

   Below md the board is not a table and iob.css already unwinds .text-truncate
   there, so this stays behind the same min-width the table rules use. */
@media (min-width: 768px) {
    .iob-brand .sb-board > [role="row"] > .sb-col-comments.text-truncate {
        white-space: normal;
        /* The clamp below owns the clipping now. */
        overflow: visible;
    }

        /* The link IS the comment -- see the PlaceHolder in ListView.aspx. */
        .iob-brand .sb-board > [role="row"] > .sb-col-comments.text-truncate a {
            display: -webkit-box;
            -webkit-box-orient: vertical;
            -webkit-line-clamp: 1;
            line-clamp: 1;
            overflow: hidden;
        }
}

/* --- A floor under the name column, once there is width to spare -----------

   The other half of what the clamp above started. Undoing nowrap stopped the
   comments column forcing the table past 100%, but it left the column DEMANDING
   its whole text, and under table-layout: auto the width is dealt out in
   proportion to that demand. The width: 20% hint further up is a preference the
   algorithm meets and then sails past, so on a wide screen comments takes
   whatever the other columns are not using. Measured at 1,920px: on People the
   comments column is 1,000px of a 1,647px board, on List 1,099px of the same
   1,647px. Neither overflows -- this is not the old defect coming back -- but
   the name column is left on 123px and 110px.

   On People that costs nothing and is left alone. The photograph fixes every
   row at 83px, so a name that wraps to a second line takes no extra height; at
   1,920px exactly one of twenty-two names wraps, no word is ever broken, and
   the board is the same 1,856px tall either way. List has no photograph. Its
   rows are 38px, a wrapped name makes one 59px, and fifteen of twenty-two wrap
   at 1,200px -- the list is 1,134px tall for content that needs 943px.

   So this is a floor under the name rather than a cap over the comment. A cap
   is what the resource board uses, and it is wrong here: the width it releases
   goes to whichever column is greediest next, which on these two boards is
   Status. Measured on People with a 22rem cap on comments, the status chip --
   a filled block that paints its whole cell -- went from 133px to 255px for the
   word "In", and the longest comment lost half its visible text, 137 characters
   to 74. A floor moves width from the comment to the name and touches nothing
   else: the chip stayed at 84px against 88px, and comments kept 367px.

   1,200px, not the 768px the table rules use, because below about 1,059px there
   is no surplus left to redistribute -- every column is already at its
   min-content and a floor can only be paid for by overflowing. Measured on
   People, the tighter of the two because of the photograph: at 992px the floor
   pushed the board to 786px inside a 719px card body, and at 800px to 754px
   inside 727px. At 1,200px the same board fits with about 140px to spare.

   Not the resource board. It has ten optional columns in any combination, so
   its worst case is a user preference rather than a fixed set, and its name
   column is already held up by the comments cap at the end of this file. The
   :not() follows iob.css, which excludes it from the status cap the same way. */
@media (min-width: 1200px) {
    .iob-brand .sb-board:not(.rs-board) > [role="row"] > .sb-col-name {
        min-width: 10rem;
    }
}

/* --- A card header with a control in it ------------------------------------

   Upcoming Events on the dashboard, and Mark all as read on the Messages
   folders -- the pattern for any card whose controls belong to the card rather
   than to the page. The title stays left, the control goes right, and
   flex-wrap means a narrow phone stacks them rather than squeezing the
   control.

   The filter was above the list before, inside the card body, where GetEvents
   could hide it whenever the list came back empty. That is backwards: the
   filter is the usual REASON the list is empty, and hiding it removed the only
   way back. In the header it cannot be hidden by the state of the thing it
   controls.

   .card-header sets font-weight 500 and the section font size for the title,
   neither of which the control should inherit -- .iob-cardtools__filter gives
   the label its own muted, smaller treatment, .form-select carries its own,
   and .btn carries both. Reused rather than restated so the filter looks the
   same here as it did above the list. */
.iob-brand .card-header.iob-cardhead--split {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: var(--space-2) var(--space-3);
    /* The height the header has WITH a control in it. Upcoming Events always has
       its filter, but Mark all as read comes and goes with whether anything is
       unread, and without this the header shrank 7px at the moment you clicked it
       -- the card jumping under the cursor that just acted on it. It also keeps
       the two split headers the same height as each other. min-, not fixed: a
       folder name and a control that wrap to two lines on a phone still get the
       room. */
    min-height: 3.5rem;
}

    /* iob.css still gives every .btn margin: 0.375rem, from the MDB era. In a flex
       header that is 6px of dead space between the control and the card's own
       padding, which puts the button out of line with everything else down the
       card's right edge. Zeroed here rather than globally, for the same reason
       .iob-cardtools .btn zeroes it: 0.375rem is load-bearing for the many places
       that still lay buttons out by margin. */
    .iob-brand .card-header.iob-cardhead--split .btn {
        margin: 0;
    }

/* --- Fewer messages on a phone -------------------------------------------

   GetMessages sends ten, and ten is right on a desktop where the card sits in
   a column beside the numbers. On a phone it is about a thousand pixels of
   scroll for a PREVIEW whose full version is one tap away under "Go to
   messages", and it pushes Upcoming Events off the bottom of the screen.

   Capped here rather than in GetMessages because the server cannot know the
   viewport: Take(5) would take five from the desktop too. display: none also
   takes the hidden five out of the accessibility tree, so a phone screen
   reader is told five items rather than ten with five silently unreachable.

   md, not lg: a tablet has the room, and this is about a narrow screen rather
   than about the sidebar collapsing. On the messages list only -- Upcoming
   Events already caps at ten in GetEvents and Announcements at three. */
@media (max-width: 767.98px) {
    .iob-brand .iob-feed--cap5 > .iob-feed__item:nth-child(n+6) {
        display: none;
    }
}

/* --- The resource list, laid out as a table at md and up -------------------

   Same defect and same fix as the status board in 81d686b. /resources/search/
   builds its list as Bootstrap .row divs -- one for the header, one per
   resource -- and a flex row sizes its columns from its own content, knowing
   nothing about the row above it. One long serial number or a two-line status
   chip widens that cell in that row only, and by the right-hand side nothing
   lines up with the header. A table is a table.

   Differences from the board worth knowing, because the selectors cannot be
   shared:

   The board carries ARIA table semantics -- role="table", role="row",
   role="cell" -- and its rules hook those. This page has none of them, so
   these hook the Bootstrap classes instead. Giving it the roles would be the
   better fix and is a bigger change than column alignment; it is not done
   here.

   The wrapper is .rs-board rather than the card body because the body also
   holds divNoResources, and an empty-state message must not become a table
   row.

   No width hints. The board's exist because its columns are known and fixed;
   here there are ten optional columns in any combination, and under
   table-layout: auto the browser balances what it is given. Add hints only if
   a real column set proves it needs them. */
@media (min-width: 768px) {
    .iob-brand .rs-board {
        display: table;
        width: 100%;
    }

    .iob-brand .rs-board > .row {
        display: table-row;
        margin: 0;
    }

        /* .col-12 sets width: 100%, which on a table-cell is a 100% hint from
           every column at once -- no hint at all. flex: none undoes the shrink
           and grow that no longer apply. */
        .iob-brand .rs-board > .row > [class*="col-"]:not(.sb-disclosure) {
            display: table-cell;
            vertical-align: top;
            width: auto;
            flex: none;
            padding: var(--space-1) var(--space-2);
        }

        /* The disclosure button belongs to the phone layout only. It has to be
           said here as well as in the .sb-board block above: this board carries
           both classes now, the two selectors weigh the same, and these rules
           come later in the file -- so without this the cell wins its own
           display and the desktop board grows an empty tenth column. */
        .iob-brand .rs-board > .row > .sb-disclosure {
            display: none;
        }

        /* Borders belong to the row, but a table-row cannot paint one in every
           browser, so they go on the cells and the row's own border comes off. */
        .iob-brand .rs-board > .row.border-bottom,
        .iob-brand .rs-board > .row.border-top {
            border: 0;
        }

        .iob-brand .rs-board > .row.border-bottom > [class*="col-"] {
            border-bottom: 1px solid var(--border-hairline);
        }

        .iob-brand .rs-board > .row.border-top > [class*="col-"] {
            border-top: 1px solid var(--border-hairline);
        }

        /* The comments column, and why this board caps it.

           The three comments cells in ResourceSearch.aspx carry
           .sb-col-comments now, so the width hint and the one-line clamp in the
           .sb-board rules above reach this board instead of it keeping its own
           copy. What stood here was max-width: 0, the same fix the status board
           carried and wrong the same way: max-width on a table-cell is
           undefined in CSS 2.1 and Chrome resolves it against the COLUMN, so
           the cell asked the table for nothing and the slack went to its
           neighbours. Measured at 1,920px with Name, Type, Status, Assigned To,
           Return Time, Comments and Active shown, one 138-character comment:
           comments 181px while Return Time took 285px for a date that needs
           about 110px.

           Removing it is not enough on its own, and here the board parts
           company with People. Under table-layout: auto the width is dealt out
           by max-content demand, and a clamped comment still DEMANDS its whole
           text -- 900px for that comment. A width hint does not restrain that;
           the shared 20% is a preference the algorithm meets and then sails
           past. Same viewport with the class on and nothing else: comments
           916px of a 1,662px board, Name down to 191px, three of eleven names
           wrapped to a second line, tall rows 49px to 71px.

           That is the column this board has already fought for once -- see the
           note in iob.css, where losing the photograph bought the name cell
           182px and stopped "Large Conference Room 1" breaking across three
           lines. A person is "Canas, Kyle" and fits. A resource is "Large
           Conference Room 2" and does not.

           So the cap comes back as a length instead of zero, on this board
           only. It does not cap the cell: 22rem is 352px and the cell still
           measures 527px, because what a length does here is hold down the
           column's max-content DEMAND rather than the box -- which is the whole
           difference from max-width: 0, that held the demand down to nothing.
           Measured at 1,920px: comments 527px, Name back to 295px, no name
           wrapped, tall rows 49px, header drawn in full, ellipsis drawn, board
           1,662px against a 1,662px card body, no page overflow. At 1,100px
           every column is near its minimum and the cap barely bites: comments
           231px to 190px, Name 112px to 134px, one fewer name wrapped.

           Both comment modes, unlike the rule this replaces. Display full
           comments is the same text in the same column and wants the same
           restraint; the clamp that would wrongly flatten it to one line is
           scoped to .text-truncate above, which that mode does not carry. With
           the cap it measures 527px and the tall rows come in at 52px rather
           than 71px, because the names stop wrapping. */
        .iob-brand .rs-board > .row > .sb-col-comments {
            max-width: 22rem;
        }
}

/* --- Tagify, the manager fields' clear button -------------------------------------

   The × that clears a Manager or Second Level Manager (js/user-picker.js,
   select mode) is 14x14 as Tagify ships it, under the 24px floor every other
   tap target in this application was raised to in the phone pass. This enlarges
   the target only: the glyph is still 14px, centred by Tagify's own
   inline-flex, and the 40px field has room for it. The multi-select user picker
   (iob-user-picker--multi, the setup pages that add people in bulk) gets the
   same: its remove buttons are how a keyboard or touch user takes a person out.
   Neither reaches the chips on Inbox/MessageEdit.aspx, which keep their size. */
.iob-brand .tagify--select .tagify__tag__removeBtn,
.iob-brand .tagify.iob-user-picker--multi .tagify__tag__removeBtn {
    width: 24px;
    height: 24px;
}

/* --- Tagify, dark mode --------------------------------------------------------

   Tagify drives the Manager and Second Level Manager search fields
   (js/user-picker.js, on Profile/UserProfileEdit.aspx and Setup/UserEdit.aspx)
   and the user and team fields on Inbox/MessageEdit.aspx. It replaced the
   Telerik RadComboBox on the manager fields on 25 September 2026, and this
   replaces the RadComboBox dark-mode override that used to sit here.

   Tagify themes itself through custom properties rather than fixed colours, so
   this sets those properties to this file's tokens and does not restyle its
   classes. That is also why a Tagify upgrade is less likely to break it than a
   Telerik one was. Its light defaults (#E5E5E5 chips, a white dropdown, black
   text) are left alone in light mode, where they already read fine; in dark mode
   they put a pale chip and a white dropdown on a navy card.

   The dropdown is appended to <body>, not inside the field, and Tagify declares
   its variables on :root. So the dropdown gets its own rule: it inherits the
   overridden values from .iob-brand, the same way the RadComboBox popup did. */
[data-bs-theme="dark"] .iob-brand .tagify {
    --tags-border-color: var(--border-control);
    --tags-hover-border-color: var(--border-control);
    --tag-bg: var(--surface-sunken);
    --tag-hover: var(--surface-sunken);
    --tag-text-color: var(--text-strong);
    --tag-text-color--edit: var(--text-strong);
    --tag-remove-btn-color: var(--text-body);
    --input-color: var(--text-body);
    --placeholder-color: var(--text-muted);
    --placeholder-color-focus: var(--text-muted);
}

[data-bs-theme="dark"] .iob-brand .tagify__dropdown {
    --tagify-dd-bg-color: var(--surface-card);
    --tagify-dd-text-color: var(--text-body);
    --tagify-dd-color-primary: var(--action-primary);
    box-shadow: var(--shadow-overlay);
}

/* --- The access and member lists (Setup/CalendarAccess.aspx, TeamMembers.aspx) ---

   The filter box over a long list: one calendar has 219 individual entries,
   and the largest team 284 members. */
.iob-brand .iob-list-filter {
    width: 16rem;
    max-width: 100%;
}

/* Since the lists are paged it is a search box with a Search button (7 Oct
   2026), so it needs the button's width on top of the field's 16rem. The
   button carries .btn-sm and .m-0: the theme's 6px .btn margin made the group
   taller than the button, and the field stretched to fill it. */
.iob-brand .iob-list-filter--search {
    width: 21rem;
}

/* A muted note beside a list value: "No effect" on an access entry (a team set
   to No Access, or a grant no higher than the calendar's default), "Inactive"
   on a team member. Muted, not a warning colour -- neither is a problem to fix,
   and an access entry starts to count if the calendar's default is lowered. */
.iob-brand .iob-list-note {
    color: var(--text-muted);
    font-size: var(--font-size-meta);
    margin-left: var(--space-1);
    white-space: nowrap;
}

/* SUPPORT SESSION BAR. Shown only when an admin has signed in as a customer from the
   Admin site (Site.master, ImpersonationBar). Same yellow as the Admin site's
   "Log in as..." button (Bootstrap's warning). Fixed colours, not theme tokens: it
   has to read as a warning in both light and dark mode. */
.iob-brand .iob-impersonation {
    background-color: #ffc107;
    color: #000;
    text-align: center;
    padding: .375rem var(--space-3);
    font-size: .9rem;
}

.iob-brand .iob-impersonation i {
    margin-right: .5rem;
}

/* --- The welcome guide (js/welcome-tour.js) -----------------------------------

   The popovers a new account sees on its first visit to Home. Blue, not
   Bootstrap's grey popover, which washed out against the grey shell (Chris,
   9 Oct 2026). The same blue in both themes, since it has to stand out from a
   dark page as well as a light one. White text on --blue-dark is 5.84:1 (see
   the action colour note at the top of this file). The arrow draws its fill and
   border from the same variables, so it is blue too. */
.iob-brand .iob-tour {
    --bs-popover-bg: var(--blue-dark);
    --bs-popover-border-color: var(--blue-dark);
    --bs-popover-header-bg: var(--blue-dark);
    --bs-popover-header-color: var(--white);
    --bs-popover-body-color: var(--white);
    --bs-popover-arrow-border: var(--blue-dark);
    --bs-popover-max-width: 20rem;
    --bs-popover-box-shadow: 0 12px 32px rgba(13, 110, 253, .35), 0 4px 12px rgba(0, 0, 0, .18);
    box-shadow: var(--bs-popover-box-shadow);
}

.iob-brand .iob-tour .popover-header {
    font-size: 1rem;
    font-weight: 600;
    padding-bottom: var(--space-1);
    border-bottom: 0;
}

/* Bootstrap draws a line under the header where a bottom-placed arrow meets it.
   Header and body are one colour here, so there is nothing to separate. */
.iob-brand .iob-tour .popover-header::before {
    display: none;
}

.iob-brand .iob-tour .popover-header i {
    margin-right: var(--space-2);
    color: var(--brand-tint);
}

.iob-brand .iob-tour .popover-body {
    padding-top: var(--space-2);
}

.iob-brand .iob-tour__welcome {
    font-weight: 600;
    margin-bottom: var(--space-2);
}

.iob-brand .iob-tour__text {
    margin-bottom: var(--space-3);
}

.iob-brand .iob-tour__actions {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: var(--space-2);
}

/* The buttons are drawn for the blue. Next is the one white button, so it is
   the obvious way on; Back is a white outline; Skip is plain text, on the left,
   away from Next, so it is not hit on the way to it. */
.iob-brand .iob-tour .btn {
    margin: 0;
}

.iob-brand .iob-tour__next {
    background-color: var(--white);
    border-color: var(--white);
    color: var(--blue-darker);
    font-weight: 600;
}

.iob-brand .iob-tour__next:hover {
    background-color: var(--brand-tint);
    border-color: var(--brand-tint);
    color: var(--blue-darkest);
}

.iob-brand .iob-tour__back {
    background-color: transparent;
    border-color: rgba(255, 255, 255, .7);
    color: var(--white);
}

.iob-brand .iob-tour__back:hover {
    background-color: rgba(255, 255, 255, .15);
    border-color: var(--white);
    color: var(--white);
}

.iob-brand .iob-tour__skip {
    margin-right: auto;
    padding-left: 0;
    background-color: transparent;
    border-color: transparent;
    color: var(--white);
    text-decoration: underline;
}

.iob-brand .iob-tour__skip:hover {
    color: var(--brand-tint);
}

/* Bootstrap's focus ring is blue, which disappears on this background. */
.iob-brand .iob-tour .btn:focus-visible {
    outline: 2px solid var(--white);
    outline-offset: 2px;
    box-shadow: none;
}

/* The item each popover points at: a blue ring and a light blue wash, so the
   popover and the item read as a pair. An outline, inset, rather than a
   box-shadow: outlines paint above the element's own children, so the ring
   shows over an active link's background, and an inset one is not clipped by
   the rail's scroller. */
.iob-brand .iob-tour-target {
    outline: 3px solid var(--blue);
    outline-offset: -3px;
    border-radius: var(--radius-sm);
    background-color: rgba(13, 110, 253, .12);
}
