/*
Theme Name: Americon Bid
Theme URI: https://www.americonconstructionco.com
Author: Americon Construction
Version: 2.0
Description: Professional Americon bid calendar theme.
*/

/* =============================================================================
   AMERICON DESIGN SYSTEM
   =============================================================================

   ORGANISATION
   ------------
   1. Tokens        – brand constants + semantic tokens that flip for dark mode
   2. Base          – element defaults, typography, focus
   3. Layout        – page shells, headers, the classes templates actually use
   4. Components    – nav, cards, lists, filters, badges, dropzone, footer
   5. Bootstrap map – hand Bootstrap our palette through its own CSS variables

   WHY THERE IS NO @layer HERE
   ---------------------------
   Bootstrap arrives as its own <link> and is therefore *unlayered*. In the CSS
   cascade, unlayered rules beat layered ones, so wrapping this file in @layer
   would make every rule below lose to Bootstrap. Instead this sheet is enqueued
   with Bootstrap as a dependency, so it simply loads later and wins on source
   order at equal specificity. That is also why the `!important` pile that used
   to live in this file is gone: it was never needed.

   DARK MODE
   ---------
   Follows the OS setting only (no manual toggle). Semantic tokens use
   light-dark(), which is driven by the `color-scheme` declared on :root.
   Bootstrap's own components need its `data-bs-theme` attribute instead; a
   small inline script in header.php sets that from the same media query
   before first paint.
   ========================================================================== */


/* =============================================================================
   1. TOKENS
   ========================================================================== */

:root {
  color-scheme: light dark;

  /* --- Brand constants (identical in both themes) --- */
  --am-red: #ED1C24;
  --am-red-dark: #C0101D;
  --am-red-bright: #FF3A42;

  /* The brand red as a fill with WHITE TEXT ON IT.
     #ED1C24 carries white at 4.38:1 -- under AA, and button labels are 16px
     semibold, which is not large text by any reading. This is the smallest
     step down that clears 4.5 (it measures 4.68), so a primary button is a
     shade deeper than the topbar beside it and no one will pick the two apart.
     --am-red itself is untouched: it edges the topbar and the page rules,
     where nothing sits on top of it and the brand value is the right one. */
  --am-red-cta: #E51820;
  --am-blue: #272E7E;
  --am-blue-bright: #5964D3;
  --am-amber: #F59E0B;

  /* Private-project identity, on the calendar markers and the private tile.
     Private used to be red, which stopped working once red was reserved for
     "closing today": the same hue meant a category in one place and an urgency
     in another, a few hundred pixels apart.

     Teal rather than the obvious violet because the choice was measured, not
     picked. Violet #9085E9 sits ΔE 12.3 from the dark-mode brand blue -- under
     the 15 floor, i.e. hard to separate even with full colour vision, and the
     two would have been adjacent markers in the same cell. Magenta #D55181
     cleared the blue but landed ΔE 10.7 from the urgent red, rebuilding the
     collision this token exists to remove. Teal clears everything in the set:
     worst pair ΔE 8.1 protan / 25.7 normal in light, 10.1 / 20.0 in dark.

     (Values from scripts/validate_palette.js in the dataviz skill, run against
     this theme's own surfaces -- #FFFFFF and #1E232B -- not against defaults.) */
  --am-teal: #0F7A57;
  --am-teal: light-dark(#0F7A57, #199E70);

  /* --- Semantic surfaces ---
     Each token is declared twice on purpose: the plain light value first, then
     the light-dark() version. A browser without light-dark() support drops the
     second declaration and keeps the first, so the site degrades to a correct
     light theme instead of losing the colour entirely.

     The dark values used to bottom out at #0E1116 canvas / #171B22 surface --
     near-black, with #E6E9EF text over it. That is a fine contrast ratio and a
     tiring screen: this is a page a coordinator leaves open all day, and
     maximum contrast on a large dark field is what causes the glow around
     white text that makes long sessions uncomfortable. The whole dark ramp is
     lifted one step and the text pulled slightly off white. Text on surface
     still measures ~12:1, well past AA, so nothing is traded but the glare. */
  --am-canvas:        #F4F5F7;
  --am-canvas:        light-dark(#F4F5F7, #161A21);
  --am-surface:       #FFFFFF;
  --am-surface:       light-dark(#FFFFFF, #1E232B);
  --am-surface-sunk:  #F7F8FA;
  --am-surface-sunk:  light-dark(#F7F8FA, #1A1F26);
  --am-header-bg:     #FFFFFF;
  --am-header-bg:     light-dark(#FFFFFF, #1A1F26);

  /* --- Semantic text --- */
  --am-text:          #1F2530;
  --am-text:          light-dark(#1F2530, #DDE2EA);
  --am-text-muted:    #5E6673;
  --am-text-muted:    light-dark(#5E6673, #9BA4B3);
  /* Was #818A98, itself lifted from #757F8F to fix the DARK value. The lift
     was measured against the dark surface and never against the light one,
     where it left small uppercase labels at 3.2-3.5:1 -- under AA on all four
     surfaces at once. #646D79 measures 5.2:1 on a card and 4.8:1 on the page.
     The dark half was already fine and is untouched.
     tests/test-contrast.php measures both halves now, which is the actual fix. */
  --am-text-subtle:   #646D79;
  --am-text-subtle:   light-dark(#646D79, #8A93A2);

  /* --- Lines & accents ---
     Borders move up with the surfaces they sit on; a #2A313C hairline is
     invisible against a #1E232B card. */
  --am-border:        #E2E5EA;
  --am-border:        light-dark(#E2E5EA, #313945);

  /* The heavier of the two decorative boundaries: stat tiles, the dashed empty
     state, the dropzone, hover states, <kbd>. None of these is the only thing
     identifying a control -- each sits on a component that also has a fill, a
     label or a heading -- so they are not bound by 1.4.11 and are free to stay
     light. Deliberately NOT used on form controls; see --am-field-border. */
  --am-border-strong: #C9CED7;
  --am-border-strong: light-dark(#C9CED7, #454E5C);

  /* Form controls only, and load-bearing.
     A text input here is a white box on a white card: no fill difference, no
     underline, nothing but this border saying a field exists. WCAG 1.4.11 asks
     3:1 of exactly that, and the decorative border above delivers 1.6:1 light
     and 1.9:1 dark -- so the reliable way to find a field was to click and
     watch for a cursor.

     Its own token rather than a heavier --am-border-strong: that token backs
     about twenty decorative boundaries, and darkening it to input weight would
     have put a hard edge on every stat tile and empty state on the site to fix
     a problem none of them had. Clears 3:1 on both the card and the page in
     both schemes. */
  --am-field-border: #7F8792;
  --am-field-border: light-dark(#7F8792, #6B7686);

  --am-accent:        var(--am-blue);
  --am-accent:        light-dark(var(--am-blue), var(--am-blue-bright));
  --am-danger:        var(--am-red);
  --am-danger:        light-dark(var(--am-red), var(--am-red-bright));

  /* --- The same two hues, as INK ---
     --am-accent and --am-danger are FILLS. Their dark values are chosen to
     hold up as a block of colour, and they do: --am-accent is 3.15:1 against a
     card, which clears the 3:1 floor for a graphic.

     Text is a different problem with a different number, and reusing the fill
     for it put every link, every active filter, every card title and both
     callout labels at roughly 3:1 in dark mode. Nobody noticed for the same
     reason nobody notices any of this: in light mode the identical CSS
     measures 10:1.

     So the fill keeps its name and these carry the reading. They differ from
     the fills only in dark mode, and every value is measured by
     tests/test-contrast.php against all four surfaces plus the tinted
     component backgrounds each one actually lands on. */
  --am-accent-ink:    var(--am-blue);
  --am-accent-ink:    light-dark(var(--am-blue), #99A2F0);
  --am-danger-ink:    var(--am-red-dark);
  --am-danger-ink:    light-dark(var(--am-red-dark), #FF7A80);

  /* --- Status: done, and needs attention ---
     Green and amber, for "this section is complete" against "this one still
     needs something". Neither is a brand colour and neither should become one:
     they say what state a thing is in, which is why they live beside
     --am-danger rather than beside --am-red.

     They existed before these tokens did, as eight raw hex values inside
     americon-project-editor.css, and being raw is what let one of them be
     wrong. The ready badge measured #15803D on its own 8% green tint at
     4.15:1 -- under AA -- and had done since it was written, because the
     contrast checker measures tokens and there was no token here to measure.
     --am-success-ink is the smallest step down from that green which clears
     4.5 on every surface including its own tint (it measures 4.74).

     --am-success is one value in both schemes, unusually: it is only ever a
     fill or a border, it needs 3:1 rather than 4.5:1, and #159B46 clears that
     on the light surfaces (3.31) and the dark ones (4.37) alike. A second
     value would have been two numbers to keep true instead of one. */
  --am-success:       #159B46;
  --am-success-ink:   #14762F;
  --am-success-ink:   light-dark(#14762F, #4ADE80);

  /* --am-amber is the fill; this is the reading. Same fill/ink split as
     --am-accent, and for the same reason: amber is legible as a block of
     colour long before it is legible as words on a page. */
  --am-warn-ink:      #92400E;
  --am-warn-ink:      light-dark(#92400E, #FCD34D);

  /* --- The footer band ---
     One value each, no light-dark(): this band is dark in both schemes by
     design, the way the brand's own printed material is. It is not a surface
     that follows the page.

     Tokens rather than the literals that used to be down in section 4.12 for
     one reason only -- tests/test-contrast.php measures tokens, so as literals
     these four inks were the last text on the site nobody was checking. They
     pass comfortably (5.8:1 at the tightest, on the small print). The point is
     that the next person to adjust them will find out if they stop. */
  --am-footer-bg:         #141A21;
  --am-footer-ink:        #C5CCD6;
  --am-footer-brand-ink:  #EEF1F5;
  --am-footer-link:       #FBBF24;
  --am-footer-link-hover: #FCD34D;
  --am-footer-meta-ink:   #8B95A3;

  /* The single structural red: the topbar and the rule under a page title.
     One token so the two cannot drift, and so the brand accent has a name
     separate from --am-danger, which means "something is wrong". In dark mode
     it steps back from full #ED1C24, which buzzes against a dark field. */
  --am-rule:          var(--am-red);
  --am-rule:          light-dark(var(--am-red), #D93A40);

  /* --- Elevation ---
     Dark-mode alphas come down with the surfaces: a .45 black drop under a
     card that is itself nearly black reads as a hole in the page rather than a
     lift, and it is half of what made the dark theme feel heavy. */
  --am-shadow-sm: 0 1px 2px rgb(16 24 40 / .06);
  --am-shadow-sm: 0 1px 2px light-dark(rgb(16 24 40 / .06), rgb(0 0 0 / .3));
  --am-shadow:    0 1px 3px rgb(16 24 40 / .08), 0 8px 24px rgb(16 24 40 / .06);
  --am-shadow:    0 1px 3px light-dark(rgb(16 24 40 / .08), rgb(0 0 0 / .34)),
                  0 8px 24px light-dark(rgb(16 24 40 / .06), rgb(0 0 0 / .26));
  --am-shadow-lg: 0 12px 40px rgb(16 24 40 / .12);
  --am-shadow-lg: 0 12px 40px light-dark(rgb(16 24 40 / .12), rgb(0 0 0 / .42));

  /* --- Geometry --- */
  --am-radius-sm: .375rem;
  --am-radius: .625rem;
  --am-radius-lg: 1rem;
  --am-radius-pill: 999px;

  /* --- Fluid type scale (min at 360px, max at 1280px) --- */
  --am-text-xs:  clamp(.6875rem, .66rem + .12vw, .75rem);
  --am-text-sm:  clamp(.8125rem, .79rem + .11vw, .875rem);
  --am-text-md:  clamp(.9375rem, .91rem + .12vw, 1rem);
  --am-text-lg:  clamp(1.0625rem, 1.02rem + .2vw, 1.1875rem);
  --am-text-xl:  clamp(1.25rem, 1.16rem + .4vw, 1.5rem);
  --am-text-2xl: clamp(1.5rem, 1.3rem + .9vw, 2.125rem);

  /* --- Spacing rhythm --- */
  --am-gap-xs: .375rem;
  --am-gap-sm: .625rem;
  --am-gap: 1rem;
  --am-gap-lg: 1.5rem;
  --am-gap-xl: 2.5rem;

  /* --- Shell ---
     Three widths, and only one of them is a number anybody should type twice.

     --am-shell-max   the reading column: forms, lists, prose.
     --am-shell-wide  what a template asks for when its content is genuinely
                      wider than a reading column -- today only the month grid.
     --am-shell-chrome the header and footer bands.

     Chrome is the widest of the two rather than the reading column, because a
     nav band narrower than the content beneath it has no edge to sit on: the
     logo lands mid-air, inboard of the page's own left edge, while the header
     background runs full-bleed behind it. Wider is the ordinary relationship
     -- chrome frames the column -- and `max()` guarantees the bands can never
     come out narrower than they were before this rule existed.

     These are derived, not repeated. Change --am-shell-wide and the header,
     the footer and the calendar all move together; that is the point.

     WHY --am-shell-wide HAS NO PIXEL CEILING
     It used to be min(96vw, 1680px). The 1680 was there to stop day columns
     growing without limit, and it did -- but a cap in px is a cap on ONE
     monitor size: below about 1750px of window it never applied at all, and
     above it the month sat in the middle of the screen with a hand's width of
     empty page down each side, on exactly the displays that had room to show
     more of it.

     Wide is now a proportion of the window, which is the thing that actually
     varies between the people using this. The 4% it holds back is margin, not
     a limit -- the card needs to stop short of the bezel to read as a card.

     What the cap was really protecting is the SHAPE of a day cell, and that is
     answered where the shape is decided: cal.js sizes the cell ceiling from
     the measured column width, so cells stay wider than they are tall on any
     screen instead of on one. Prose is unaffected either way -- forms, lists
     and text keep --am-shell-max, because a reading column has an ideal width
     and a month grid does not. */
  --am-shell-max: 1180px;
  --am-shell-wide: 96vw;
  --am-shell-chrome: max(var(--am-shell-max), var(--am-shell-wide));
  --am-shell-pad: clamp(1rem, .5rem + 2vw, 2rem);

  /* --- How tall the page may be ---
     100dvh is the viewport; this is the part of it a page may actually use.

     They differ for administrators. The admin bar is position:fixed, so it
     contributes nothing to flow -- but WordPress pairs it with a rule on the
     html element, margin-top: 32px !important, and that margin IS in flow. A
     body of a full 100dvh therefore starts 32px down and ends 32px past the
     bottom of the window, so every page on the site scrolls by exactly the
     height of the admin bar no matter how little is on it.

     That is ugly on an ordinary page and actively wrong on the calendar
     dashboard, which sizes its day cells by asking "does the document
     overflow?" (cal.js, fitRows). It reads the admin bar's 32px as the grid
     being too tall, shrinks the rows -- and, because that first correction
     locks the direction it may correct in, never grows them back. The month
     ends up squeezed into the top two-thirds of the window with the leftover
     sitting empty underneath. Only administrators see it, because
     americon-core.php hides the bar from everybody else, which is why the
     screen looks right to the people it was built for and wrong to whoever
     built it.

     Subtracting the bar here rather than in cal.js fixes the phantom scroll on
     every page, and leaves the calendar's overflow question honest.

     The html rule above is quoted without braces on purpose. This comment is
     inside the :root block, and tools/lib/css-tokens.php reads that block by
     cutting at its first closing brace -- so a braced example here ends the
     palette early and every token below it silently vanishes from the contrast
     tests. tests/test-login-screen.php catches it, which is how this note came
     to be written. */
  --am-viewport: 100dvh;

  /* --- Focus ring --- */
  --am-ring: 0 0 0 2px var(--am-surface), 0 0 0 4px var(--am-accent);

  /* --- Motion --- */
  --am-ease: cubic-bezier(.2, .6, .3, 1);
  --am-speed: 160ms;
}

@media (prefers-reduced-motion: reduce) {
  :root { --am-speed: 0ms; }

  *,
  *::before,
  *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
}


/* =============================================================================
   2. BASE
   ========================================================================== */

html {
  scroll-behavior: smooth;
}

body {
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
  font-size: var(--am-text-md);
  line-height: 1.55;
  background-color: var(--am-canvas);
  color: var(--am-text);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;

  /* Sticky footer: header + main + footer fill at least the viewport. */
  min-height: var(--am-viewport);
  display: flex;
  flex-direction: column;
}

/* The admin bar's own height, taken off the page's. WordPress puts .admin-bar
   on <body> alongside the `html { margin-top }` this is compensating for, and
   switches that margin to 46px at its own 782px breakpoint -- so this follows
   it at the same width rather than at one of ours. */
body.admin-bar {
  --am-viewport: calc(100dvh - 32px);
}

@media screen and (max-width: 782px) {
  body.admin-bar {
    --am-viewport: calc(100dvh - 46px);
  }
}

h1, h2, h3, h4, h5, h6 {
  line-height: 1.2;
  letter-spacing: -.011em;
  text-wrap: balance;
  color: var(--am-text);
}

p, li {
  text-wrap: pretty;
}

a {
  color: var(--am-accent-ink);
  text-decoration-thickness: 1px;
  text-underline-offset: .15em;
  transition: color var(--am-speed) var(--am-ease);
}

a:hover {
  color: var(--am-danger-ink);
}

img,
svg {
  max-width: 100%;
  height: auto;
}

hr {
  border: 0;
  border-top: 1px solid var(--am-border);
  opacity: 1;
  margin-block: var(--am-gap-lg);
}

/* --- Focus -----------------------------------------------------------------
   The previous stylesheet removed every focus ring site-wide with
   `outline: none !important`, which left keyboard users with no visible
   position. :focus-visible gives the same clean look for mouse clicks while
   keeping keyboard focus obvious. Never delete this without a replacement. */

:focus-visible {
  outline: 2px solid var(--am-accent);
  outline-offset: 2px;
  border-radius: var(--am-radius-sm);
}

.btn:focus-visible,
.form-control:focus-visible,
.form-select:focus-visible,
.form-check-input:focus-visible {
  outline: none;
  box-shadow: var(--am-ring);
}

/* Skip link – first tab stop on every page. */
.am-skip-link {
  position: absolute;
  left: var(--am-gap);
  top: var(--am-gap);
  z-index: 1080;
  padding: .6rem 1rem;
  border-radius: var(--am-radius-sm);
  background: var(--am-red);
  color: #fff;
  font-weight: 600;
  box-shadow: var(--am-shadow);
  transform: translateY(calc(-100% - var(--am-gap) * 2));
  transition: transform var(--am-speed) var(--am-ease);
}

.am-skip-link:focus-visible {
  transform: translateY(0);
  color: #fff;
  outline-color: #fff;
}


/* =============================================================================
   3. LAYOUT
   =============================================================================
   Every class in this section is referenced by a template. Before this rewrite
   .americon-page-container, .americon-page-header, .americon-calendar-card,
   .site-main and .americon-login-page appeared in markup with zero rules
   behind them, so the "calendar card" was a bare div and the dashboard was the
   only unpadded page on the site.
   ========================================================================== */

/* A flat 3px rule, not the three-stop red-to-blue gradient it was. A gradient
   across the full width of the window is the most decorative element a page
   can open with -- it reads as a banner, and it put a second accent colour on
   screen before the first piece of content. One solid brand line does the same
   job of claiming the page and then stops asking for attention. */
.americon-topbar {
  height: 3px;
  background: var(--am-rule);
}

.site-main {
  flex: 1 0 auto;   /* pushes the footer to the bottom on short pages */
  display: block;
}

/* The one page shell. Templates pair it with nothing else. */
.americon-page-container {
  width: 100%;
  max-width: var(--am-shell-max);
  margin-inline: auto;
  padding-inline: var(--am-shell-pad);
  padding-block: clamp(1.25rem, .8rem + 1.8vw, 2.5rem);
}

.americon-page-container.is-wide {
  max-width: 1400px;
}

/* A shell inside a shell is one shell.
   -----------------------------------
   The templates open the container, and several shortcodes open one of their
   own as well -- [project_edit] does, because it is also placed by hand on
   pages that have no template around it. Where both happen the page pays for
   the padding twice: up to 5rem of nothing between the site header and the
   project title, and a reading measure inset by two gutters on each side.

   That is what put the empty band under the navbar on /project-details. It is
   fixed here, once, rather than by teaching each shortcode whether it has been
   wrapped -- a shortcode cannot know, and the two that guessed would disagree.

   The outer shell keeps its padding and its measure; the inner one becomes a
   plain block. Nothing needs to change in any template or any shortcode, and a
   shortcode dropped on a bare page still gets its shell, because then it is the
   outer one. */
.americon-page-container .americon-page-container {
  max-width: none;
  padding-inline: 0;
  padding-block: 0;
}

.americon-page-header {
  margin-bottom: var(--am-gap-lg);
  text-align: center;
}

.americon-page-header .entry-title,
.americon-page-title {
  font-size: var(--am-text-2xl);
  font-weight: 800;
  letter-spacing: -.02em;
  margin: 0;
  color: var(--am-text);
}

/* The red rule under page titles replaces the old all-red heading, which was
   heavy at large sizes and failed contrast against the light canvas.

   It was a 3.5rem x 3px pill, and a rounded lozenge floating under a heading
   is an ornament -- the pill radius in particular reads as a shape someone
   placed rather than a rule that belongs to the type. Now 2.25rem x 2px with
   effectively square ends: narrower than the shortest title it can sit under,
   so it stays a tick mark against the heading instead of a swash, and the same
   weight as a hairline rule elsewhere on the page. */
.americon-page-header .entry-title::after,
.americon-page-title::after {
  content: "";
  display: block;
  width: 2.25rem;
  height: 2px;
  margin: .6rem auto 0;
  border-radius: 1px;
  background: var(--am-rule);
}

/* A dek, not a paragraph: it should sit on one line on a desktop and only
   wrap when the viewport genuinely cannot hold it. 62ch was a prose measure
   and broke an 82-character sentence across two lines on a 1600px screen.
   text-wrap: balance splits it evenly rather than orphaning a word when it
   does wrap on a phone. */
.americon-page-subtitle,
.americon-page-header + .lead {
  margin: .75rem auto 0;
  max-width: min(100%, 92ch);
  color: var(--am-text-muted);
  font-size: var(--am-text-md);
  text-wrap: balance;
}


/* =============================================================================
   4. COMPONENTS
   ========================================================================== */

/* --- 4.1 Header & navigation --------------------------------------------- */

.site-header {
  background-color: var(--am-header-bg);
  border-bottom: 1px solid var(--am-border);
  position: sticky;
  top: 0;
  z-index: 1030;
  -webkit-backdrop-filter: saturate(1.4) blur(8px);
  backdrop-filter: saturate(1.4) blur(8px);
}

.site-header .navbar {
  padding-block: .5rem;
  background: transparent;
}

/* The nav band rides the chrome shell, not the reading column. The markup
   reuses .americon-page-container for its padding and centring, and that
   class caps at the reading column -- which put the logo 250px inboard of
   the calendar dashboard's own left edge, floating in the middle of a
   full-bleed bar. Overriding the cap here keeps the padding and drops the
   part that does not apply to chrome. */
.site-header .navbar > .americon-page-container {
  max-width: var(--am-shell-chrome);
}

.americon-navbar-logo {
  height: clamp(38px, 3.2vw, 52px);
  width: auto;
}

/* The logo art is white-on-transparent, so it needs a dark plate in light mode
   and none in dark mode. */
@media (prefers-color-scheme: light) {
  .navbar-brand {
    background: var(--am-blue);
    padding: .3rem .7rem;
    border-radius: var(--am-radius);
  }
}

.navbar-brand:focus-visible {
  outline-offset: 3px;
}

/* Role-aware action nav on the right of the header. */
.am-nav-list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--am-gap-xs);
  list-style: none;
  margin: 0;
  padding: 0;
}

.am-nav-link {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  padding: .4rem .75rem;
  border-radius: var(--am-radius-sm);
  border: 1px solid transparent;
  font-size: var(--am-text-sm);
  font-weight: 500;
  color: var(--am-text);
  /* These are buttons in every other respect -- padded, bordered on hover,
     one of them filled red -- and nothing here ever turned off the browser's
     default underline, so the whole header nav, "Sign in" included, rendered
     underlined. The hover plate is what carries affordance here. */
  text-decoration: none;
  white-space: nowrap;
  transition: background-color var(--am-speed) var(--am-ease),
              border-color var(--am-speed) var(--am-ease),
              color var(--am-speed) var(--am-ease);
}

.am-nav-link:hover {
  background: var(--am-surface-sunk);
  border-color: var(--am-border);
  color: var(--am-text);
}

/* Marks the page you are on. Paired with aria-current="page" in header.php so
   it is announced, not just coloured. */
.am-nav-link[aria-current="page"] {
  background: color-mix(in oklab, var(--am-accent) 12%, transparent);
  border-color: color-mix(in oklab, var(--am-accent) 35%, transparent);
  color: var(--am-accent-ink);
  font-weight: 600;
}

.am-nav-link--primary {
  background: var(--am-red);
  border-color: var(--am-red);
  color: #fff;
  font-weight: 600;
}

.am-nav-link--primary:hover {
  background: var(--am-red-dark);
  border-color: var(--am-red-dark);
  color: #fff;
}

.am-nav-link--danger {
  color: var(--am-danger-ink);
  border-color: color-mix(in oklab, var(--am-danger) 35%, transparent);
}

.am-nav-link--danger:hover {
  background: color-mix(in oklab, var(--am-danger) 10%, transparent);
  border-color: var(--am-danger);
  color: var(--am-danger-ink);
}

.am-nav-link--quiet {
  color: var(--am-text-muted);
}

/* Who is signed in. A span, not a link -- see the note in header.php.
   Baseline alignment rather than centre, so the name and the smaller role
   sit on one reading line instead of floating at different heights. */
.am-nav-identity {
  display: inline-flex;
  align-items: baseline;
  gap: .4rem;
  padding: .4rem .25rem;
  font-size: var(--am-text-sm);
  white-space: nowrap;
}

.am-nav-identity-name {
  font-weight: 600;
  color: var(--am-text);
}

/* --am-text-muted, the same ink Sign out carries. Already measured against
   every surface by tests/test-contrast.php, so this adds no new token to the
   palette and nothing to the coverage gate. */
.am-nav-identity-role {
  font-size: var(--am-text-xs);
  color: var(--am-text-muted);
}

/* Between the point the nav stops collapsing and roughly 1200px an
   administrator carries eight items plus a name, and .am-nav-list wraps to a
   second row. The role is the least load-bearing thing there -- the name
   still answers "whose session is this?" -- so it is what gives way.
   Below 992px the whole nav is a stacked menu with room for both. */
@media (min-width: 992px) and (max-width: 1199.98px) {
  .am-nav-identity-role { display: none; }
}

/* Divider before the sign-out control. */
.am-nav-sep {
  width: 1px;
  align-self: stretch;
  margin-inline: .35rem;
  background: var(--am-border);
}

@media (max-width: 991.98px) {
  .site-header { position: static; }
  .am-nav-sep { display: none; }

  /* The opened menu needs a row of its own.

     Bootstrap gives .navbar-collapse `flex-basis: 100%` exactly so it wraps
     under the brand and the toggler, and its own .container carries
     `flex-wrap: inherit` to let that happen. This header uses
     .americon-page-container instead, which is a plain block made into a flex
     row by .d-flex -- and flex-wrap then defaults to nowrap. So the panel had
     nowhere to go: opening it put a full-width item onto a single unwrappable
     row and crushed the logo and the toggler sharing it.

     Gated below the expand breakpoint. Above it the three items belong on one
     line, which is what nowrap is already doing correctly. */
  .site-header .navbar > .americon-page-container {
    flex-wrap: wrap;
  }

  /* Stacked, not a wrapping row of pills.

     .am-nav-list is a horizontal action bar at desktop widths and was
     inheriting that here, which is what made an opened menu read as a ragged
     block of buttons rather than as a menu -- .am-nav-link is
     `white-space: nowrap`, so the items could not even break evenly. */
  .am-nav-list {
    flex-direction: column;
    align-items: stretch;
    padding-block: var(--am-gap-sm);
  }

  /* Full-width rows. `display: flex` rather than the inline-flex above is what
     lets a link fill the row it now owns; the block padding is what takes it
     from roughly 32px to the 44px a thumb needs. */
  .am-nav-link {
    display: flex;
    justify-content: flex-start;
    padding-block: .7rem;
    white-space: normal;
  }
}


/* --- 4.2 Surfaces & cards ------------------------------------------------- */

.am-surface {
  background: var(--am-surface);
  border: 1px solid var(--am-border);
  border-radius: var(--am-radius-lg);
  box-shadow: var(--am-shadow);
}

.americon-calendar-section {
  display: block;
}

/* A layout wrapper only. The card chrome belongs to the calendar component
   itself (dashboard_plugin/assets/cal.css) so the shortcode renders finished
   wherever it is used -- putting it here would leave a bare calendar on any
   page that is not the dashboard template. Content that is *not* the calendar
   (a page body typed into the dashboard page) still gets a surface. */
.americon-calendar-card {
  display: block;
}

.americon-calendar-card > :not(.am-bid-calendar):only-child {
  display: block;
  padding: clamp(1rem, .7rem + 1.4vw, 1.75rem);
  background: var(--am-surface);
  border: 1px solid var(--am-border);
  border-radius: var(--am-radius-lg);
  box-shadow: var(--am-shadow);
}

/* --- 4.2c Calendar dashboard: the whole month on one screen ----------------
   A month grid is a scanning tool. Having to scroll to see the second half of
   the month defeats the point -- you cannot compare the 8th with the 24th if
   only one of them is on screen at a time.

   Nothing here constrains a height. An earlier version made this page a
   viewport-tall flex column and let the calendar take the leftover, which
   backfired: handed a fixed height it could not honour, FullCalendar clipped
   the month and put a scrollbar inside the grid. The height now comes out of
   the row height instead, which cal.js measures and adjusts -- see the "Fit
   mode" note in dashboard_plugin/assets/cal.css.

   What is left here is the other half of the job: reclaiming the vertical
   space between the title and the grid, which is spacing rather than content
   and is worth more as calendar rows. Every one of these rules removes
   padding; none removes anything from the page.

   Keyed off the template's own body class, not off :has(.am-bid-calendar
   .is-fit) as it was.

   Hanging the page chrome off the calendar's `fit` attribute made the whole
   layout depend on how one shortcode happened to be called, and it failed
   exactly that way in production: this page's BODY had a bare [bid_calendar]
   typed into it, so page-calendar-dashboard.php took its "the content already
   places a calendar" branch, the body's copy rendered with no attributes, and
   every rule below stopped matching at once. The result was a page that looked
   like its stylesheet had not loaded, with nothing to point at.

   The dependency was also backwards. These rules style the page's own
   furniture -- its title block, its tiles, its footer -- none of which the
   calendar owns or should get a vote on. WordPress puts
   page-template-page-calendar-dashboard on <body> for this template on every
   render, whatever the body content says, so that is the honest switch. It
   drops the :has() requirement as a side benefit.

   `fit` still exists and still matters: it is what cal.css tightens the card's
   own padding on, and what cal.js reads to decide whether to shrink day cells
   to fit the window. It just governs the calendar now instead of the page. */

/* The page's own spacing, ungated.

   These three sat inside the width query with everything else and so went
   missing on exactly the windows that needed them most. They take height out
   of margins, never out of content, and a narrow screen is not harmed by a
   tighter gap above the title -- so there is nothing here worth gating.

   The top pad is deliberately smaller than the bottom: the site header above
   it already ends in a rule, so the title does not need a further half-inch of
   air to read as separate from it, and every pixel spent there is one the
   month grid does not get. */
.page-template-page-calendar-dashboard .site-main > .americon-page-container {
  padding-top: var(--am-gap-sm);
  padding-bottom: var(--am-gap);
}

/* Every gap in the stack above the grid is now the small step rather than the
   full one. Individually these are 6px each; together with the tighter top pad
   they come to most of a calendar row, and a row of month is worth more than
   the air between a heading and the figures it introduces. */
.page-template-page-calendar-dashboard .site-main .americon-page-header,
.page-template-page-calendar-dashboard .site-main .am-stats {
  margin-bottom: var(--am-gap-sm);
}

.page-template-page-calendar-dashboard .site-main .americon-page-header .entry-title {
  font-size: clamp(1.6rem, 1.2rem + 1.4vw, 2.1rem);
}

/* Wider than the standard shell. A month grid is seven columns of names, and
   it is the only content on the page -- the 1400px cap that suits a form or a
   list of projects just leaves gutters here. Capped rather than fluid so the
   day cells stop growing on a very wide monitor, where a 300px-wide Tuesday
   helps nobody. */
.page-template-page-calendar-dashboard .americon-page-container.is-wide {
  max-width: var(--am-shell-wide);
}

/* Gated on width only. It used to carry `and (min-height: 44rem)` as well,
   which had the logic backwards: a window shorter than 704px was handed the
   ROOMY layout -- the full-height title block, the three-line tiles, the
   stacked footer -- and so was the one window guaranteed to scroll. The
   compaction is worth most exactly where there is least height to spend, and
   cal.js is already measuring the real overflow underneath it either way.

   Width is still a real gate -- below this the tiles wrap anyway, the view
   switch has no margin to sit in beside the title, and a phone scrolls
   comfortably -- but it was set at 62rem (992px), which is higher than the
   layout actually needs and high enough to be missed by an ordinary desktop.
   A 1465px window at 150% browser zoom is 977 CSS px: under the old gate by
   fifteen pixels, so the page rendered with its colours (ungated) and none of
   its compaction (gated), which looks like a half-loaded stylesheet rather
   than a breakpoint.

   56rem (896px) is what the contents want. Three tiles at their 13rem minimum
   plus gaps come to ~656px, and the centred title clears the absolutely
   positioned switch with room to spare, so nothing here collides before the
   tiles wrap of their own accord.

   THIS COMMENT USED TO SIT ABOVE `.site-main > .americon-page-container`, and
   it sat there UNOPENED -- the `/*` had been lost in an edit, so twenty lines
   of prose were live CSS tokens. A stylesheet cannot report that: the parser
   simply keeps collecting the garbage as a selector prelude until it meets the
   next `{`, swallows the rule that brace belonged to, and carries on. What was
   swallowed was the page container's own padding, which is why this page had
   40px of dead air above its title and below its subscribe band while every
   rule around it applied normally. */
@media (min-width: 56rem) {

  /* Footer: one line instead of three.

     At rest it is the single tallest block on this page after the calendar --
     167px of logo, brand, links and copyright, on a dashboard someone keeps
     open all day. Everything in it stays; it just stops being stacked, and the
     logo mark goes since the brand is spelled out in words beside it and again
     in the header above.

     The screens that share this are exactly the ones that ask to fit the
     window: the month grid, and both project editors (4.2d). A second copy of
     these four rules per page was the alternative -- 120px of footer is 120px
     whichever page it is under, and one of the copies would have drifted.

     They are keyed differently, and the difference matters. The dashboard has
     its page-template body class; the editors are found by the marker their
     own shortcodes print, because the equivalent body class is not reliably
     there. See 4.2d.

     Keyed on .am-editor and not on .is-enhanced, unlike 4.2d: a shorter footer
     is right on this screen whether or not the script ran, and it costs
     nothing if the page below it scrolls. */
  .page-template-page-calendar-dashboard .americon-footer,
  body:has(.am-editor) .americon-footer {
    margin-top: var(--am-gap);
    padding-block: .55rem;
  }

  .page-template-page-calendar-dashboard .americon-footer .americon-footer-inner,
  body:has(.am-editor) .americon-footer .americon-footer-inner {
    grid-template-columns: auto 1fr auto;
    gap: 0 var(--am-gap-lg);
  }

  .page-template-page-calendar-dashboard .americon-footer .americon-footer-logo-link,
  body:has(.am-editor) .americon-footer .americon-footer-logo-link {
    display: none;
  }

  /* The copyright spans every column by default, which puts it on a second row
     under its own divider -- 52px for one line of small grey text. In the
     third column it rides along on the row that already exists. */
  .page-template-page-calendar-dashboard .americon-footer .americon-footer-meta,
  body:has(.am-editor) .americon-footer .americon-footer-meta {
    grid-column: auto;
    padding-top: 0;
    border-top: 0;
    text-align: right;
    font-size: var(--am-text-xs);
    white-space: nowrap;
  }

  /* Links move off the right edge now that the copyright is sitting there. */
  .page-template-page-calendar-dashboard .americon-footer .americon-footer-links,
  body:has(.am-editor) .americon-footer .americon-footer-links {
    justify-content: center;
  }
}


/* --- 4.2d The project editors: the form on one screen ----------------------
   Both of them: the create form and the edit screen. They are the same
   component -- one <form> split into sections behind a rail, printed by two
   shortcodes -- and this used to be gated on .am-editor--create, so only one of
   them got the treatment. The edit screen is the LONGER of the two (it carries
   a draft bar, a bid-state bar, seven sections and an email preview), which
   made it the one that needed this and the one that did not have it.

   The rail is already one section at a time -- that is what the rail is for.
   But the page around it still grew to whatever the open section happened to
   be, so Overview alone came to roughly a viewport and a hundred pixels: the
   window scrolled, and what went under the fold was the Save button and the
   footer. Scrolling a seven-section form to reach a button that the rail exists
   to make unnecessary is the worst of both layouts.

   So the page is given the window's height and the OPEN SECTION becomes the
   only thing that can scroll. The head, the state bars, the rail, the
   required-fields status and the save bar are then fixed furniture: whatever
   section you are in and however long it is, the button is in the same place,
   the rail still says what is left to fill, and a draft still says it is a
   draft -- which is exactly the warning that used to scroll away on the invite
   section, the longest panel on the screen.

   Nothing is hidden or dropped to achieve that. Every rule here either takes
   height out of padding or moves an overflow from the window into the panel.

   WHY :has(), AND NOT THE BODY CLASS
   ----------------------------------
   Two separate reasons, and both land on the same selector.

   First: the cap must not apply when the editor script has not run. Without it
   every section is on the page at once by design (see the no-JS note in
   americon-project-editor.css) and that screen is legitimately taller than the
   window -- capping it would clip the form rather than scroll it. `is-enhanced`
   is the class the script adds to say it is driving, and it sits on a
   descendant, so the ancestors that carry the cap have to ask for it. Where
   :has() is unsupported the query simply does not match and the page scrolls
   exactly as it did before, which is the honest fallback.

   Second, and this is what these rules were first written against and shipped
   broken on: .page-template-page-create-project IS NOT ON THE BODY. WordPress
   only emits that class when the template is ASSIGNED to the page in its meta.
   The create page resolves through page-create-project.php by slug instead --
   the template hierarchy's page-{slug}.php rule -- which renders the identical
   screen and sets no such class. So every rule here matched nothing, on a page
   that was otherwise correct, with nothing on screen to say why. (The dashboard
   above keeps its class because that page is assigned; Americon -> Site Setup
   in wp-admin is what reports the difference.)

   .am-editor is printed by the shortcode that prints the form -- either of
   them -- so it cannot be absent while the form is present, and it does not
   care which template or which page the form was placed on. (It was
   .am-editor--create here, which is the create form's modifier; dropping the
   modifier is what brings the edit screen in.)

   The short-window gate is real. Below ~34rem of height the head, the rail and
   the save bar leave the panel too little to be worth locking, and a page that
   scrolls beats a 90px porthole. Below 64rem of width the rail lies down as a
   horizontal strip (americon-project-editor.css) and the two-column geometry
   this depends on is gone. */

@media (min-width: 64rem) and (min-height: 34rem) {

  /* :not(.is-flowing) on every rule of the cap.

     A section that is a list rather than a form asks for the opposite of the
     cap: the messages under Mail are read down a table, and a table in a
     porthole a third of the window tall is read three rows at a time, with a
     second scrollbar inside the page's. The editor marks such a section
     data-am-flow; the script puts is-flowing on the root while it is open (the
     server prints it for the section the page opens on), and every rule of the
     cap steps aside. The page then scrolls as any other list screen does, the
     rail returns to being sticky beside it (americon-project-editor.css) and
     nothing here is undone piecemeal -- the rules simply do not match, which
     is the same fallback the block already has for a browser without :has().

     Gated on the class rather than on the panel because the cap lives on
     body and .site-main, three ancestors above the panel that asks for it. */

  /* The cap itself. min-height would let the page grow again the moment the
     panel content exceeded it, which is the whole thing being fixed; overflow
     is hidden because a scrollbar on the window here would mean a pixel has
     escaped the panel, and hiding it is better than a 3px scroll that moves
     the header. */
  body:has(.am-editor.is-enhanced:not(.is-flowing)) {
    height: var(--am-viewport);
    overflow: hidden;
  }

  /* Each link in the chain has to be allowed to shrink below its content, or
     min-content sizing quietly re-inflates the page from the inside. */
  body:has(.am-editor.is-enhanced:not(.is-flowing)) .site-main {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
  }

  body:has(.am-editor.is-enhanced:not(.is-flowing)) .site-main > .americon-page-container {
    display: flex;
    flex: 1 1 auto;
    min-height: 0;
    padding-block: var(--am-gap-sm);
  }

  /* The edit screen arrives wrapped in the template's shell as well as its own
     (see "a shell inside a shell"), so the inner one has to carry the column
     too -- otherwise the flex chain stops one element short of the editor and
     the whole cap silently does nothing. */
  body:has(.am-editor.is-enhanced:not(.is-flowing)) .americon-page-container > .americon-page-container {
    display: flex;
    flex: 1 1 auto;
    min-height: 0;
  }

  .am-editor.is-enhanced:not(.is-flowing) {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
    gap: var(--am-gap-sm);
  }

  /* Everything except the body keeps its own height; the body absorbs every
     pixel of the difference. Left to the default shrink factor the browser
     would take the overflow proportionally out of all of them, which is how a
     title ends up clipped instead of a form scrolled.

     `> *` rather than a list of classes. The edit screen puts a variable stack
     above the body -- a save notice, a draft bar, a bid-state bar, the
     "this is public" question -- any of which may be absent, and a list here
     would go stale the first time one more is added. There is exactly one
     child that is meant to stretch, so name that one and let the rest be
     furniture. */
  .am-editor.is-enhanced:not(.is-flowing) > * {
    flex: none;
  }

  .am-editor.is-enhanced:not(.is-flowing) > .am-editor-body {
    flex: 1 1 auto;
    min-height: 0;
    align-items: stretch;   /* the rail and the panels both fill the row */
  }

  /* A page title has the whole screen under it saying which project this is.
     This is the same trade the dashboard makes with its own title.

     Not gated on is-flowing, the two of them: they compact the head, they are
     not part of the cap, and a title that changed size as the rail moved
     between Mail and Overview would be the head jumping for no reason. */
  .am-editor.is-enhanced .am-editor-title {
    font-size: var(--am-text-xl);
  }

  .am-editor.is-enhanced .am-editor-head {
    padding-bottom: var(--am-gap-sm);
  }

  /* The draft bar and the other state bars are compacted to match, in
     americon-project-editor.css under this same gate -- with the component
     they belong to, rather than a second set of paddings out here. */

  /* Sticky was the answer to a scrolling page. There is not one any more, and a
     sticky element inside a fixed-height column just adds an offset that no
     longer corresponds to anything. */
  .am-editor.is-enhanced:not(.is-flowing) .am-editor-rail {
    position: static;
    max-height: 100%;
    align-content: start;
  }

  .am-editor.is-enhanced:not(.is-flowing) .am-editor-panels {
    min-height: 0;
    grid-template-rows: minmax(0, 1fr);
  }

  /* The form is the panel plus the save bar, in that order, and only those two
     are ever laid out: the inactive sections carry `hidden` and the section
     field is an <input type="hidden">, none of which generate a box. The panel
     takes the space, the save bar takes what it needs.

     On the edit screen the form itself is hidden when the open section is one
     of the two that live outside it (the invite email and Delete, each with a
     form of its own). Those panels are then the only child of .am-editor-panels
     and land in the same 1fr row, so they scroll exactly as the others do and
     the save bar is off screen rather than floating under Delete. */
  .am-editor.is-enhanced:not(.is-flowing) .am-editor-form {
    min-height: 0;
    grid-template-rows: minmax(0, 1fr) auto;
    gap: var(--am-gap);
  }

  /* The one scroll on the screen, and it belongs to the section you are
     reading. align-content because a grid with room to spare stretches its
     rows by default -- which would pull the two visibility cards to half the
     panel's height on a tall window.

     Thin, as the rail's own overflow already is: a full-width scrollbar down
     the inside edge of a panel reads as a second column. */
  .am-editor.is-enhanced:not(.is-flowing) .am-editor-panel {
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: thin;
    align-content: start;
    padding-right: .4rem;   /* the scrollbar's lane, off the fields */
  }

  /* Say that it scrolls.

     The cap does not shrink a section, it moves the overflow into the panel --
     but nothing on screen said so. A panel simply stopped mid-sentence, or
     halfway down a button, with the page around it visibly not moving, and that
     reads as a rendering fault rather than as more content below. It is the
     same cue the plan-file list already carries, for the same reason.

     The fade is painted by a mask, not an ::after overlay: this is the bottom
     of a form, so the last few pixels are usually a field or a button, and a
     gradient laid over them paints across a focus ring even with
     pointer-events: none. A mask is applied to the element's own box and
     therefore stays at the visible bottom edge while the content scrolls
     underneath it. Clipped content is still rendered, so find-in-page and copy
     are unaffected.

     Driven by a class, not by scroll(), because the cue has to be WRONG in two
     directions before it is worth having: absent when the panel does not
     overflow at all -- a permanent fade over a short Photo section would be a
     lie about content that is not there -- and lifted once the last row is
     reached, so the end of the form is not left permanently dimmed. Neither is
     answerable in CSS: "does this element overflow" has no selector. So the
     editor script measures and toggles (americon-project-editor.js, "Scroll
     cue"), which also puts the cue in Firefox and Safari rather than only where
     scroll-driven animations have landed. With the script absent there is no
     panel scroll to cue -- this whole block is gated on is-enhanced. */
  .am-editor.is-enhanced:not(.is-flowing) .am-editor-panel.is-more-below {
    -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 2.5rem), transparent 100%);
            mask-image: linear-gradient(to bottom, #000 calc(100% - 2.5rem), transparent 100%);
  }

  /* Both of these were the save bar coping with being at the end of a long
     page. It is now the bottom row of a grid that is already the height of the
     window, which is the position sticky was approximating. */
  .am-editor.is-enhanced:not(.is-flowing) .am-savebar {
    position: static;
    margin-top: 0;
  }

  /* The trade picker gives up its own scroll here.

     Its 14rem cap exists because the list runs to several dozen trades on a
     page that scrolls. This panel scrolls, so the cap buys nothing and costs
     something specific: two scrollbars a few pixels apart down the same edge,
     one of which moves the list and one of which moves the form. Which one the
     wheel drives then depends on where the pointer happens to be, and there is
     no way to tell them apart by looking.

     The filter and its count scroll away with the list, which is the trade.
     They sit directly above it, so they are on screen whenever the list is
     where you are reading. */
  .am-editor.is-enhanced:not(.is-flowing) .am-trade-picker {
    max-height: none;
    overflow-y: visible;
  }

  /* The email preview gives up its clamp for the same reason.

     It opens at a fraction of its height because the panel is for copying the
     message, not reading it, and 600px of email would bury the buttons. In a
     panel that scrolls that argument still holds -- so the clamp stays -- but
     the EXPANDED state must not add a second scroller inside the first. Once
     "Show the whole email" has been pressed the preview is as tall as it is,
     and the panel's own scroll is what moves it. */
  .am-editor.is-enhanced:not(.is-flowing) .am-preview.is-open > .am-email-preview {
    max-height: none;
    overflow: visible;
  }

  /* And the plan-file list, third of the same trade.

     Its 26rem cap was written against a page that scrolled -- fifty rows being
     "the whole screen again", the problem the sections exist to remove. The
     panel scrolls, so fifty rows are fifty rows of a section that scrolls, and
     the cap costs exactly what the trade picker's did: a second scrollbar a few
     pixels from the first, with no way to tell by looking which one the wheel
     will drive.

     It was left in when those two were relaxed, and the panel fade above is
     what makes that an actual defect rather than an inconsistency -- the list's
     own fade and the panel's would stack a couple of rows apart down the same
     edge, two dimmed bands saying the same thing about different scrollers. The
     mask and its scroll-driven animation go with the cap; the panel's cue now
     covers this list as it covers every other section.

     The filebar scrolls away with the list, which is the same trade already
     accepted for the trade picker's filter: it sits directly above the rows, so
     it is on screen whenever the list is where you are reading. */
  .am-editor.is-enhanced:not(.is-flowing) .am-planlist--scroll {
    max-height: none;
    overflow-y: visible;
    padding: 0;
    margin: 0;
    animation: none;
    -webkit-mask-image: none;
            mask-image: none;
  }
}

.am-section-title {
  font-size: var(--am-text-lg);
  font-weight: 700;
  margin: 0;
}

.am-eyebrow {
  font-size: var(--am-text-xs);
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--am-text-subtle);
}


/* --- 4.2b Dashboard stat tiles --------------------------------------------
   The headline figures above the calendar. Each tile is a link into the
   matching filtered list, so the number is a starting point and not an
   ornament. */

.am-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 13rem), 1fr));
  gap: var(--am-gap);
  list-style: none;
  margin: 0 0 var(--am-gap-lg);
  padding: 0;
}

/* Number first, its words beside it.

   This was the layout, but only inside a min-width media query, and it kept
   not arriving: gated at 62rem it missed an ordinary desktop at 150% zoom by
   fifteen CSS pixels, and the page rendered as three stacked lines per tile
   -- label, then figure, then hint -- which is 115px of card to present one
   number and its name.

   There is no width at which stacking is better, so there is no longer a
   query. Side by side is ~66px with every word still on the page, the figure
   reads larger relative to the tile rather than smaller, and the grid areas
   below wrap on a narrow screen without help. A layout this basic should not
   have been conditional on anything. */
.am-stat {
  display: grid;
  grid-template-columns: auto 1fr;
  grid-template-areas: "value label";
  align-items: center;
  column-gap: .65rem;
  row-gap: 0;
  height: 100%;
  padding: .4rem clamp(.875rem, .6rem + .8vw, 1.25rem);
  background: var(--am-surface);
  border: 1px solid var(--am-border);
  border-left: 3px solid var(--am-border-strong);
  border-radius: var(--am-radius);
  color: inherit;
  text-decoration: none;
  transition: border-color var(--am-speed) var(--am-ease),
              box-shadow var(--am-speed) var(--am-ease),
              transform var(--am-speed) var(--am-ease);
}

.am-stat:hover {
  color: inherit;
  box-shadow: var(--am-shadow);
  transform: translateY(-1px);
}

.am-stat:focus-visible {
  outline: none;
  box-shadow: var(--am-ring);
}

.am-stat-label {
  grid-area: label;
  /* Centred on the figure now that it is the only thing beside it. It sat on
     `end` to hug a hint line underneath that no longer renders -- left alone,
     the label would ride high against the bottom of a one-row cell. */
  align-self: center;
  font-size: var(--am-text-xs);
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--am-text-subtle);
}

.am-stat-value {
  grid-area: value;
  font-size: clamp(1.5rem, 1.25rem + .8vw, 2rem);
  font-weight: 750;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
  color: var(--am-text);
}

/* No longer rendered on the dashboard tiles -- the hint moved to the link's
   title attribute so it costs no height. Kept because the class is part of
   this component's public surface and a caller may still emit it; without a
   grid-area it simply flows after the label rather than claiming a row. */
.am-stat-hint {
  font-size: var(--am-text-xs);
  color: var(--am-text-muted);
}

/* --- Tile colour ----------------------------------------------------------
   Two independent facts, two independent classes.

   --am-stat-hue is set by the tone class and says which metric this is. The
   is-zero class says whether there is anything to act on, and that is what
   decides how much of the hue actually shows. Tone used to do both jobs by
   flipping to 'calm' at zero, which meant a quiet week rendered four identical
   grey tiles -- the row looked broken precisely when it was telling the truth.

   An active tile gets four cues, not one: a full-strength stripe, a tinted
   panel, the number in the hue, and a filled dot before the label. Colour is
   never the only carrier -- the uppercase label names the metric in text, which
   is also what discharges the light-mode contrast warning on amber (2.15:1 on
   white, too faint to be a lone signal at 3px). */
.am-stat--urgent  { --am-stat-hue: var(--am-danger); }
.am-stat--warn    { --am-stat-hue: var(--am-amber); }
.am-stat--public  { --am-stat-hue: var(--am-accent); }
.am-stat--private { --am-stat-hue: var(--am-teal); }

/* Grey on purpose, and it is the only tile whose hue means "not live". A draft
   is off the calendar, sends nothing and is nobody's deadline yet, so giving it
   one of the four signal colours would put work that is not out into the same
   visual class as work that is. It escalates to --urgent, and only then, when a
   draft has gone past its own bid date without ever being published -- the one
   version of this that acting today cannot fix. */
/* Green, and the only tile that is: it is the one figure on a summary row that
   is FINISHED. "3 bidding" is a promise and "3 bids in" is an envelope on the
   desk, and a coordinator scanning the row has to be able to tell the two apart
   without reading the labels under them. It is the same green .am-pill--in uses
   on the rows below, so the tile and the rows agree about what an ending looks
   like. */
.am-stat--go      { --am-stat-hue: var(--am-success); }

.am-stat--draft   { --am-stat-hue: var(--am-text-subtle); }

/* Retained so an older cached page, or any caller still passing tone="calm",
   renders as a deliberate neutral rather than inheriting no hue at all. */
.am-stat--calm    { --am-stat-hue: var(--am-border-strong); }

.am-stat {
  border-left-color: var(--am-stat-hue, var(--am-border-strong));
}

.am-stat-value {
  color: var(--am-stat-hue, var(--am-text));
}

/* The dot is redundant with the stripe on purpose: it survives a forced-colours
   mode that flattens borders, and it gives the label a fixed left edge so the
   three tiles line up whether or not they are active. */
.am-stat-label::before {
  content: "";
  display: inline-block;
  width: .5rem;
  height: .5rem;
  margin-right: .4rem;
  border-radius: var(--am-radius-pill);
  background: var(--am-stat-hue, var(--am-border-strong));
  vertical-align: baseline;
}

.am-stat:not(.is-zero) {
  background:
    linear-gradient(0deg,
      color-mix(in oklab, var(--am-stat-hue) 7%, transparent),
      color-mix(in oklab, var(--am-stat-hue) 7%, transparent)),
    var(--am-surface);
  border-color: color-mix(in oklab, var(--am-stat-hue) 30%, var(--am-border));
  border-left-color: var(--am-stat-hue);
}

/* Zero: the hue recedes to a trace of itself rather than vanishing. Enough to
   keep the four tiles telling themselves apart at a glance; not enough to read
   as "something needs you". The number goes back to plain ink, because a big
   coloured 0 is the exact false alarm this is avoiding. */
.am-stat.is-zero {
  border-left-color: color-mix(in oklab, var(--am-stat-hue) 32%, var(--am-border));
}

.am-stat.is-zero .am-stat-value {
  color: var(--am-text-subtle);
}

.am-stat.is-zero .am-stat-label::before {
  background: color-mix(in oklab, var(--am-stat-hue) 38%, transparent);
}

/* Urgency earns motion; the other tones do not. Same 2.4s period as the
   calendar's own "soon" marker, so the two surfaces agree about what imminent
   looks like.

   Deliberately NOT named am-pulse: cal.css already defines a keyframes of that
   name for the day markers, and it loads after this file. Keyframes are global
   and last-definition-wins, so reusing the name would have handed the dot
   whatever amplitude the calendar happened to want -- a cross-file override
   with no selector anywhere to explain it. */
@media (prefers-reduced-motion: no-preference) {
  .am-stat--urgent:not(.is-zero) .am-stat-label::before {
    animation: am-stat-dot-pulse 2.4s var(--am-ease) infinite;
  }
}

@keyframes am-stat-dot-pulse {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.25); }
}


/* --- 4.2c Deadlines -------------------------------------------------------
   Rendered by americon_render_deadline(). Absolute time and zone come from
   the server; the relative half is refreshed in place by
   mu-plugins/assets/americon-deadlines.js. */

.am-deadline {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .1rem .4rem;
}

.am-deadline-abs {
  font-variant-numeric: tabular-nums;
}

.am-deadline-rel {
  display: inline-flex;
  align-items: center;
  gap: .3em;
  font-size: var(--am-text-xs);
  font-weight: 650;
  padding: .1rem .45rem;
  border-radius: var(--am-radius-pill);
  white-space: nowrap;
  background: color-mix(in oklab, var(--am-text-muted) 12%, transparent);
  color: var(--am-text-muted);
}

/* The urgency glyph. It inherits the pill's colour, so it needs no per-state
   rule of its own -- the three tints below carry it.

   It exists because those tints were the ONLY thing separating "closing soon"
   from "closes today", which is invisible in greyscale, on a printed bid list,
   and to anyone with a colour vision deficiency. The pill also carries the
   urgency as visually hidden text for screen readers; see
   americon_render_deadline(). */
.am-deadline-ico {
  font-size: .9em;
}

/* Emitted since this module was written, styled by nothing until now: an
   unparseable date rendered a bare em dash with the same weight as a real
   deadline beside it. */
.am-deadline--unknown {
  color: var(--am-text-subtle);
}

/* Amber for "within 72 hours", red for "before midnight tonight". */
.am-deadline--soon .am-deadline-rel {
  background: color-mix(in oklab, var(--am-amber) 20%, transparent);
  color: var(--am-warn-ink);
}

.am-deadline--today .am-deadline-rel {
  background: color-mix(in oklab, var(--am-red) 16%, transparent);
  color: var(--am-red-dark);
  color: light-dark(var(--am-red-dark), var(--am-red-bright));
}

.am-deadline--past .am-deadline-rel {
  background: color-mix(in oklab, var(--am-text-muted) 12%, transparent);
  color: var(--am-text-subtle);
}

.am-deadline--past .am-deadline-abs {
  text-decoration: line-through;
  text-decoration-thickness: 1px;
  opacity: .75;
}


/* Active-filter chip, shown when a dashboard tile narrowed the list to a
   window the preset row cannot express. It used to lift itself with a
   negative margin tuned to the filter panel's old --am-gap-lg spacing; the
   panel now closes on --am-gap and the chip needs no correction. */
.am-active-filter {
  margin: 0 0 var(--am-gap-sm);
}

.am-chip {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  padding: .25rem .35rem .25rem .7rem;
  border-radius: var(--am-radius-pill);
  background: color-mix(in oklab, var(--am-accent) 12%, transparent);
  color: var(--am-accent-ink);
  font-size: var(--am-text-sm);
  font-weight: 600;
}

.am-chip a {
  display: inline-grid;
  place-items: center;
  width: 1.25rem;
  height: 1.25rem;
  border-radius: 50%;
  color: inherit;
  text-decoration: none;
  line-height: 1;
}

.am-chip a:hover {
  background: color-mix(in oklab, var(--am-accent) 25%, transparent);
  color: inherit;
}


/* --- 4.3 Definition grids (project details) -------------------------------
   Replaces stacks of <div class="small text-muted"> with a real <dl>, which
   screen readers announce as term/description pairs. */

.am-dl {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr));
  gap: var(--am-gap) var(--am-gap-lg);
  margin: 0;
}

.am-dl > div {
  min-width: 0;
}

.am-dl dt {
  font-size: var(--am-text-xs);
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--am-text-subtle);
  margin-bottom: .15rem;
}

.am-dl dd {
  margin: 0;
  font-size: var(--am-text-md);
  color: var(--am-text);
  overflow-wrap: anywhere;
}

.am-dl dd.is-strong {
  font-weight: 650;
}


/* --- 4.3a Scope of work ---------------------------------------------------
   The one block of prose on the project page. Everything around it is a
   label/value pair set at --am-text-md in a grid; this is sentences, and it
   needs the two things sentences need and a grid cell does not give them: a
   measure short enough to read, and room between the lines.

   Rendered by americon_scope_html() in mu-plugins/americon-scope.php, which
   emits nothing at all when the project has no scope -- so there is no empty
   state here to style. The heading above it is dropped with it. */

.am-scope {
  max-width: 68ch;   /* longer lines lose the reader on the way back */
  color: var(--am-text);
  font-size: var(--am-text-md);
  line-height: 1.65;
}

.am-scope p {
  margin: 0 0 .75rem;
  overflow-wrap: anywhere;   /* a pasted spec line can be one long token */
}

.am-scope p:last-child {
  margin-bottom: 0;
}


/* --- 4.3b The project board ------------------------------------------------
   .am-board wraps everything the two list shortcodes emit, and exists so this
   file can recognise a list screen from the outside. */

/* SURFACES: ONE, NOT THREE.

   index.php renders every page body inside .americon-entry -- a bordered,
   padded, --am-surface card. That is right for an article and wrong for a
   list: it put a panel around a panel around a stack of row cards, so the
   filter bar and the rows were drawn on the same colour as the thing
   containing them, and the whole list ran at a narrower measure than every
   other screen on the site because the entry's padding stacked on top of the
   page container's. Flattened, the rows sit on the canvas and their own
   borders are the only edges on the page that mean anything. */
.americon-entry:has(.am-board) {
  padding: 0;
  background: none;
  border: 0;
  border-radius: 0;
  margin-bottom: 0;
}

/* TITLE LEFT, NOT CENTRED.

   The same argument the dashboard header makes further down, and it applies
   with more force here: a centred title cannot share a line with anything, so
   the title, the dek and the brand rule each needed a row of their own before
   the first project appeared. Left-aligned they cost two rows and the red
   rule becomes the block's left edge instead of a swash under it. This is
   what every list screen worth copying does, for the same reason -- it leaves
   the most room for the thing the page is actually for. */
.am-board > .americon-page-header,
.americon-entry:has(.am-board) > .americon-page-header {
  text-align: left;
  margin-bottom: var(--am-gap);
  border-left: 3px solid var(--am-rule);
  padding-left: .7rem;
}

/* .americon-entry sizes its .entry-title down to --am-text-xl, which is right
   for an article heading in a stack of articles and wrong for the title of a
   screen. With the entry card gone the heading has to be given back the page
   scale it would have had anywhere else. */
.americon-entry:has(.am-board) > .americon-page-header .entry-title {
  font-size: var(--am-text-2xl);
}

/* The centred tick under the title is what that left edge replaced. */
.am-board > .americon-page-header .americon-page-title::after,
.americon-entry:has(.am-board) > .americon-page-header .entry-title::after,
.americon-entry:has(.am-board) > .americon-page-header .americon-page-title::after {
  display: none;
}

/* The dek. In the bare path the page printed the heading and the shortcode
   printed this, so it is the board's own first child; in the full path it
   sits inside the header the shortcode built. Both want the same left-aligned
   treatment and they want different margins. */
.am-board > .americon-page-subtitle {
  margin: 0 0 var(--am-gap-lg);
  max-width: none;
  text-align: left;
  font-size: var(--am-text-sm);
}

.am-board > .americon-page-header .americon-page-subtitle {
  margin: .3rem 0 0;
  max-width: none;
  text-align: left;
  font-size: var(--am-text-sm);
}


/* --- 4.4 Project list rows ------------------------------------------------
   ONE PROJECT, ONE LINE, FIVE COLUMNS.

   Two problems, one shape.

   FINDING THINGS. The row this replaces was a title with everything else --
   city, state, trades, deadline, plan-holder warning -- run into one wrapped
   grey sentence and a badge pinned right. Every fact was present and none
   were findable: the due date, the single thing a bid list exists to answer,
   sat at a different horizontal position in every row. Fixed column tracks,
   shared by every row and by the caption strip above them, put it in the same
   place each time, so "what closes first" is read down a column instead of
   row by row.

   FITTING THINGS. A bid board is read fifty rows at a time. The first draft of
   those columns kept the old card-per-row treatment -- a border, a radius and
   a 10px gap around each -- and cost about 90px a project, which is ten
   projects on a laptop and a very long scroll for a busy month. One surface
   with hairline separators, one line per project, is about 40px. Same data,
   same columns, twice the board.

   URGENCY IS THE ONLY THING ALLOWED TO SHOUT. Everything here is deliberately
   quiet except the deadline, which gets a coloured spine, a tinted row and a
   tinted pill with its own glyph: red today, amber inside three days. Three
   carriers rather than one, because colour alone is invisible in greyscale,
   on a printed bid list and to a reader with a colour vision deficiency.

   Divs rather than a <table>: this has to collapse onto a phone, which a
   table cannot do without abandoning its semantics, and the whole row is one
   link. The alignment a table would have given comes from --am-row-cols. */

.am-list {
  /* One surface for the whole board. The rows are no longer cards -- they are
     hairline-separated bands inside this one, which is what buys the density
     and what makes the column captions read as a header rather than as a
     floating line of small text. */
  background: var(--am-surface);
  border: 1px solid var(--am-border);
  border-radius: var(--am-radius);
  overflow: hidden;

  /* THE COLUMN DEFINITION, WRITTEN ONCE.
     The caption strip and every row read this, so they cannot drift apart --
     the failure mode of every table built out of divs. Project takes the
     slack; the rest are fixed, because a column only aligns if it is the same
     width in every row. */
  --am-row-cols:  minmax(0, 1fr) 12rem 13.5rem 1.25rem;
  --am-row-areas: "main loc due cue";

  --am-row-pad: clamp(.75rem, .55rem + .6vw, 1rem);

  /* Staff get one more track, for the plan-holder alert. A signed-out visitor
     never sees the column at all rather than seeing an empty one -- the width
     goes back to the project name, which is the cell that always wants it. */


  /* The tick-box gutter. Fixed rather than `auto` because the caption strip
     has no tick box and still has to start where the rows' content does. */
  --am-select-col: 1rem;
  --am-row-gutter: 0rem;
}

/* The flags track is sized for what actually lands in it, which is two badges
   at once far more often than one: a draft with nobody holding the plans yet
   is the normal state of a project on the day it is created, and at 9.5rem
   that pair ran clean across the deadline beside it. DRAFT is ~4.8rem and
   "No plan holders" ~8.2rem, so 13.5rem holds the pair and the cell wraps
   rather than spills when a third alert turns up.

   The width comes out of Location, which was carrying 13rem to print "La
   Crosse, WI", and the deadline takes a little more for the same reason the
   alerts did: "Aug 28, 12:00 PM" plus its relative pill does not fit 12rem. */
.am-list--flags {
  --am-row-cols:  minmax(0, 1fr) 11rem 13.5rem 13.5rem 1.25rem;
  --am-row-areas: "main loc status due cue";
}

.am-list[data-am-selectable] {
  --am-row-gutter: calc(var(--am-select-col) + var(--am-gap-sm));
}

/* Column captions. Only meaningful while the columns exist, so they go at the
   first breakpoint; below it each cell that needs one names itself. */
.am-list-head {
  display: grid;
  grid-template-columns: var(--am-row-cols);
  align-items: center;
  gap: var(--am-gap);

  /* Rows open with a 3px spine and close with the container's own edge, and
     they carry the tick-box gutter. The captions have neither and have to
     make up the difference by hand, or they sit a hair left of the columns
     they name. */
  padding: .4rem var(--am-row-pad) .4rem
           calc(var(--am-row-pad) + 3px + var(--am-row-gutter));
  background: var(--am-surface-sunk);
  border-bottom: 1px solid var(--am-border);
  font-size: var(--am-text-xs);
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--am-text-subtle);
}

.am-list-head > :last-child { text-align: right; }

/* THE CAPTIONS ARE CONTROLS.
   A strip that looks like a table header is read as one, and a table header
   that does nothing when pressed is worse than no header at all -- so the
   three columns that can be ordered are links, and the one that cannot
   (Alerts) is deliberately left as plain text so the difference is visible
   before you click. */
.am-list-sort {
  display: inline-flex;
  align-items: center;
  gap: .25rem;
  color: inherit;
  text-decoration: none;
  letter-spacing: inherit;
  border-radius: var(--am-radius-sm);
}

.am-list-sort:hover {
  color: var(--am-text);
  text-decoration: underline;
}

.am-list-sort:focus-visible {
  outline: 2px solid var(--am-accent);
  outline-offset: 2px;
  color: var(--am-text);
}

/* The column currently in force. Ink rather than a background: the strip is
   already a tinted band, and a second fill inside it reads as a button. */
.am-list-sort[data-dir] {
  color: var(--am-text);
}

.am-sort-ico {
  /* One glyph for both directions, turned over. The arrow only ever has to
     mean "this end of the column first". */
  transition: transform var(--am-speed) var(--am-ease);
}

.am-list-sort[data-dir="desc"] .am-sort-ico {
  transform: rotate(180deg);
}

/* The row shell: tick box, spine, separator. Everything that is about the row
   as a whole rather than about its cells, and everything that must sit
   outside the <a>. */
.am-list-item {
  display: grid;
  grid-template-columns: var(--am-select-col) minmax(0, 1fr);
  align-items: center;
  gap: var(--am-gap-sm);
  padding-inline-start: var(--am-row-pad);

  /* Transparent by default and coloured by the :has() rules below. Reserved
     rather than added on urgency, so a row does not shift sideways by 3px the
     moment its deadline crosses into "soon" -- which americon-deadlines.js
     will do, live, on a page left open. */
  border-inline-start: 3px solid transparent;
  border-bottom: 1px solid var(--am-border);
  transition: background-color var(--am-speed) var(--am-ease);
}

/* No checkbox (signed-out visitor): the row takes the full width rather than
   leaving an empty gutter. */
.am-list-item:not(:has(.am-select)) {
  grid-template-columns: minmax(0, 1fr);
}

.am-list-item:last-child {
  border-bottom: 0;
}

/* URGENCY.
   Driven by the class americon-deadlines.js maintains on the pill, so a board
   left open overnight re-colours its own rows as the deadlines cross over.
   The tints are deliberately weak -- 6% -- because every row on a busy week
   is one of them, and a list where half the rows are shouting is a list where
   none of them are. The spine is what carries it at a glance; the tint only
   has to make the spine look intentional. */
.am-list-item:has(.am-deadline--today) {
  border-inline-start-color: var(--am-danger);
  background: color-mix(in oklab, var(--am-danger) 6%, transparent);
}

.am-list-item:has(.am-deadline--soon) {
  border-inline-start-color: var(--am-amber);
  background: color-mix(in oklab, var(--am-amber) 7%, transparent);
}

.am-list-item:has(.am-deadline--past) {
  border-inline-start-color: var(--am-border-strong);
}

/* A closed bid is still worth listing and is no longer worth reading first. */
.am-list-item:has(.am-deadline--past) .am-list-title {
  color: var(--am-text-muted);
}

.am-list-item:hover {
  background: var(--am-surface-sunk);
}

/* Hover has to deepen the urgency tint rather than replace it: the plain rule
   above would wash a "closes today" row back to neutral under the pointer,
   which is the row it is least acceptable to lose. */
.am-list-item:hover:has(.am-deadline--today) {
  background: color-mix(in oklab, var(--am-danger) 13%, transparent);
}

.am-list-item:hover:has(.am-deadline--soon) {
  background: color-mix(in oklab, var(--am-amber) 15%, transparent);
}

/* SELECTED.
   A ticked row is tinted, not just ticked. The box is a 1em square in a
   ninety-pixel-wide gutter; on a screen searched five times for the same
   seven hundred contacts, "have I already taken this one" has to be
   answerable from across the room, which is what every mail client and
   file manager does with a selected row. Last so it wins over urgency:
   what is chosen matters more than when it closes, and the deadline pill
   still says the date. */
.am-list-item:has(.am-select:checked) {
  border-inline-start-color: var(--am-accent);
  background: color-mix(in oklab, var(--am-accent) 9%, transparent);
}

.am-list-item:hover:has(.am-select:checked) {
  background: color-mix(in oklab, var(--am-accent) 15%, transparent);
}

.am-list-row {
  display: grid;
  grid-template-columns: var(--am-row-cols);
  grid-template-areas: var(--am-row-areas);
  align-items: center;
  gap: var(--am-gap);
  min-width: 0;
  padding: .5rem var(--am-row-pad) .5rem 0;
  color: inherit;
  text-decoration: none;
}

/* Inset, not the shared --am-ring: that ring draws a --am-surface halo around
   the focused element, which reads as a gap punched in a table. */
.am-list-row:focus-visible {
  outline: 2px solid var(--am-accent);
  outline-offset: -3px;
  border-radius: var(--am-radius-sm);
}

.am-list-title {
  grid-area: main;
  min-width: 0;
  font-weight: 650;
  font-size: var(--am-text-md);
  color: var(--am-text);

  /* Clipped, not wrapped. A long project name wrapping to a second line would
     make one row in the list taller than the others, which is the thing this
     layout is for. The full name is the row's own link text, so it is still
     reachable -- and the details page it leads to opens with it. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.am-list-row:hover .am-list-title {
  color: var(--am-accent-ink);
  text-decoration: underline;
}

/* Scoped to .am-list, unlike its siblings: cal.js builds a span of the same
   class in the calendar's month-list view, which is a different component
   inside a flex line and must not inherit a grid area or this type scale. */
.am-list .am-list-loc {
  grid-area: loc;
  display: flex;
  align-items: center;
  gap: .35rem;
  min-width: 0;
  font-size: var(--am-text-sm);
  color: var(--am-text-muted);
}

.am-list .am-list-loc .am-i {
  flex: none;
  color: var(--am-text-subtle);
}

/* The ellipsis lives here rather than on the cell: a text node inside a flex
   container is an anonymous flex item and cannot be clipped. The untruncated
   value is on the cell's title attribute. */
.am-list .am-list-loc-text {
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Wrapping is the safety valve, not the layout. Every badge in here is
   white-space: nowrap, so a cell that can neither wrap nor clip has only one
   remaining way to fit its contents -- drawing them over the column next
   door, which is what a draft with no plan holders was doing to the deadline.

   The track above holds the two badges that appear together routinely; the
   third (contractors not yet told) is rare enough that costing that one row a
   second line is the right trade against squeezing every row to fit it.
   Overflow is hidden as the last resort, for a single badge wider than the
   whole column, where a clipped pill still beats an overlapped one. */
.am-list-status {
  grid-area: status;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .25rem .35rem;
  min-width: 0;
  overflow: hidden;
}

/* Badges keep their intrinsic width and move to the next line instead. A
   shrunk badge does not reflow -- its text is nowrap, so it simply hangs out
   of its own pill, which looks like a rendering fault rather than a tight
   column. */
.am-list-status > * {
  flex: none;
}

.am-list-due {
  grid-area: due;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .1rem .4rem;
  min-width: 0;
  overflow: hidden;
  font-size: var(--am-text-sm);
}

/* One line for the deadline itself, never wrapped: the shared .am-deadline is
   inline-flex with wrap, which would drop the pill onto a second line and
   take the whole row's height with it. The wrap above is for the cell's other
   child -- the "Bid due" caption, which becomes visible below the breakpoint
   where the column headings go away. */
.am-list-due .am-deadline {
  flex-wrap: nowrap;
  align-items: center;
  gap: .4rem;
  min-width: 0;
}

.am-list-due .am-deadline-abs {
  color: var(--am-text);
  white-space: nowrap;

  /* The date is the fact; the pill restates it less precisely. So when
     something has to give -- human_time_diff can return "in under a minute"
     against a column sized for "in 15 days" -- the pill is what gets clipped
     by the cell's overflow, and never the time the bid closes. */
  flex: none;
}

.am-list-due .am-deadline-rel {
  min-width: 0;
}

/* The chevron. It replaces the words "View details", which cost seven
   characters of column on every row to say what a row-wide link already says
   -- and which were aria-hidden even then, so nobody who could not see the
   arrow was ever getting them. */
.am-list-cue {
  grid-area: cue;
  display: inline-flex;
  align-items: center;
  justify-self: end;
  color: var(--am-text-subtle);
}

.am-list-cue .am-i {
  transition: transform var(--am-speed) var(--am-ease);
}

.am-list-row:hover .am-list-cue {
  color: var(--am-accent-ink);
}

.am-list-row:hover .am-list-cue .am-i {
  transform: translateX(2px);
}

/* Each cell's own caption, for the widths where the caption strip is gone.
   Present in the DOM at every width so the reading order stays "Bid due,
   August 15" rather than a bare date, and hidden while the strip is saying
   the same thing -- otherwise every heading is announced twice, once detached
   from its data. */
.am-cell-label,
.am-sort-label {
  /* .am-cell-label is reused by the header's "Signed in as" prefix, which is
     the same job: a word a screen reader needs and the eye does not, because
     on screen the position already carries the meaning. */
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* .am-sort-label in particular MUST be position:absolute rather than
   display:none while it is hidden. It is a child of the caption grid, and an
   absolutely positioned child is not a grid item -- so it consumes no column
   track. Any other way of hiding it shifts every caption one column to the
   right of the data it names. */

/* Five columns stop fitting here: the four fixed ones hold their width and
   the project name is squeezed to an ellipsis after three characters. Two
   columns and two lines -- still every fact, still aligned down the page,
   and the captions move into the cells that need one. */
@media (max-width: 68rem) {
  /* The strip stops being captions and becomes a sort bar.
     Hiding it outright was the first cut and it took the only way to reorder
     the list with it -- on exactly the devices where scrolling to find the
     right project costs the most. The three links are already here; they just
     need a word in front of them to make sense without columns underneath. */
  .am-list-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: .3rem .9rem;
    padding-inline: var(--am-row-pad);
    text-transform: none;
    letter-spacing: 0;
  }

  /* Captions for columns that no longer exist, and the chevron's empty cell. */
  .am-head-static { display: none; }

  .am-sort-label {
    position: static;
    width: auto;
    height: auto;
    margin: 0;
    overflow: visible;
    clip-path: none;
    text-transform: uppercase;
    letter-spacing: .06em;
  }

  .am-list-sort {
    font-size: var(--am-text-sm);
    font-weight: 600;
    text-transform: none;
    letter-spacing: 0;
  }

  /* Only the template changes. Every cell keeps the grid-area name it was
     given above, which is the whole point of naming them: the layout is
     re-cut here without a single cell being re-assigned, so a cell cannot end
     up in two places at two widths. */
  .am-list-row {
    grid-template-areas:
      "main due"
      "loc  status";
    grid-template-columns: minmax(0, 1fr) 13.5rem;
    row-gap: .15rem;
  }

  /* The chevron has nowhere to go in a two-line row without stretching it, and
     the row is a link with or without it. */
  .am-list-cue { display: none; }

  /* Only the deadline gets its caption back. A pin already says "location",
     and a badge reading PUBLIC does not need one reading ACCESS beside it; a
     bare date next to a project name genuinely is ambiguous. */
  .am-list-due .am-cell-label {
    position: static;
    width: auto;
    height: auto;
    margin: 0;
    overflow: visible;
    clip-path: none;
    font-size: var(--am-text-xs);
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--am-text-subtle);
  }
}

/* One column. The order is the reading order: what it is, when it closes,
   where it is, what kind of project it is. */
@media (max-width: 48rem) {
  .am-list-row {
    grid-template-areas:
      "main"
      "due"
      "loc"
      "status";
    grid-template-columns: minmax(0, 1fr);
    align-items: start;
    row-gap: .2rem;
    padding-block: .6rem;
  }

  /* Stacked rows have already given up a uniform height, so the reason to
     clip a long project name has gone with it. */
  .am-list-title {
    white-space: normal;
    overflow: visible;
    overflow-wrap: anywhere;
  }
}


/* --- 4.4a Quick-filter presets -------------------------------------------- */

/* Presets, count row and column captions are three bands of chrome between
   the filters and the first project, so each one is spaced at --am-gap-sm.
   At --am-gap they added up to a screen and a half of scrolling before any
   data on a laptop. */
.am-presets {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .4rem;
  margin-bottom: var(--am-gap-sm);
}

.am-preset {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  padding: .3rem .8rem;
  border-radius: var(--am-radius-pill);
  border: 1px solid var(--am-border);
  background: var(--am-surface);
  color: var(--am-text-muted);
  font-size: var(--am-text-sm);
  font-weight: 600;
  text-decoration: none;
  transition: border-color var(--am-speed) var(--am-ease),
              color var(--am-speed) var(--am-ease),
              background-color var(--am-speed) var(--am-ease);
}

.am-preset:hover {
  border-color: var(--am-border-strong);
  color: var(--am-text);
}

/* The urgency dot, on the two windows that have one. Same tokens as the row
   spines, so a reader learns "red means today" once rather than per control.
   Only a decoration: the chip's own words say which window it is, and the dot
   is drawn by CSS so it never reaches a screen reader. */
.am-preset--today::before,
.am-preset--soon::before {
  content: "";
  width: .45rem;
  height: .45rem;
  flex: none;
  border-radius: 50%;
}

.am-preset--today::before { background: var(--am-danger); }
.am-preset--soon::before  { background: var(--am-amber); }

/* The facet count. A tint of the chip's own ink rather than a second colour:
   it is a property of the chip, not a badge sitting on one. */
.am-preset-count {
  padding: 0 .35rem;
  border-radius: var(--am-radius-pill);
  background: color-mix(in oklab, currentColor 14%, transparent);
  font-variant-numeric: tabular-nums;
  font-size: var(--am-text-xs);
}

/* Empty facets stay visible and stay clickable -- "nothing closes today" is
   the most reassuring thing this page can say, and a chip that vanished when
   empty would make its absence indistinguishable from a bug. They just stop
   asking for attention, dot included. */
.am-preset.is-zero {
  opacity: .55;
}

.am-preset.is-zero::before {
  background: var(--am-border-strong);
}

/* The publication row: All · Published · Drafts, staff only.

   A second row of the same chips rather than a new component, because it is the
   same kind of control and learning it twice would be the cost of styling it
   twice. Set slightly apart from the due-window row above it: the two answer
   different questions -- when is this due, and is it even out yet -- and
   running them together as one seven-chip strip reads as one long filter whose
   options happen to contradict each other. */
.am-presets--status {
  margin-top: calc(var(--am-gap-sm) * -1 + .1rem);
  margin-bottom: var(--am-gap-sm);
  padding-top: .45rem;
  border-top: 1px dashed var(--am-border);
}

/* Grey, matching the Draft badge on the rows and the draft tile on the
   dashboard, so "not live" is one colour everywhere it appears. */
.am-preset--status-draft::before {
  content: "";
  width: .45rem;
  height: .45rem;
  flex: none;
  border-radius: 50%;
  background: var(--am-text-subtle);
}

/* Clicking the active preset clears it, so it reads as a toggle. */
.am-preset[aria-current="true"] {
  background: color-mix(in oklab, var(--am-accent) 12%, transparent);
  border-color: color-mix(in oklab, var(--am-accent) 35%, transparent);
  color: var(--am-accent-ink);
}


/* --- 4.4b List toolbar, density, selection, bulk bar ---------------------- */

.am-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--am-gap-sm) var(--am-gap);
  margin-bottom: var(--am-gap-sm);
}

.am-toolbar-selectall {
  display: inline-flex;
  align-items: center;
  margin: 0;
}

.am-toolbar-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--am-gap-sm);
  margin-left: auto;
}

/* Segmented control: density, and the calendar/list switch below share it. */
.am-density,
.am-viewswitch {
  display: inline-flex;
  padding: .2rem;
  gap: .15rem;
  background: var(--am-surface-sunk);
  border: 1px solid var(--am-border);
  border-radius: var(--am-radius-pill);
}

.am-density-btn,
.am-viewswitch-btn {
  appearance: none;
  border: 0;
  background: transparent;
  padding: .35rem .85rem;
  border-radius: var(--am-radius-pill);
  font-size: var(--am-text-sm);
  font-weight: 600;
  color: var(--am-text-muted);
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color var(--am-speed) var(--am-ease),
              color var(--am-speed) var(--am-ease);
}

.am-density-btn:hover,
.am-viewswitch-btn:hover {
  color: var(--am-text);
}

/* aria-pressed / aria-current drive the selected look, so the accessible
   state and the visible state cannot drift apart. */
.am-density-btn[aria-pressed="true"],
.am-viewswitch-btn[aria-current="true"] {
  background: var(--am-surface);
  color: var(--am-text);
  box-shadow: var(--am-shadow-sm);
}

.am-viewswitch {
  margin-bottom: var(--am-gap-lg);
}

/* --- Dashboard page header: the app-header pattern ------------------------
   Title and dek left, view controls right, both rows sharing one band.

   The centred arrangement this replaces is a marketing-page shape, and it was
   expensive here for a structural reason rather than a stylistic one: centred
   text cannot share a line with anything: the title needs a row, the dek needs
   a row, and any control needs a third or has to be lifted out of flow to
   overlap one of them. Three attempts at this header all foundered on that --
   a full-width band, an absolutely positioned switch, then a three-column grid
   with an empty spacer column existing purely to keep a sentence centred.

   Left-aligned, the problem stops existing. The text column stacks title over
   dek at its natural width and the controls take the space left beside it, so
   two rows of content occupy the height of two rows and no more. This is what
   Linear, Stripe, Vercel and GitHub all do with a page header, and the reason
   is the same in each: it is the arrangement that leaves the most room for the
   thing the page is actually for.

   ~52px against the ~155px it replaces, and the saving is permanent rather
   than conditional -- there is no breakpoint here to miss. */
.page-template-page-calendar-dashboard .americon-page-header {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  column-gap: var(--am-gap);
  row-gap: .1rem;
  text-align: left;

  /* The brand rule, moved from under the title to beside the block.
     Underneath, it was a centred swash that cost ~10px of vertical space to
     say something the topbar already says; as a left edge it costs none and
     reads as a section marker, which is what it was doing anyway. */
  border-left: 3px solid var(--am-rule);
  padding-left: .7rem;
}

.page-template-page-calendar-dashboard .americon-page-header .entry-title {
  grid-column: 1;
  grid-row: 1;
}

/* The centred rule under the title is what the left edge above replaced. */
.page-template-page-calendar-dashboard .americon-page-header .entry-title::after {
  display: none;
}

.page-template-page-calendar-dashboard .americon-page-subtitle {
  grid-column: 1;
  grid-row: 2;
  /* Was `margin: .75rem auto 0` -- the auto sides centred it as a block and
     the top margin opened a gap the grid's row-gap now owns. */
  margin: 0;
  max-width: none;
  font-size: var(--am-text-sm);
}

/* The view switch and its help button travel together. Placing the wrapper
   rather than the switch itself is what keeps the "?" out of the segmented
   control, where it would read as a third view to choose from. */
.am-header-controls {
  display: inline-flex;
  align-items: center;
  gap: .15rem;
}

/* Spans both rows so it centres against the pair, not against the title. */
.page-template-page-calendar-dashboard .americon-page-header .am-header-controls {
  grid-column: 2;
  grid-row: 1 / span 2;
  justify-self: end;
  margin-bottom: 0;
}

/* Narrow: the dek is a full sentence and the switch cannot shrink, so the two
   columns stop fitting. The controls drop under the text block and stay left,
   which keeps the header left-aligned all the way down rather than changing
   personality at a breakpoint. */
@media (max-width: 40rem) {
  .page-template-page-calendar-dashboard .americon-page-header {
    grid-template-columns: 1fr;
  }

  .page-template-page-calendar-dashboard .americon-page-header .am-header-controls {
    grid-column: 1;
    grid-row: 3;
    justify-self: start;
    margin-top: .5rem;
  }
}

.americon-page-container > .am-viewswitch {
  display: inline-flex;
}

/* The row's tick box. Its grid track is declared with the rest of the row
   shell in section 4.4. */
.am-select {
  margin: 0;
  flex: none;
}

/* Compact density. Set on <html> by list.js so it can apply before rows are
   read, and so it survives a page change without a flash.

   Comfortable is already one line per project, so compact is the same layout
   with the air taken out and the two most repetitive things dropped: the
   relative pill (every row says "in n days"; the date beside it says the same
   thing more precisely) and the location column. About 26px a row against 40,
   which is roughly forty projects on a laptop.

   The urgency spine and tint survive it, because :has(.am-deadline--today)
   matches a class rather than a visible pill. The one signal worth spending
   height on at comfortable density costs no height at all here. */
:root[data-am-density="compact"] .am-list-row {
  padding-block: .2rem;
}

:root[data-am-density="compact"] .am-list-title {
  font-size: var(--am-text-sm);
}

:root[data-am-density="compact"] .am-list-due,
:root[data-am-density="compact"] .am-list-loc {
  font-size: var(--am-text-xs);
}

:root[data-am-density="compact"] .am-deadline-rel {
  display: none; /* the absolute time stays; the pill is the first thing to go */
}

:root[data-am-density="compact"] .am-list-warn {
  padding: .1rem .4rem;
}

/* --- The invitation basket ------------------------------------------------
   The list and a running summary of what is chosen, side by side. One column
   below the breakpoint, where a 20rem rail would leave the contact rows too
   narrow to read the company and the address on one line. */

.am-invite-layout {
  display: grid;
  gap: var(--am-gap);
  align-items: start;
}

@media (min-width: 992px) {
  .am-invite-layout {
    grid-template-columns: minmax(0, 1fr) 19rem;
  }

  /* Sticky, because the answer to "how many have I got" has to stay on screen
     while scrolling the four hundred rows it is counting. */
  .am-basket {
    position: sticky;
    top: var(--am-gap);
  }
}

.am-basket {
  padding: var(--am-gap);
  background: var(--am-surface);
  border: 1px solid var(--am-border);
  border-radius: var(--am-radius);
}

.am-basket-title {
  margin: 0 0 var(--am-gap-sm);
  font-size: var(--am-text-sm);
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--am-text-subtle);
}

.am-basket-empty {
  margin: 0;
  font-size: var(--am-text-sm);
  color: var(--am-text-subtle);
}

.am-basket-total {
  margin: 0 0 var(--am-gap);
  font-size: var(--am-text-sm);
}

.am-basket-total strong {
  font-size: 1.5rem;
  font-weight: 700;
  margin-right: .25rem;
}

.am-basket-head {
  margin: var(--am-gap) 0 .35rem;
  font-size: var(--am-text-xs);
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--am-text-subtle);
}

.am-basket-outcomes,
.am-basket-routes {
  margin: 0;
  padding: 0;
  list-style: none;
}

.am-basket-outcomes li,
.am-basket-routes li {
  display: flex;
  align-items: baseline;
  gap: var(--am-gap-sm);
  padding: .2rem 0;
  font-size: var(--am-text-sm);
}

/* The name takes the slack so every count lands on the same right edge --
   a column of numbers is scannable and a ragged one is not. */
.am-basket-route-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow-wrap: anywhere;
}

.am-basket-route-n {
  flex: 0 0 auto;
  font-variant-numeric: tabular-nums;
  font-weight: 650;
}

/* The chosen contacts, by name.
   Bounded and scrolling, because the rail is sticky: forty names at a row
   each would push the Review button under the fold, and "Select all
   matching" can put four hundred here. Eight rows or so is enough to see
   what was just ticked and to scan for a name; the count above says the
   rest. */
.am-basket-chosen {
  margin: 0;
  padding: 0;
  list-style: none;
  max-height: 14rem;
  overflow-y: auto;
  overscroll-behavior: contain;
  border-top: 1px solid var(--am-border);
}

.am-basket-chosen:empty {
  display: none;
}

.am-basket-chosen li {
  display: flex;
  align-items: center;
  gap: var(--am-gap-sm);
  padding: .15rem 0;
  border-bottom: 1px solid var(--am-border);
  font-size: var(--am-text-sm);
}

.am-basket-chosen-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The x: quiet until pointed at, and a full 1.5rem target either way. */
.am-basket-chosen-remove {
  flex: 0 0 auto;
  width: 1.5rem;
  height: 1.5rem;
  padding: 0;
  border: 0;
  border-radius: var(--am-radius-sm);
  background: transparent;
  color: var(--am-text-subtle);
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
}

.am-basket-chosen-remove:hover,
.am-basket-chosen-remove:focus-visible {
  background: var(--am-surface-sunk);
  color: var(--am-danger-ink);
}

/* Colour is a second channel here, never the only one: every row is already
   named in words beside its dot. */
.am-basket-dot {
  flex: 0 0 auto;
  width: .5rem;
  height: .5rem;
  border-radius: 50%;
  background: var(--am-text-subtle);
}

.am-basket-dot--go     { background: var(--am-success); }
.am-basket-dot--warn   { background: var(--am-warning); }
.am-basket-dot--danger { background: var(--am-danger); }

.am-basket-held {
  margin-top: var(--am-gap);
  padding-top: var(--am-gap-sm);
  border-top: 1px solid var(--am-border);
}

.am-basket-note {
  margin: var(--am-gap-sm) 0 0;
  font-size: var(--am-text-xs);
  color: var(--am-text-subtle);
}

/* The way out of a row that refuses. Sits on the status it is contradicting,
   so it reads as part of the refusal rather than a stray link. */
.am-rowfix {
  display: inline-block;
  margin-left: .4rem;
  font-size: var(--am-text-xs);
  font-weight: 650;
  white-space: nowrap;
}

/* A form panel that is the exception rather than the answer: present, closed,
   and visibly quieter than the one above it. */
.am-formpanel--quiet {
  margin-top: var(--am-gap);
  background: transparent;
  border-style: dashed;
}

.am-formpanel--quiet > summary {
  cursor: pointer;
  padding: var(--am-gap-sm) 0;
  font-size: var(--am-text-sm);
  font-weight: 650;
  color: var(--am-text-subtle);
}

/* The bulk bar floats above the page bottom so it is reachable without
   scrolling back down a long list. */
.am-bulkbar {
  position: sticky;
  bottom: var(--am-gap);
  z-index: 20;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--am-gap-sm);
  margin-top: var(--am-gap);
  padding: var(--am-gap-sm) var(--am-gap);
  background: var(--am-surface);
  border: 1px solid var(--am-border-strong);
  border-radius: var(--am-radius);
  box-shadow: var(--am-shadow-lg);
}

.am-bulkbar[hidden] {
  display: none; /* [hidden] alone loses to display:flex */
}

.am-bulkbar-count {
  font-weight: 650;
  font-size: var(--am-text-sm);
}

.am-bulkbar form {
  display: inline;
}


/* --- 4.4c Activity panel (audit trail) ------------------------------------ */

.am-activity {
  margin-top: var(--am-gap-lg);
  padding-top: var(--am-gap);
  border-top: 1px solid var(--am-border);
}

.am-activity > summary {
  display: flex;
  align-items: center;
  gap: .5rem;
  cursor: pointer;
  list-style: none;
}

.am-activity > summary::-webkit-details-marker { display: none; }

.am-activity > summary::before {
  content: "";
  width: .5rem;
  height: .5rem;
  border-right: 2px solid var(--am-text-subtle);
  border-bottom: 2px solid var(--am-text-subtle);
  transform: rotate(-45deg);
  transition: transform var(--am-speed) var(--am-ease);
}

.am-activity[open] > summary::before {
  transform: rotate(45deg);
}

.am-activity-count {
  padding: .1rem .5rem;
  border-radius: var(--am-radius-pill);
  background: var(--am-surface-sunk);
  border: 1px solid var(--am-border);
  font-size: var(--am-text-xs);
  font-variant-numeric: tabular-nums;
  color: var(--am-text-muted);
}

.am-activity-list {
  display: grid;
  gap: .1rem;
  margin: var(--am-gap) 0 0;
  padding: 0 0 0 var(--am-gap);
  border-left: 2px solid var(--am-border);
  list-style: none;
}

.am-activity-list li {
  padding: .5rem 0;
}

.am-activity-what {
  display: block;
  font-size: var(--am-text-sm);
  color: var(--am-text);
}

.am-activity-who {
  display: block;
  font-size: var(--am-text-xs);
  color: var(--am-text-subtle);
}


/* --- 4.4d Keyboard shortcut overlay --------------------------------------- */

.am-shortcuts-dialog {
  width: min(28rem, calc(100vw - 2rem));
  padding: 0;
  border: 1px solid var(--am-border);
  border-radius: var(--am-radius-lg);
  background: var(--am-surface);
  color: var(--am-text);
  box-shadow: var(--am-shadow-lg);
}

.am-shortcuts-dialog::backdrop {
  background: rgb(0 0 0 / .5);
}

.am-shortcuts-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--am-gap);
  padding: var(--am-gap) var(--am-gap-lg);
  border-bottom: 1px solid var(--am-border);
}

.am-shortcuts-head h2 {
  margin: 0;
  font-size: var(--am-text-lg);
}

.am-shortcuts-close {
  appearance: none;
  border: 0;
  background: transparent;
  font-size: 1.5rem;
  line-height: 1;
  color: var(--am-text-muted);
  cursor: pointer;
  padding: 0 .25rem;
  border-radius: var(--am-radius-sm);
}

.am-shortcuts-close:hover {
  color: var(--am-text);
}

.am-shortcuts-list {
  margin: 0;
  padding: var(--am-gap) var(--am-gap-lg) var(--am-gap-lg);
  display: grid;
  gap: .1rem;
}

.am-shortcut-row {
  display: grid;
  grid-template-columns: 7rem 1fr;
  align-items: center;
  gap: var(--am-gap);
  padding: .45rem 0;
}

.am-shortcut-row dt,
.am-shortcut-row dd {
  margin: 0;
}

.am-shortcut-row dd {
  font-size: var(--am-text-sm);
  color: var(--am-text-muted);
}

.am-shortcuts-dialog kbd {
  display: inline-block;
  min-width: 1.6rem;
  padding: .15rem .4rem;
  border-radius: var(--am-radius-sm);
  background: var(--am-surface-sunk);
  border: 1px solid var(--am-border-strong);
  border-bottom-width: 2px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--am-text-xs);
  text-align: center;
  color: var(--am-text);
}


/* --- 4.5 Filter bars ------------------------------------------------------
   Replaces hardcoded col-md-* that summed to 13 columns for staff (and so
   wrapped unpredictably for exactly the users who filter most). Auto-fit
   tracks size themselves and never overflow. */

.am-filters {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr));
  align-items: end;
  gap: var(--am-gap);
  padding: var(--am-gap);
  margin-bottom: var(--am-gap);

  /* Sunk, not raised.

     The panel and the rows below it were both --am-surface with the same
     border and the same radius, which made the controls look like the first
     and largest item in the list of projects -- on a ten-row page it was
     about a fifth of the screen and it was the loudest fifth. Recessing it
     puts the two on different planes: the list is the page, the filters are
     the thing the page is sitting on. Nothing is hidden and nothing moved. */
  background: var(--am-surface-sunk);
  border: 1px solid var(--am-border);
  border-radius: var(--am-radius);
}

.am-filters .am-field--grow { grid-column: span 2; }

/* Submit and reset share a cell and sit on the controls' baseline, which is
   what `align-items: end` on the grid is for -- these two have no label above
   them to push them down. */
.am-field--actions {
  display: flex;
  align-items: center;
  gap: var(--am-gap);
}

.am-filters-clear {
  font-size: var(--am-text-sm);
  font-weight: 600;
  color: var(--am-text-muted);
  text-decoration: none;
  white-space: nowrap;
}

.am-filters-clear:hover {
  color: var(--am-text);
  text-decoration: underline;
}

@media (max-width: 575.98px) {
  .am-filters .am-field--grow { grid-column: span 1; }
}

/* Two shapes are supported: a wrapper element containing a <label>, and a
   <label class="am-field"> that wraps its own caption in a <span>. The second
   needs no id/for pair, so it cannot drift out of sync -- a broken `for`
   silently costs the field its accessible name and nothing looks wrong. */
.am-field label,
.am-field .form-label,
label.am-field > span {
  /* Flex with a floor, not a block. A caption that carries a help "?" holds a
     1.5rem hit target and one that does not holds a ~1rem line of text, so two
     fields side by side in the same grid row used to start their controls at
     different heights -- City sat a third of a line below the State select
     next to it. The floor is the icon's own size, so the caption measures the
     same whether or not it has one, and the row lines up either way. */
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  /* The word space that flex layout would otherwise swallow between the
     caption and its asterisk. */
  gap: .3em;
  min-height: 1.5rem;
  font-size: var(--am-text-xs);
  font-weight: 600;
  letter-spacing: .03em;
  text-transform: uppercase;
  color: var(--am-text-subtle);
  margin-bottom: .3rem;
}

/* The help link brings its own spacing for running text; inside a caption the
   gap above already provides it, and both together read as a stray indent. */
.am-field label > .am-help,
.am-field .form-label > .am-help,
label.am-field > span > .am-help {
  margin-inline-start: 0;
}

/* Groups of related controls -- the trade checkboxes. <fieldset> is the only
   element that groups form controls, and <legend> the only one that names a
   group, but both arrive with browser chrome (a border, an inset legend) that
   no other field here has. Reset to nothing so the group looks like every
   other field while keeping the semantics that make it announce correctly. */
.am-fieldset {
  min-width: 0;      /* fieldset defaults to min-content, which breaks grids */
  margin: 0;
  padding: 0;
  border: 0;
}

.am-fieldset > legend {
  float: none;
  width: auto;
  padding: 0;

  /* The rest of that intent, which the box reset above left out.
   *
   * Bootstrap's reboot sets legend to calc(1.275rem + .3vw) -- about 20px --
   * so every one of these named a group of checkboxes in heading type while the
   * text inputs beside it were captioned in 11px uppercase -- "Trades" on the
   * contact editor set larger than "Details", the heading of the panel it is
   * inside. A group of fields is a field, and is now captioned as one. */
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: .3em;
  min-height: 1.5rem;
  font-size: var(--am-text-xs);
  font-weight: 600;
  line-height: 1.4;
  letter-spacing: .03em;
  text-transform: uppercase;
  color: var(--am-text-subtle);
  margin-bottom: .3rem;
}

/* The help "?" brings its own leading space for running text; in a caption the
   flex gap above already provides it. Same correction as `.am-field label`. */
.am-fieldset > legend > .am-help {
  margin-inline-start: 0;
}

/* The required marker. Red alone is not a signal -- the asterisk carries the
   meaning and is hidden from assistive tech, which reads the real `required`
   attribute on the control instead and would otherwise announce "star". */
.am-req {
  color: var(--am-red);
  font-weight: 700;
}

/* The opposite marker, for a field that is genuinely optional on a panel where
   everything else is required.

   It is a word and not an absence, because "no asterisk" is only readable to
   someone who has already worked out that asterisks mean required and has one
   in view to compare against. It stays lowercase against the uppercase caption
   beside it so it reads as an aside rather than as part of the field's name. */
.am-optional {
  margin-inline-start: .4rem;
  /* The caption's own size. There is no step below --am-text-xs on this scale,
     and inventing one for a single chip is how a type scale stops being one --
     the weight and the lowercase do the separating instead. */
  font-size: var(--am-text-xs);
  font-weight: 500;
  letter-spacing: .02em;
  text-transform: none;
  color: var(--am-text-subtle);
}

/* Multi-line inputs.

   Bootstrap's .form-control sizes a textarea from its rows attribute and
   otherwise leaves it alone: the browser default line-height, and a resize
   handle that lets it be dragged narrower than its own column. Prose needs a
   looser line than a single-line input, and only the vertical handle is any
   use inside a grid track. */
.am-textarea {
  min-height: 7.5rem;
  line-height: 1.55;
  resize: vertical;
  /* Inherit the UI face rather than the browser's monospace-ish textarea
     default -- this is a sentence about a job, not code. */
  font-family: inherit;
}


/* --- 4.5a Standalone form panels ------------------------------------------
   A single form that is the whole point of its page: creating a user, and
   anything similar added later.

   These used to be built from Bootstrap's .container > .row > .col-lg-8 >
   .card, nested inside the theme's own page container -- two containers deep,
   so the form sat narrower than every other screen and their padding stacked.
   Worse, the card header carried `bg-white`, a hardcoded colour utility that
   beats the --bs-card-cap-bg mapping in section 5: in dark mode it stayed a
   white slab with near-white heading text on it.

   Nothing here uses a Bootstrap colour utility. Every value is a token, so
   the panel follows the theme in both modes with no second code path. */

/* --- Adding one thing, in one row ---------------------------------------
 *
 * Groups and Trades each add a single named item, and each did it through
 * .am-formpanel: a centred 54rem card carrying a heading, a rule, a hint, a
 * captioned field and its own actions row. Roughly 300px of screen, above the
 * list, to hold one text input -- so on the Groups page the groups themselves
 * began below the fold, and the answer to "what groups are there" was to
 * scroll past the form for making more of them.
 *
 * A panel is the right shape for a form somebody reads top to bottom. This is
 * not one. It is a field and a button on a page whose subject is the list
 * underneath, and the arithmetic that governs the tables applies here too:
 * what this row costs is rows-per-screen.
 *
 * Recessed rather than raised, for the reason .am-filters is recessed -- the
 * list is the page, and the control that adds to it is what the page sits on.
 * Raised, it read as the first and largest item IN the list.
 *
 * There is a second reason to leave, which is that .am-formpanel-head never
 * styled its own <h3>. It inherited the document's, so "Add a group" set at
 * section-heading size above a one-line form, and the panel announced itself
 * more loudly than the screen it was on.
 *
 * The caption goes to screen readers only. The button says "Add group", the
 * placeholder shows what a name looks like, and a visible "Group name" over a
 * single obvious field is a line of text that buys nothing -- the one case
 * where a hidden label is not an accessibility shortcut but the honest
 * description of what is already legible.
 */
.am-quickadd {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--am-gap-sm);
  padding: var(--am-gap-sm) var(--am-gap);
  margin-bottom: var(--am-gap);
  background: var(--am-surface-sunk);
  border: 1px solid var(--am-border);
  border-radius: var(--am-radius);
}

/* The field takes the row, the button takes what it needs. A flex-basis
   rather than a width so the two drop onto separate lines on a phone instead
   of crushing the input to a few characters. */
.am-quickadd .form-control {
  flex: 1 1 16rem;
  min-width: 0;
}

.am-quickadd .btn {
  flex: 0 0 auto;
}

/* What survives of the panel's hint: the part that is guidance rather than a
   restatement of the field. Below the control on its own line, because it is
   read once and the control is used every time. */
.am-quickadd-hint {
  flex: 1 0 100%;
  margin: 0;
  font-size: var(--am-text-sm);
  color: var(--am-text-muted);
}

.am-formpanel {
  /* Capped rather than full width. A text input stretched across a 1400px
     monitor is harder to use, not easier -- the eye loses the start of the
     row on the way back from the end. */
  max-width: 54rem;
  margin-inline: auto;
  padding: clamp(1.1rem, .8rem + 1.2vw, 1.85rem);
  background: var(--am-surface);
  border: 1px solid var(--am-border);
  border-radius: var(--am-radius-lg);
  box-shadow: var(--am-shadow);
}

/* Wider than the reading measure, and still capped by the page shell.
 *
 * The 54rem cap above exists so a single text input does not stretch across a
 * 1400px monitor. A form of eight SHORT fields has the opposite problem: at
 * 54rem the grid resolves to three columns, so eight fields cost three rows,
 * and the two checkbox groups beneath them get half a panel each -- narrow
 * enough that a twenty-trade list becomes twenty rows inside a scrollbar. The
 * contact editor was about a thousand pixels tall for eight short answers.
 *
 * At the shell width the fields fall into four columns and the two groups sit
 * side by side, each still wide enough for two trade names. Nothing is
 * stretched: every cell is the same ~16rem it was, there are simply more of
 * them per row.
 *
 * Unlike --wide this does NOT retune the grid. That modifier narrows its
 * columns to 11.25rem because its fields are a column map -- one repeated
 * question answered ninety times. These are typed values, and 16rem is the
 * floor for a box somebody puts a company name in. */
.am-formpanel--roomy {
  max-width: min(100%, var(--am-shell-max));
}

/* The exception on a roomy panel: a field that IS prose. Notes is a paragraph
   somebody writes and reads back, so it keeps the measure the panel gave up --
   the rest of the form is short answers and does not want one. */
.am-field--measure {
  max-width: 48rem;
}

/* A panel of controls rather than a panel of prose.
 *
 * The 54rem cap above is a READING measure -- it exists so a text input does
 * not stretch across a 1400px monitor and lose the eye on the way back. A grid
 * of short <select>s has the opposite problem: capped at 54rem the import's
 * column map got three columns, so a 30-column Outlook export became ten rows
 * of scrolling to answer what is one question per column.
 *
 * Kept inside --am-shell-max so the panel still lines up with everything else
 * on the page instead of breaking out of the page's own measure. */
.am-formpanel--wide {
  max-width: min(100%, var(--am-shell-max));
}

.am-formpanel-head {
  /* Was gap / gap-lg. A rule plus 1.5rem reads as a section break twice over,
     and these panels carry two or three heads each. */
  padding-bottom: var(--am-gap-sm);
  margin-bottom: var(--am-gap);
  border-bottom: 1px solid var(--am-border);
}

/* A second head in the same panel -- "Or paste a list of addresses" under the
   upload -- needs the space above it that the first one gets from the panel's
   own padding, or the new section reads as a caption on the field before it. */
.am-formpanel-head:not(:first-of-type) {
  margin-top: var(--am-gap-lg);
}

/* The panel head never styled its own heading, so an <h3> inside one fell
   through to the document's heading scale and set larger than
   .am-section-title -- the title of the whole screen above it. A panel
   announcing itself more loudly than the page it sits on is what made these
   read as unthemed.

   A step below .am-section-title, which is --am-text-lg: this is a section
   within a screen, and the hierarchy should say so. Scoped to the head rather
   than to the panel so a heading in a panel's BODY is untouched. */
.am-formpanel-head > h3,
.am-formpanel-head > h2 {
  margin: 0;
  font-size: var(--am-text-md);
  font-weight: 700;
}

.am-formpanel-hint {
  max-width: 62ch;   /* a comfortable measure; longer lines lose the reader */
  margin: .4rem 0 0;
  font-size: var(--am-text-sm);
  color: var(--am-text-muted);
}

.am-formgrid {
  display: grid;
  /* min(100%, …) so a single narrow column never forces a horizontal
     scrollbar on a phone. */
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
  align-items: start;
  gap: var(--am-gap);
}

/* Inside a wide panel the fields are answers to one repeated question -- which
   field is this column -- not a form to be read top to bottom. 16rem is the
   floor for a text input somebody types a company name into; a <select> whose
   longest option is "Business phone" needs less, and the extra column is worth
   more here than the extra inch of select is. Rows tighten to match: these are
   list rows now, and gap-lg between them made the map scroll for no reason. */
.am-formpanel--wide .am-formgrid {
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 11.25rem), 1fr));
  gap: .4rem var(--am-gap-sm);
}

/* --- The column map, at 92 columns -------------------------------------
 *
 * An Outlook contact export is not thirty columns, it is ninety-two, and at
 * that length the panel stops being a form and becomes a list: one repeated
 * question, answered ninety-two times, where the reader's job is to scan for
 * the few that matter rather than read top to bottom.
 *
 * So every rule below buys rows-per-screen, and the arithmetic is worth
 * stating because each saving looks trivial alone. Per field: the caption's
 * alignment floor (.4rem), the gap under it (.15rem), the select's own padding
 * (~.35rem), and the row gap (.2rem). About 1.1rem a row -- times the thirty-odd
 * rows this becomes, that is roughly a screen and a half of scrolling. The
 * narrower column does more again: it is what takes a 1180px panel from three
 * columns to four, which cuts the row count by a quarter on its own.
 *
 * WHAT IS NOT TRADED AWAY
 * The select's text stays at the body size and the caption stays legible. This
 * is a screen where misreading "Business phone" as "Business fax" writes the
 * wrong data into four hundred contact records, so shrinking the type to fit
 * more in would be buying the wrong thing. What shrinks is whitespace. */
.am-importmap .am-field label {
  /* No caption in this grid carries a help icon -- the help lives once on the
     panel head -- so the 1.5rem floor that keeps captions aligned elsewhere is
     paying for a hazard that cannot occur here. */
  min-height: 0;
  margin-bottom: .15rem;
  /* Asked for outright, and right for this screen: the caption is the FILE's
     word ("Business Street 2") and the select below it is ours. When both are
     quiet grey the eye cannot tell which is the question and which is the
     answer, and the column headings are what somebody scans. */
  font-weight: 700;
  color: var(--am-text);
}

.am-importmap .am-field .form-select {
  padding-top: .3rem;
  padding-bottom: .3rem;
}

/* --- Which columns are actually coming in ------------------------------
 *
 * Ninety-two bold captions over ninety-two identical "Do not import" selects
 * is a wall of text with about eight answers hidden inside it, and those eight
 * are the entire point of the screen. Nothing set them apart: the dropdown
 * that writes a company name into four hundred records looked exactly like the
 * eighty-four that do nothing at all.
 *
 * So the two states are painted apart, and the split is carried at BOTH ends
 * -- the columns that import step forward, the ones that do not step back.
 * Either half on its own still leaves ninety-two things at the same weight,
 * which is the actual complaint.
 *
 * COLOUR IS NOT THE ONLY CUE, WHICH IS WHY IT IS SAFE TO USE
 * The select already says "Do not import" in words. The green is a second and
 * much faster reading of a fact that is spelled out underneath it, so nobody
 * who cannot separate the two hues loses information -- which is the condition
 * 1.4.1 puts on using colour for meaning in the first place.
 *
 * --am-success rather than --am-accent, for two reasons. Blue is this form's
 * FOCUS colour (.form-select:focus), and a permanent blue edge on eight fields
 * reads as eight focused controls. And green says the right thing here: this
 * column is accounted for, not merely "this control is active".
 *
 * WHY :has() AND NOT A CLASS RENDERED BY PHP
 * The state changes while somebody works the dropdowns, and the server does
 * not see it again until "Update the preview". A class from the render would
 * be wrong for the whole span in between -- which is exactly the minutes when
 * the colour is doing its job. option[value=""]:checked is live: a field turns
 * green the moment it is answered, with no script on the page.
 *
 * The dimming is written on the SKIPPED state and the green on the mapped one,
 * rather than both on one side. A browser without :has() drops both rules and
 * every field keeps the full-strength caption it has today: such a browser
 * loses the improvement, not the legibility. */

/* Not coming in. Back to the caption weight every other form on the site uses
   -- the extra emphasis above is spent on the columns that earned it. Still
   --am-text-subtle, which is measured at 4.5:1: these are quieter, not faint,
   because finding "Business Phone" among ninety-two is also done here. */
.am-importmap .am-field:has(option[value=""]:checked) label {
  font-weight: 600;
  color: var(--am-text-subtle);
}

.am-importmap .am-field:has(option[value=""]:checked) .form-select {
  color: var(--am-text-subtle);
}

/* Scanning distance is not deciding distance. The moment a pointer or the
   keyboard lands on a skipped field somebody is choosing rather than skimming,
   and a greyed caption is the wrong thing to make that choice from.

   Stated here rather than left to .form-select:focus, which already sets this
   colour: the rules above outrank it. */
.am-importmap .am-field:has(option[value=""]:checked):is(:hover, :focus-within) label,
.am-importmap .am-field:has(option[value=""]:checked):is(:hover, :focus-within) .form-select {
  color: var(--am-text);
}

/* Coming in.
   background-COLOR, never the `background` shorthand: .form-select carries its
   chevron as a background-image, and the shorthand would take it off. */
.am-importmap .am-field:not(:has(option[value=""]:checked)) .form-select {
  background-color: color-mix(in oklab, var(--am-success) 8%, var(--am-surface));
  border-color: var(--am-success);
  /* A two-pixel edge without a two-pixel box. Doubling border-width would make
     these selects taller than the ones beside them and break the row they sit
     in; an inset ring paints inside the same geometry. Focus replaces it
     outright, which is correct -- the focus ring stays the loudest thing on
     screen. */
  box-shadow: inset 0 0 0 1px var(--am-success);
}

/* --- How much room the map gets ----------------------------------------
 *
 * WHY THIS IS `width`, NOT JUST `max-width`
 * This panel is NOT sized by a cap. It is sized by its own contents -- it sits
 * in a shrink-to-fit context, so its width is whatever the widest thing inside
 * it happens to need, and it had been settling at roughly half the room it had
 * while `max-width` sat there looking like it was in charge.
 *
 * That is why every attempt to widen it by raising the cap did nothing: the
 * cap was never the binding constraint. `width: 100%` is, because it resolves
 * against the parent rather than the content, and only then does the max-width
 * below mean anything.
 *
 * It is also why a container query cannot be used here, and this is worth
 * writing down because the idea is a good one and somebody will have it again:
 * `container-type: inline-size` applies inline-size CONTAINMENT, which by
 * definition stops the contents contributing to the width. On a shrink-to-fit
 * box that is a circular measurement, and the box collapses to nothing -- 92
 * captions stacked one per row over selects about thirty pixels wide. Tried,
 * shipped, and immediately taken back out.
 *
 * So the column count comes from auto-fit, which needs no knowledge of the
 * container at all: 16rem is the smallest column that still shows "Do not
 * import" without clipping, and at the full shell width that lands on four
 * across, stepping down to three and two as the window narrows. */
.am-formpanel--map,
.am-formpanel--wide:has(.am-importmap) {
  width: 100%;
  max-width: min(100%, var(--am-shell-wide));
}

.am-formpanel--wide .am-importmap {
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
}

/* Two grouped pickers on one row -- Trades and Groups on the contact editor.
 *
 * Each is a scrolling box of a bounded height, so stacking them spent roughly
 * 600px of a 54rem panel on two controls that were each using half the width
 * they had been given. Side by side they cost one box's height, which is what
 * brings Notes and the Save button back above the fold on a laptop.
 *
 * Below two 20rem columns they stack again, which is the layout they had, so
 * nothing about the phone view changes. */
.am-formsplit {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
  align-items: start;
  gap: var(--am-gap);
}

/* Half the width means twice the rows, and at the shared 14rem cap a
   twenty-trade vocabulary would sit one row inside a scrollbar -- a box that
   scrolls by half a line reads as broken rather than as bounded. Raised to
   clear it outright while still capping the sixty-trade case that the cap
   exists for. */
.am-formsplit .am-trade-picker {
  max-height: 18rem;
}

.am-formgrid-full {
  grid-column: 1 / -1;
}

.am-formpanel-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--am-gap-sm);
  padding-top: var(--am-gap);
  border-top: 1px solid var(--am-border);
}

/* An option with consequences, given room to explain itself, rather than a
   bare tick box with a line of text beside it. */
.am-check {
  display: flex;
  align-items: flex-start;
  gap: .7rem;
  margin: 0;
  padding: .8rem 1rem;
  background: var(--am-surface-sunk);
  border: 1px solid var(--am-border);
  border-radius: var(--am-radius-sm);
  cursor: pointer;
  transition: border-color var(--am-speed) var(--am-ease);
}

.am-check:hover {
  border-color: var(--am-border-strong);
}

/* Nudged onto the cap height of the label beside it. Checkbox baselines do
   not line up with text by default and the difference reads as a mistake. */
.am-check > input {
  margin: .15rem 0 0;
  flex: none;
}

.am-check > span {
  display: grid;
  gap: .15rem;
  font-size: var(--am-text-sm);
}

.am-check small {
  font-size: var(--am-text-xs);
  line-height: 1.5;
  color: var(--am-text-muted);
}


/* Checkbox sits on the baseline of the inputs beside it instead of being
   nudged down with a magic `mt-4`. */
.am-field--check {
  display: flex;
  align-items: center;
  gap: .5rem;
  min-height: 2.5rem;
}

.am-field--check label {
  margin: 0;
  text-transform: none;
  letter-spacing: 0;
  font-size: var(--am-text-sm);
  font-weight: 500;
  color: var(--am-text-muted);
}


/* --- 4.6 Icons ------------------------------------------------------------
   The shared set from mu-plugins/americon-icons.php. Every glyph is a <use>
   into a sprite the footer emits, on a 16x16 viewBox filling with currentColor.

   An inline SVG has a viewBox and no intrinsic size, and this sheet's own
   `img, svg { height: auto }` would let one grow to whatever box it lands in.
   So the base is 1em square: an icon is the size of the text it sits beside,
   wherever it is used, and components that want a specific size say so with a
   more specific selector (`.am-navitem-ico svg`, `.am-photo-frame--empty svg`)
   which still wins. */
.am-i {
  inline-size: 1em;
  block-size: 1em;
  flex: none;
  /* Optical centring against lowercase text. Icons sit on the text baseline
     otherwise and read as if they are falling off the line. */
  vertical-align: -.125em;
}

/* --- 4.6 Badges ----------------------------------------------------------- */

.badge {
  display: inline-block;
  padding: .25rem .6rem;
  border-radius: var(--am-radius-pill);
  font-size: var(--am-text-xs);
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  line-height: 1.35;
}

/* The icon inside a badge. The gap is a margin rather than a flex gap because
   .badge is inline-block -- making it a flex container would change how it
   sits on the line everywhere it is already used. */
.am-badge-i {
  margin-inline-end: .3em;
}

/* Same reasoning for buttons. Only on the handful of buttons that carry an
   icon -- Download all, Move to Trash, Regenerate link, Copy -- because those
   are the actions with a consequence, and nothing else on the site should
   start growing glyphs to keep up. */
.btn .am-i {
  margin-inline-end: .4em;
}

/* A Copy button for the two seconds after it worked.

   The invite buttons used to do this by swapping Bootstrap's .btn-success in
   and .btn-outline-secondary out, which only worked because those buttons
   happened to be outline-secondary; the same script on a differently styled
   button left it green forever. This is additive, so it works on any variant
   and cannot strand a class. */
.btn.is-copied,
.btn.is-copied:disabled {
  background-color: color-mix(in oklab, var(--am-success) 16%, transparent);
  border-color: color-mix(in oklab, var(--am-success) 45%, transparent);
  color: var(--am-success-ink);
  opacity: 1;
}

/* .am-status is already inline-flex with a gap, and it has the ::before dot,
   so an icon there needs neither. */
.am-status .am-badge-i {
  margin-inline-end: 0;
}

.badge-public {
  background-color: color-mix(in oklab, var(--am-blue) 14%, transparent);
  color: var(--am-blue);
  color: light-dark(var(--am-blue), var(--am-blue-bright));
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--am-blue) 32%, transparent);
}

/* Teal, not red -- the same token the calendar markers and the private stat
   tile use. This badge was the last place private was still drawn in red, and
   it was the one that undid the reason private moved off red in the first
   place: a contractor list showing a red "PRIVATE" pill next to a red "CLOSES
   TODAY" pill asks the reader to know that one hue means a category and the
   other an urgency. Red now means exactly one thing on this site.

   Measured on this theme's own surfaces: 5.3:1 light, 4.9:1 dark. */
.badge-private {
  background-color: color-mix(in oklab, var(--am-teal) 14%, transparent);
  color: var(--am-teal);
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--am-teal) 32%, transparent);
}

/* The quiet one: counts, "no photo set", a file type. Replaces the
   `bg-light text-dark` and `text-bg-light` utility pairs that were scattered
   through the plugins -- those hardcode a pale background and near-black text,
   so in dark mode they rendered as bright chips carrying invisible text. */
.badge-neutral {
  background-color: color-mix(in oklab, var(--am-text-muted) 14%, transparent);
  color: var(--am-text-muted);
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--am-text-muted) 28%, transparent);
}

/* Bid status pill on the project details page. */
.am-status {
  display: inline-flex;
  align-items: center;
  gap: .45rem;
  padding: .3rem .75rem;
  border-radius: var(--am-radius-pill);
  font-size: var(--am-text-xs);
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  white-space: nowrap;
}

.am-status::before {
  content: "";
  width: .5rem;
  height: .5rem;
  border-radius: 50%;
  background: currentColor;
}

.am-status--open {
  background: color-mix(in oklab, var(--am-success) 14%, transparent);
  color: var(--am-success-ink);
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--am-success) 30%, transparent);
}

.am-status--closed {
  background: color-mix(in oklab, var(--am-text-muted) 14%, transparent);
  color: var(--am-text-muted);
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--am-text-muted) 30%, transparent);
}


/* --- 4.7 Project thumbnails ----------------------------------------------- */

.am-project-thumb {
  width: 52px;
  height: 52px;
  border-radius: var(--am-radius-sm);
  object-fit: cover;
  display: block;
  border: 1px solid var(--am-border);
  background: var(--am-surface-sunk);
}

.am-project-thumb--placeholder {
  display: grid;
  place-items: center;
  background:
    repeating-linear-gradient(45deg,
      var(--am-surface-sunk) 0 6px,
      color-mix(in oklab, var(--am-border) 60%, transparent) 6px 12px);
}

/* Featured photo on the project edit form. */
.am-details-photo {
  padding: .5rem;
  background: var(--am-surface-sunk);
  border: 1px solid var(--am-border);
  border-radius: var(--am-radius);
  overflow: hidden;
}

.am-details-photo-img {
  width: 100%;
  max-height: 260px;
  object-fit: cover;
  border-radius: var(--am-radius-sm);
  display: block;
}

/* Long addresses must be allowed to break, or they push the card wider than
   the viewport on a phone. */
.americon-email {
  overflow-wrap: anywhere;
  text-decoration: none;
}

.americon-email:hover {
  text-decoration: underline;
}


/* --- 4.8 File list -------------------------------------------------------- */

.am-file-list {
  display: grid;
  gap: .5rem;
  list-style: none;
  margin: 0;
  padding: 0;
}

.am-file-row {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: var(--am-gap-sm);
  padding: .75rem 1rem;
  background: var(--am-surface-sunk);
  border: 1px solid var(--am-border);
  border-radius: var(--am-radius-sm);
}

.am-file-name {
  font-weight: 550;
  overflow-wrap: anywhere;
}

/* The file-type chip: PDF, DWG, XLSX.

   Lived in the project editor's own stylesheet, which meant the staff file
   list had it and the contractor's -- the list a bid is actually priced from
   -- did not. americon_plan_file_chip() renders both, so the rule belongs
   where both can reach it.

   Sized as a square rather than a pill because it sits at the head of a row as
   a marker, not inline with the text as a label. */
.am-planrow-ext {
  display: inline-grid;
  place-items: center;
  flex: none;
  width: 2.1rem;
  height: 2.1rem;
  border-radius: var(--am-radius-sm);
  background: var(--am-surface);
  border: 1px solid var(--am-border);
  font-size: .625rem;
  font-weight: 750;
  letter-spacing: .02em;
  color: var(--am-text-muted);
  text-transform: uppercase;
}

/* In the contractor row the chip leads the file name on the same line. */
.am-file-row .am-planrow-ext {
  margin-inline-end: .55rem;
  vertical-align: -.7rem;
}

/* "No plan holders" on a project row: the one thing in that row somebody has
   to act on. Amber rather than red -- nothing is wrong, something is waiting.

   A pill rather than the loose bold amber text it was. Beside a PUBLIC badge
   in the access column, bare coloured text read as a piece of the row that
   had been shouted at rather than as a state the project is in; and at three
   words of unbounded amber it was outweighing the deadline, which is the one
   thing on this page allowed to be the loudest. The same tint recipe as the
   "closing soon" deadline pill, deliberately: they mean related things and a
   reader should not have to learn two amber treatments. */
.am-list-warn {
  display: inline-flex;
  align-items: center;
  gap: .3em;
  padding: .15rem .5rem;
  border-radius: var(--am-radius-pill);
  background: color-mix(in oklab, var(--am-amber) 20%, transparent);
  color: var(--am-warn-ink);
  font-size: var(--am-text-xs);
  font-weight: 700;
  white-space: nowrap;
}

.am-file-size {
  font-size: var(--am-text-xs);
  color: var(--am-text-subtle);
  font-variant-numeric: tabular-nums;
}

/* --- Plan revisions -------------------------------------------------------
   The label sits beside the filename and the date under it, because the two
   answer different halves of one question: "which revision is this" and
   "is mine older than it". A contractor holding Rev B needs both -- two
   different files can carry the same label across a re-issue.

   Amber rather than red: this is information, not an alarm. Red is the bid
   deadline, and a colour that means "urgent" everywhere else on the site must
   not be spent on a file that is merely current. */
.am-plan-rev {
  display: inline-block;
  margin-left: .5rem;
  padding: .05rem .4rem;
  border-radius: var(--am-radius-sm);
  background: color-mix(in oklab, var(--am-amber) 20%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--am-amber) 45%, transparent);
  font-size: var(--am-text-xs);
  font-weight: 700;
  letter-spacing: .01em;
  white-space: nowrap;
  vertical-align: baseline;
}

.am-plan-rev-at {
  display: block;
  font-size: var(--am-text-xs);
  color: var(--am-text-muted);
}

/* The editor's in-place revision input. Narrow on purpose: a revision label
   is "Rev B", and a field sized for a sentence invites one. */
.am-plan-rev-field {
  flex: 0 0 auto;
  width: 11rem;
}

@media (max-width: 34rem) {
  .am-plan-rev-field {
    width: 100%;
  }
}


/* --- 4.8a Trade picker ---------------------------------------------------- *
 *
 * The checkbox group on the create and edit forms. auto-fill rather than
 * auto-fit so a short list keeps its column width instead of stretching three
 * trades across the full form -- with auto-fit, "Roofing" would sit alone in a
 * 40rem-wide box and read as a section heading rather than a choice. */

.am-trade-picker {
  display: grid;
  /* The floor is the longest name in the vocabulary -- "Earthwork &
     Excavation" -- plus its checkbox and its count, measured rather than
     guessed. At 11rem that one option wrapped and every other cell in its row
     grew to match, so the grid read as broken by a single trade. A column that
     cannot hold the widest label is not a narrower grid, it is a ragged one. */
  grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr));
  gap: .35rem var(--am-gap-sm);
  max-height: 14rem;
  overflow-y: auto;
  padding: .65rem .8rem;
  background: var(--am-surface-sunk);
  border: 1px solid var(--am-border);
  border-radius: var(--am-radius-sm);
}

/* A choice, not a caption.
 *
 * These are <label>s inside .am-field, so `.am-field label` (0,1,1) outranked
 * the plain `.am-trade-option` (0,1,0) and dressed all twenty of them as field
 * captions: uppercase, 600 weight, letter-spaced, and -- the part that showed
 * -- `flex-wrap: wrap`, which let the count break onto a second line the
 * moment a name ran long. Uppercase with .03em tracking is about a quarter
 * wider than the sentence case these labels are stored in, so the leak caused
 * the overflow it then wrapped.
 *
 * Written as `.am-field .am-trade-option` to win that match outright, and the
 * inherited caption properties are reset by name rather than left to
 * cascade -- a reader of either rule can see what the other one is doing. */
.am-field .am-trade-option,
.am-trade-option {
  display: flex;
  align-items: center;
  /* Never. The count belongs on the label's first line, whatever its length. */
  flex-wrap: nowrap;
  gap: .45rem;
  margin: 0;
  min-height: 0;
  /* The whole row is the hit target, not just the 16px box. */
  cursor: pointer;
  font-size: var(--am-text-sm);
  font-weight: 400;
  letter-spacing: normal;
  text-transform: none;
  color: inherit;
}

.am-trade-option input {
  margin: 0;
  flex: none;
}

/* Below min-content, so a name too long for its column at a narrow viewport
   wraps inside the cell instead of pushing the count out of it. The count sets
   flex:none below and so is unaffected. */
.am-trade-option > span {
  min-width: 0;
}

/* How many contacts carry this trade, beside its label in the directory's
   filter rail.

   The number is the reason the rail is usable at all: it says whether a trade
   is worth filtering by BEFORE somebody filters by it and lands on an empty
   screen wondering whether the filter is broken or the trade is.

   Pushed to the end of the row rather than sitting against the label, so the
   column of numbers reads as a column. */
.am-trade-count {
  margin-inline-start: auto;
  flex: none;
  padding-inline: .4em;
  font-size: var(--am-text-xs);
  font-variant-numeric: tabular-nums;
  color: var(--am-text-subtle);
  background: var(--am-surface);
  border-radius: var(--am-radius-pill);
}

/* What the chip's count leaves out: contacts in this group or trade who would
   not receive the invitation. Deliberately quieter than the count itself and
   deliberately not red -- it is a fact about the list, not a fault, and a
   warning colour on a filter rail would have twenty of them shouting at once.
   It follows the count rather than replacing it, so the number the coordinator
   acts on stays the number that will be emailed. */
.am-trade-held {
  padding-inline-end: .4em;
  font-size: var(--am-text-xs);
  font-variant-numeric: tabular-nums;
  color: var(--am-text-subtle);
  opacity: .8;
}

/* A field that takes the whole filter grid rather than one column. The trade
   rail is twenty checkboxes; in an 11rem column it would be twenty rows. */
.am-filters .am-field--full {
  grid-column: 1 / -1;
}


/* --- 4.8b The trade rail, folded ------------------------------------------
 *
 * Twenty checkboxes in five rows is a little over 200px, and on the contacts
 * screen that block sat between the search box and the first row of the
 * directory. So the commonest visit to the page -- look one firm up by name --
 * paid for the rarest filter on it with a third of the fold, and did so on
 * every load.
 *
 * Folded, not removed, and it opens itself the moment a trade is actually
 * ticked: a narrowed list must never be the work of a control that is out of
 * sight. Closed, the summary still says how many are ticked, so the state is
 * legible without opening anything -- the same job the group chip above the
 * rail does for the filter you arrive with.
 *
 * The <legend> above it still names the group and carries the help link. The
 * summary names the STATE, which is why the two do not read as a duplicate. */

.am-filterfold {
  min-width: 0;
}

.am-filterfold > summary {
  display: flex;
  align-items: center;
  gap: .45rem;
  width: fit-content;
  max-width: 100%;
  padding: .25rem .5rem .25rem .3rem;
  border-radius: var(--am-radius-sm);
  cursor: pointer;
  font-size: var(--am-text-sm);
  font-weight: 600;
  color: var(--am-accent-ink);

  /* The browser's own triangle, removed in both dialects -- ours is the
     ::before below, which turns with the disclosure. */
  list-style: none;
}

.am-filterfold > summary::-webkit-details-marker {
  display: none;
}

.am-filterfold > summary:hover {
  background: var(--am-surface);
}

/* Drawn, not a glyph: it rotates with the state and does not depend on a font
   happening to carry a chevron. */
.am-filterfold > summary::before {
  content: "";
  flex: none;
  width: .45rem;
  height: .45rem;
  margin-inline-start: .15rem;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(-45deg);
  transition: transform .15s ease;
}

.am-filterfold[open] > summary::before {
  transform: rotate(45deg);
}

@media (prefers-reduced-motion: reduce) {
  .am-filterfold > summary::before { transition: none; }
}

/* The name of the group, on the control that opens it. This is the only place
   the word "Trades" appears now -- the <legend> that used to sit above the
   fold is screen-reader-only, because a heading over a closed disclosure is a
   whole line spent repeating the line under it. */
.am-filterfold-label {
  font-weight: 700;
}

/* Punctuation between the name and the state. aria-hidden in the markup, so
   this is styling a character nobody hears. */
.am-filterfold-sep {
  font-weight: 400;
  color: var(--am-text-subtle);
}

/* What the fold is doing, closed. Two states, and they are deliberately not
   the same colour: "20 to choose from" is an invitation and stays quiet, while
   "3 ticked" is the reason the list below is short and is set in the body ink
   as a fact. */
.am-filterfold-state {
  font-weight: 400;
  color: var(--am-text-subtle);
}

.am-filterfold-count {
  font-weight: 700;
  color: var(--am-text);
}

/* The help link is a flex item in the summary now. It must not be squeezed,
   and it keeps its own 1.5rem hit target. */
.am-filterfold > summary > .am-help {
  flex: none;
  margin-inline-start: .1rem;
}

.am-filterfold > .am-trade-picker {
  margin-top: .45rem;
}


/* --- 4.4c Import preview --------------------------------------------------
   What an import would do, before it does it. The tiles are the dashboard's
   own, at a smaller size: five of them side by side is a summary, five at
   dashboard scale is a page somebody has to scroll past to reach the thing
   they came to read. */

.am-stats--compact {
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 9rem), 1fr));
  gap: var(--am-gap-sm);
  margin-bottom: var(--am-gap);
}

.am-stats--compact .am-stat {
  padding: var(--am-gap-sm) var(--am-gap);
}

.am-stats--compact .am-stat-value {
  font-size: var(--am-text-xl);
}

/* A finding inside a panel: unknown categories, unrecognised states, a firm
   spelled two ways. Not a callout and not an error -- each of these is a list
   somebody may want to act on and none of them stops the import, so the
   styling is quieter than .am-panel--danger and louder than body copy. */
.am-panel-note {
  margin-top: var(--am-gap);
  padding: var(--am-gap-sm) var(--am-gap);
  background: var(--am-surface-sunk);
  border-inline-start: 3px solid var(--am-amber);
  border-radius: var(--am-radius-sm);
}

.am-panel-note strong {
  display: block;
  font-size: var(--am-text-sm);
}

.am-panel-note p {
  margin: .25rem 0 .5rem;
  font-size: var(--am-text-sm);
  color: var(--am-text-muted);
}

/* One note is louder than the rest: the addresses inside our own organisation,
   which is the only finding that changes what gets written. The others are
   things to think about; this one has already acted. */
.am-panel-note--warn {
  background: color-mix(in oklab, var(--am-danger) 8%, var(--am-surface-sunk));
  border-inline-start-color: var(--am-danger);
}

.am-panel-note--warn strong {
  color: var(--am-danger-ink);
}

/* --- The import verdict -------------------------------------------------
   One sentence above the evidence, saying what pressing the button does.
   Deliberately louder than the panel it introduces: the counts are what the
   answer was derived from, and somebody who reads only one thing on this
   screen should read this. */
.am-verdict {
  margin-bottom: var(--am-gap);
  padding: var(--am-gap-sm) var(--am-gap);
  border-inline-start: 4px solid var(--am-border);
  border-radius: var(--am-radius-sm);
  background: var(--am-surface-sunk);
}

.am-verdict-line {
  margin: 0;
  font-size: var(--am-text-lg);
  font-weight: 600;
  color: var(--am-text);
}

.am-verdict-note {
  margin: .35rem 0 0;
  font-size: var(--am-text-sm);
  color: var(--am-text-muted);
}

/* Three states, because "78 of 94 would import", "nothing would import" and
   "all 94 would import" are different answers and only one is good news. */
.am-verdict--success {
  border-inline-start-color: var(--am-success);
  background: color-mix(in oklab, var(--am-success) 8%, var(--am-surface-sunk));
}

.am-verdict--warn {
  border-inline-start-color: var(--am-amber);
  background: color-mix(in oklab, var(--am-amber) 10%, var(--am-surface-sunk));
}

.am-verdict--danger {
  border-inline-start-color: var(--am-danger);
  background: color-mix(in oklab, var(--am-danger) 8%, var(--am-surface-sunk));
}

/* --- Segmented row filter -----------------------------------------------
   Links, not buttons: the choice belongs in the URL so a reload keeps it and
   a colleague can be sent straight to the problems. */
.am-segmented {
  display: inline-flex;
  margin-bottom: var(--am-gap-sm);
  padding: .15rem;
  gap: .15rem;
  border: 1px solid var(--am-border);
  border-radius: var(--am-radius-pill);
  background: var(--am-surface-sunk);
}

.am-segmented-item {
  padding: .3rem .85rem;
  border-radius: var(--am-radius-pill);
  color: var(--am-text-muted);
  font-size: var(--am-text-sm);
  font-weight: 600;
  text-decoration: none;
}

.am-segmented-item:hover,
.am-segmented-item:focus-visible {
  color: var(--am-text);
}

.am-segmented-item.is-active {
  background: var(--am-surface);
  color: var(--am-accent-ink);
  box-shadow: var(--am-shadow-sm);
}

/* --- Collapsed detail, and the plain-text block inside it ---------------- */
.am-details {
  margin-top: var(--am-gap);
  padding: var(--am-gap-sm) var(--am-gap);
  border: 1px solid var(--am-border);
  border-radius: var(--am-radius-sm);
  background: var(--am-surface-sunk);
}

.am-details > summary {
  cursor: pointer;
  font-size: var(--am-text-sm);
  font-weight: 600;
  color: var(--am-accent-ink);
}

.am-code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: var(--am-text-sm);
  white-space: pre;
}

/* --- A field and its button, inside a table cell ------------------------
   Used by the groups and trades screens, where renaming happens in the row
   rather than on a page of its own: the list IS the editor, so a rename is
   one field away rather than a round trip through a form and back.

   Wraps rather than overflowing, because the same cell is a stacked block on
   a phone, where .am-usertable turns each row into a card. */
.am-inline-form {
  display: flex;
  flex-wrap: wrap;
  gap: var(--am-gap-sm);
  align-items: center;
}

.am-inline-form .form-control {
  flex: 1 1 8rem;
  min-width: 0;
}

/* Label-and-number pairs: "Excavating 7". Wraps rather than scrolling, because
   the interesting ones are sorted to the front and a reader who stops halfway
   has still seen what matters. */
.am-inline-counts {
  display: flex;
  flex-wrap: wrap;
  gap: .35rem var(--am-gap-sm);
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: var(--am-text-sm);
}

.am-inline-counts li {
  display: inline-flex;
  align-items: center;
  gap: .4em;
  padding: .15rem .55rem;
  background: var(--am-surface);
  border: 1px solid var(--am-border);
  border-radius: var(--am-radius-pill);
}

.am-inline-counts li span {
  font-variant-numeric: tabular-nums;
  color: var(--am-text-subtle);
}


/* --- 4.8b Plan holders ---------------------------------------------------- *
 *
 * The ask a contractor sees above the plan files, and the resulting list on
 * the staff side. Styled as an ordinary panel rather than a modal or an
 * overlay on purpose: this is a request, and it should not read as a wall.
 * The files sit directly below it and stay reachable. */

.am-planholder {
  display: grid;
  gap: var(--am-gap-sm);
  margin-bottom: var(--am-gap);
  padding: var(--am-gap);
  background: var(--am-surface-sunk);
  border: 1px solid var(--am-border);
  border-radius: var(--am-radius);
}

.am-planholder-text {
  display: grid;
  gap: .2rem;
}

.am-planholder-text strong {
  /* Balanced so the question does not break after its last word, which is
     the shape that reads as a warning rather than an invitation. */
  text-wrap: balance;
}

.am-planholder-text span {
  font-size: var(--am-text-sm);
  color: var(--am-text-muted);
}

.am-planholder-fields {
  display: grid;
  /* auto-fit rather than fixed columns: three fields side by side on a
     desktop, stacked on a phone, with no breakpoint to maintain. */
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: var(--am-gap-sm);
  margin-bottom: var(--am-gap-sm);
}

.am-planholder-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--am-gap-sm);
}

.am-planholder-thanks {
  margin-bottom: var(--am-gap);
  font-size: var(--am-text-sm);
  color: var(--am-text-muted);
}

.am-planholders {
  display: grid;
  gap: var(--am-gap-sm);
  padding: var(--am-gap);
  background: var(--am-surface-sunk);
  border: 1px solid var(--am-border);
  border-radius: var(--am-radius);
}

.am-planholders-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--am-gap-sm);
}

/* --- Notify plan holders --------------------------------------------------
   Collapsed by default. Most visits to a project page are not "tell forty
   contractors something"; opening the compose box costs one click and keeps a
   send from being two stray keystrokes away at all times. */
.am-notify > summary {
  cursor: pointer;
  padding: .35rem 0;
  font-size: var(--am-text-sm);
  font-weight: 650;
  color: var(--am-text);
}

.am-notify > summary:focus-visible {
  outline: 2px solid var(--am-accent);
  outline-offset: 2px;
  border-radius: var(--am-radius-sm);
}

.am-notify-form {
  padding-top: var(--am-gap-sm);
}


/* --- 4.9 Empty states ----------------------------------------------------- */

.am-empty {
  display: grid;
  place-items: center;
  gap: .35rem;
  padding: var(--am-gap-xl) var(--am-gap);
  text-align: center;
  background: var(--am-surface);
  border: 1px dashed var(--am-border-strong);
  border-radius: var(--am-radius);
  color: var(--am-text-muted);
}

.am-empty strong {
  color: var(--am-text);
  font-size: var(--am-text-lg);
}

/* The glyph, when the caller passes one. Large and faint on purpose: it is
   the thing that makes an empty region read as deliberate rather than as
   something that failed to load, and it must not compete with the sentence
   under it for attention. */
.am-empty-ico {
  font-size: 2rem;
  color: var(--am-text-subtle);
  opacity: .8;
  margin-bottom: .15rem;
}


/* --- 4.10 Login ----------------------------------------------------------- */

.americon-login-page {
  display: grid;
  place-items: center;
  padding-block: clamp(2rem, 1rem + 5vw, 5rem);
  padding-inline: var(--am-shell-pad);
}

.americon-login-card {
  width: 100%;
  max-width: 26rem;
  padding: clamp(1.5rem, 1rem + 2.5vw, 2.5rem);
  background: var(--am-surface);
  border: 1px solid var(--am-border);
  border-radius: var(--am-radius-lg);
  box-shadow: var(--am-shadow-lg);
}

.americon-login-card h1 {
  font-size: var(--am-text-xl);
  font-weight: 750;
  margin: 0 0 .35rem;
  text-align: center;
}

.americon-login-intro {
  text-align: center;
  color: var(--am-text-muted);
  font-size: var(--am-text-sm);
  margin-bottom: var(--am-gap-lg);
}

.americon-login-legal {
  text-align: center;
  color: var(--am-text-subtle);
  font-size: var(--am-text-xs);
  margin-top: var(--am-gap);
}


/* --- 4.10a Notice pages ---------------------------------------------------
   The page mu-plugins/americon-notice-page.php renders every front-end
   wp_die() into: "yes, we are bidding", the unsubscribe confirmation, a plan
   file that will not open, an invite link that has expired.

   It reuses .americon-login-card rather than introducing a second centred card
   on purpose. Both are the same object -- one short piece of business on an
   otherwise empty page -- and the sign-in screen is the surface a contractor
   most often sees just before or just after one of these. Two card recipes a
   click apart is how a portal starts to look like several products.

   These rules are here, in the shipped stylesheet, and not in the PHP that
   prints the markup: tests/test-contrast.php reads colours out of stylesheets,
   so a colour written into a PHP string is a colour nothing measures.
   ------------------------------------------------------------------------- */

/* Wider than the sign-in card. That one holds two fields and wants to be
   narrow; these hold a paragraph or two of explanation -- 26rem sets a measure
   of roughly 40 characters, which reads as a column of broken phrases. */
.am-notice-card {
  max-width: 34rem;
}

/* The same recipe as .americon-login-page, and not that class itself: a notice
   is not a sign-in, and a reader of this stylesheet should not have to work out
   why the unsubscribe confirmation is laid out by a rule named for the login
   screen. Centred in the viewport, so a two-line answer does not sit alone
   against the top edge of a tall window. */
.am-notice-shell {
  display: grid;
  place-items: center;
  padding-block: clamp(2rem, 1rem + 5vw, 5rem);
  padding-inline: var(--am-shell-pad);
}

/* Centred with the heading it labels, and in the brand red rather than the
   subtle grey .am-eyebrow carries elsewhere. Everywhere else an eyebrow sits
   above a section in a busy page and must not compete; here it is one of three
   things on screen, and it is the one that says which flow this is. */
.am-notice-eyebrow {
  text-align: center;
  color: var(--am-danger-ink);
  margin: 0 0 .5rem;
}

/* .americon-page-title brings its own centred red rule; it only needs the
   space under it that a page header would normally give it. */
.am-notice-card .americon-page-title {
  font-size: var(--am-text-xl);
  margin-bottom: var(--am-gap-lg);
}

.am-notice-message > :last-child {
  margin-bottom: 0;
}

/* The form on a notice page -- the recover-my-invitation address box, the
   optional "anything the project manager should know?" line. Bare <input> and
   <button> elements used to carry inline widths and paddings written at the
   call site; they are .form-control and .btn now, so they are the same
   controls as every other form in the portal. */
.am-notice-message label {
  display: block;
  font-weight: 600;
  font-size: var(--am-text-sm);
  margin-bottom: .35rem;
}

.am-notice-message .form-control {
  margin-bottom: var(--am-gap);
}

.am-notice-actions {
  display: flex;
  flex-wrap: wrap;
  gap: .75rem;
  margin-top: var(--am-gap-lg);
}

/* One action fills the card, which is the common case and the one that should
   look decided. Two or more sit side by side at their natural width, so the
   primary is not a wall of red beside a secondary of the same size. */
.am-notice-actions > .btn:only-child {
  width: 100%;
}

/* The header on a notice page is a mark and one link, not a nav bar, so it
   does not need the nav's minimum height. */
.am-notice-bar {
  padding-block: .35rem;
}

/* The wordmark, for a deploy with no logo file. It replaces an <img> inside
   .navbar-brand, which is a blue plate in light mode -- so the type on it is
   white in both schemes, not var(--am-text). */
.am-notice-wordmark {
  font-weight: 700;
  font-size: var(--am-text-md);
  color: #FFFFFF;
  padding-block: .15rem;
}

/* No plate under art that was not drawn for one -- an uploads-folder logo or a
   Customizer one, which americon_notice_logo() falls back to and which is
   drawn for a light ground. */
.am-notice-brand--flat {
  background: none;
  padding: 0;
}


/* --- 4.11 Upload dropzone -------------------------------------------------
   Moved here from a PHP string literal in Americon-project-create so it can
   use the shared tokens and follow dark mode like everything else. */

.dz-card {
  border: 1.5px dashed var(--am-border-strong);
  border-radius: var(--am-radius);
  padding: clamp(1rem, .7rem + 1.4vw, 2rem);
  background: var(--am-surface-sunk);
  text-align: center;
  transition: border-color var(--am-speed) var(--am-ease),
              background-color var(--am-speed) var(--am-ease);
}

/* A file is being dragged somewhere on this page but is not over the panel yet.
   Every file drag here is intercepted, so a drop that misses does nothing at
   all -- this is what tells you where it was supposed to land while you can
   still steer. Deliberately quieter than .dragover, which means "let go now". */
.dz-card.dz-armed {
  border-color: color-mix(in oklab, var(--am-accent) 55%, var(--am-border-strong));
}

.dz-card.dragover {
  border-color: var(--am-accent);
  background: color-mix(in oklab, var(--am-accent) 8%, var(--am-surface-sunk));
}

.dz-hint {
  font-size: var(--am-text-sm);
  color: var(--am-text-muted);
}


/* --- 4.11a Invite email preview -------------------------------------------
   The deliberate exception to the theming rules above.

   This box shows the sender exactly what the invite will look like in a
   contractor's mail client, and that email hardcodes a white background with
   dark text -- as email must, since it cannot rely on the recipient's theme.
   Rendering the preview on a dark surface would show the sender something
   other than what gets sent, which defeats the point of a preview.

   So the colours here are fixed on purpose, and stated rather than inherited
   from a Bootstrap utility that would flip or fail to flip by accident. */

.am-email-preview {
  background: #FFFFFF;
  color: #212529;
  max-height: 420px;
  overflow: auto;
}

/* The email body carries its own link colours; keep the preview honest by not
   imposing the site's. */
.am-email-preview a {
  color: #0D6EFD;
}

.dz-previews {
  display: grid;
  gap: .25rem;
}

.dz-previews .dz-item {
  display: flex;
  align-items: center;
  gap: .75rem;
  padding: .625rem .5rem;
  border-bottom: 1px solid var(--am-border);
}

.dz-previews .dz-item:last-child { border-bottom: 0; }

.dz-previews .dz-thumb {
  width: 38px;
  height: 38px;
  flex: none;
  border-radius: var(--am-radius-sm);
  overflow: hidden;
  display: grid;
  place-items: center;
  background: var(--am-surface);
  border: 1px solid var(--am-border);
}

.dz-progress {
  height: .3rem;
  background: var(--am-border);
  border-radius: var(--am-radius-pill);
  overflow: hidden;
}

.dz-progress > div {
  height: 100%;
  width: 0%;
  background: var(--am-accent);
  transition: width var(--am-speed) linear;
}

.dz-remove {
  border: none;
  background: transparent;
  color: var(--am-danger-ink);
  border-radius: var(--am-radius-sm);
  padding: .25rem .5rem;
}

.dz-remove:hover {
  background: color-mix(in oklab, var(--am-danger) 12%, transparent);
}


/* --- 4.12 Footer ----------------------------------------------------------
   The old rule set made this a grid, then tried to stack it on mobile with
   `flex-direction: column` — a no-op on a grid container, so the footer never
   stacked. It is a grid that genuinely reflows now. */

.americon-footer {
  flex: none;
  margin-top: var(--am-gap-xl);
  padding: var(--am-gap-lg) var(--am-shell-pad) var(--am-gap);
  background-color: var(--am-footer-bg);
  color: var(--am-footer-ink);
  border-top: 3px solid var(--am-amber);
}

.americon-footer-inner {
  /* Same shell as the header band above it -- the two ends of the page frame
     the content, so they have to agree with each other before they agree with
     anything else. */
  max-width: var(--am-shell-chrome);
  margin-inline: auto;
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  gap: var(--am-gap) var(--am-gap-lg);
}

.americon-footer-left {
  display: flex;
  align-items: center;
  gap: .75rem;
}

.americon-footer-logo {
  height: 32px;
  width: auto;
  opacity: .9;
}

.americon-footer-brand {
  font-weight: 700;
  font-size: var(--am-text-sm);
  color: var(--am-footer-brand-ink);
}

.americon-footer-links {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: .5rem var(--am-gap-lg);
  font-size: var(--am-text-sm);
}

.americon-footer a {
  color: var(--am-footer-link);
  font-weight: 500;
}

.americon-footer a:hover {
  color: var(--am-footer-link-hover);
  text-decoration: underline;
}

.americon-footer a:focus-visible {
  outline-color: var(--am-footer-link);
}

.americon-footer-meta {
  grid-column: 1 / -1;
  padding-top: var(--am-gap);
  border-top: 1px solid rgb(255 255 255 / .08);
  font-size: var(--am-text-xs);
  color: var(--am-footer-meta-ink);
  text-align: center;
}

@media (max-width: 720px) {
  .americon-footer-inner {
    grid-template-columns: 1fr;
    justify-items: center;
    text-align: center;
  }
  .americon-footer-links { justify-content: center; }
}


/* --- 4.13 Editorial content (index.php fallback) -------------------------- */

.americon-entry {
  padding: var(--am-gap-lg);
  background: var(--am-surface);
  border: 1px solid var(--am-border);
  border-radius: var(--am-radius);
  margin-bottom: var(--am-gap);
}

.americon-entry .entry-title {
  font-size: var(--am-text-xl);
  margin-top: 0;
}

.pagination {
  --bs-pagination-color: var(--am-text);
  --bs-pagination-bg: var(--am-surface);
  --bs-pagination-border-color: var(--am-border);
  --bs-pagination-hover-bg: var(--am-surface-sunk);
  --bs-pagination-active-bg: var(--am-red);
  --bs-pagination-active-border-color: var(--am-red);
}

/* paginate_links( type => list ) output */
.page-numbers {
  display: flex;
  flex-wrap: wrap;
  gap: .35rem;
  list-style: none;
  margin: 0;
  padding: 0;
}

.page-numbers li a,
.page-numbers li span {
  display: inline-grid;
  place-items: center;
  min-width: 2.25rem;
  height: 2.25rem;
  padding-inline: .5rem;
  border: 1px solid var(--am-border);
  border-radius: var(--am-radius-sm);
  background: var(--am-surface);
  color: var(--am-text);
  font-size: var(--am-text-sm);
  font-variant-numeric: tabular-nums;
}

.page-numbers li a:hover {
  border-color: var(--am-border-strong);
  background: var(--am-surface-sunk);
}

.page-numbers li span.current {
  background: var(--am-red);
  border-color: var(--am-red);
  color: #fff;
  font-weight: 650;
}


/* --- 4.14 Screen-reader-only text -----------------------------------------
   WordPress core emits `.screen-reader-text` on its own -- the heading inside
   the_posts_pagination(), the "Search for:" label in get_search_form() -- and
   this sheet never defined it, so those strings rendered as visible stray text
   in the middle of the page. The clip-path/1px shape is the standard one: the
   text stays in the accessibility tree and out of the visual layout, which
   `display: none` would not do. */

.screen-reader-text {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}


/* --- 4.15 Admin console ----------------------------------------------------
   The user administration screens: a list of accounts, and the editor for one.

   These sit inside the page body, which index.php already wraps in
   .americon-entry -- a bordered, padded surface. So nothing here adds a second
   card around the whole thing; the panels below are the *sections* within that
   surface, separated by their own quieter chrome. Nesting card inside card is
   how the create form ended up two containers deep before section 4.5a. */

.am-console {
  display: grid;
  /* Was --am-gap-lg. A console is a working screen with four or five stacked
     blocks, and 1.5rem between each of them cost most of a fold before the
     first row of data. The blocks already carry borders and surfaces, so the
     separation was being paid for twice. */
  gap: var(--am-gap);
}

/* The page title above a console.
 * ------------------------------
 * index.php prints a centred hero -- 2.125rem, 800 weight, red rule under it
 * -- because for an ordinary page that title is the only thing naming the
 * screen. Above a console it is not: the console prints its own head, so
 * /contacts opened at the import wizard led with "Contacts" as a banner and
 * then "Import contacts" as the real heading, and the two of them plus the
 * container's own padding pushed the first control below the fold.
 *
 * The h1 stays -- it is the page's landmark and the only one -- but it is
 * demoted to a label: page scale is spent on the console's head, which is the
 * heading that actually says where you are. Same treatment .am-board already
 * gets, for the same reason.
 */
.americon-page-container:has(.am-console) {
  padding-block: var(--am-gap) clamp(1.25rem, .8rem + 1.8vw, 2.5rem);
}

.americon-entry:has(.am-console) > .americon-page-header {
  margin-bottom: var(--am-gap);
  text-align: start;
}

.americon-entry:has(.am-console) > .americon-page-header .entry-title {
  font-size: var(--am-text-sm);
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--am-text-subtle);
}

/* The centred tick belongs to a hero, and this is no longer one. */
.americon-entry:has(.am-console) > .americon-page-header .entry-title::after {
  display: none;
}

/* With the title demoted, "Back to contacts" and the console head are the two
   things at the top of the screen, and stacking them spent a whole line on a
   link four words long. */
.am-console > .am-backlink + .am-console-head {
  margin-top: calc(var(--am-gap-xs) - var(--am-gap));
}

.am-console-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--am-gap);
}

.am-console-sub {
  margin: .3rem 0 0;
  font-size: var(--am-text-sm);
  color: var(--am-text-muted);
}

.am-backlink {
  margin: 0;
  font-size: var(--am-text-sm);
  font-weight: 600;
}


/* --- The message being answered -------------------------------------------
   Shown above the reply box on the composer, so a coordinator answers with the
   question in front of them rather than from memory.

   Deliberately quieter than the form below it: this is context, and the thing
   on this screen that should hold the eye is the empty box. The left rule is
   the quote convention every mail client uses, which is what a reader already
   reads it as -- no label needed to say "this is what they said". */
.am-answering {
  margin: 0 0 var(--am-gap);
  padding: .75rem 0 .75rem 1rem;
  border-left: 3px solid var(--am-border-strong);
  background-color: var(--am-surface-sunk);
  border-radius: 0 var(--am-radius-sm) var(--am-radius-sm) 0;
}

.am-answering-who {
  margin: 0;
  font-size: var(--am-text-sm);
  color: var(--am-text-muted);
}

.am-answering-subject {
  margin: .15rem 0 0;
  font-weight: 600;
}

/* The contractor's own words. Wrapped rather than clipped: a question cut off
   mid-sentence is the one thing this block exists to prevent. */
.am-answering-body {
  margin: .5rem 0 0;
  padding: 0;
  border: 0;
  font-size: var(--am-text-sm);
  color: var(--am-text);
  white-space: normal;
  overflow-wrap: anywhere;
}

.am-answering-note {
  margin: .6rem 0 0;
  font-size: var(--am-text-sm);
  color: var(--am-text-muted);
}

/* .am-console owns the vertical rhythm, so the filter bar's own bottom margin
   would double the gap above the table. */
.am-console > .am-filters {
  margin-bottom: 0;
}


/* --- The account table ----------------------------------------------------
   A table, not the .am-list-row cards used for projects. A project row has one
   thing you scan for (the deadline); an account has six, and comparing "who
   has not signed in since March" across rows is exactly what columns are for.

   The wrapper scrolls rather than the page. A body that scrolls sideways
   breaks every other screen on the site, and `overflow-x: auto` on the wrapper
   is also what makes the table keyboard-scrollable -- hence tabindex="0" is
   not needed here, since the rows contain focusable links. */

.am-tablewrap {
  overflow-x: auto;
  background: var(--am-surface);
  border: 1px solid var(--am-border);
  border-radius: var(--am-radius);
}

.am-usertable {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--am-text-sm);
}

.am-usertable th {
  padding: .7rem clamp(.6rem, .4rem + .5vw, 1rem);
  text-align: left;
  font-size: var(--am-text-xs);
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--am-text-subtle);
  background: var(--am-surface-sunk);
  border-bottom: 1px solid var(--am-border);
  white-space: nowrap;
}

/* --- Sortable column headings ------------------------------------------
 *
 * The heading becomes a link and stops looking like one: no underline, no link
 * colour, the same small uppercase grey as the headings beside it. That is
 * deliberate rather than lazy. A header row where two of six are blue and
 * underlined reads as two links that happen to sit in a header, and the eye
 * spends its first pass working out why Company is a link and Actions is not.
 *
 * What replaces the underline is the arrow and the hover. Both are real
 * affordances -- the arrow appears under the pointer before it is clicked, so
 * the header advertises what it does at the moment somebody is thinking about
 * doing it, which an underline sitting there permanently does not.
 *
 * The state itself is on the <th> as aria-sort, which is what assistive
 * technology reads; none of the colour below is load-bearing for it. */
.am-sort {
  display: inline-flex;
  align-items: center;
  gap: .3rem;
  color: inherit;
  text-decoration: none;
  /* The heading is already uppercase and letter-spaced by .am-usertable th;
     inheriting it means this rule never has to restate the typography and so
     can never fall out of step with it. */
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
}

.am-sort:hover,
.am-sort:focus-visible {
  color: var(--am-text);
}

/* The arrow is the state, so it is only solid on the column that HAS a state.
   On the other four it fades in under the pointer as the thing that says "this
   heading does something", then leaves again. visibility rather than display
   so the heading does not change width as the pointer crosses it -- a header
   row that shifts sideways on hover reads as a rendering fault. */
.am-sort-mark {
  /* The logical properties .am-i uses, not width/height. They are the same two
     boxes, and stating it the same way is what keeps this an override of that
     rule rather than a second rule racing it on source order. */
  inline-size: .7em;
  block-size: .7em;
  flex: none;
  opacity: 0;
  transition: opacity var(--am-speed) var(--am-ease),
              transform var(--am-speed) var(--am-ease);
}

.am-sort:hover .am-sort-mark,
.am-sort:focus-visible .am-sort-mark {
  opacity: .55;
}

.am-sort.is-sorted {
  color: var(--am-text);
}

.am-sort.is-sorted .am-sort-mark {
  opacity: 1;
}

/* One glyph, turned over. A second icon in the sprite would be a second thing
   to keep pointing the same way as this one. */
.am-sort.is-desc .am-sort-mark {
  transform: rotate(180deg);
}

/* The row height of every operational list on the site.
 *
 * Half a rem of air above and below, not three quarters. These tables are read
 * by scanning down a column for the one row that is wrong, and the cost of a
 * loose row is paid once per row: at .75rem a screen holds eight contacts, at
 * .5rem it holds eleven, and the eleventh is the one that would otherwise need
 * a scroll to find. The floor is the tallest control a cell can hold -- a
 * button, a badge -- and this padding sits just above it, so nothing here
 * clips or crowds. Anything that still needs room asks for it on its own
 * class rather than lifting the number for every table. */
.am-usertable td {
  padding: .5rem clamp(.6rem, .4rem + .5vw, 1rem);
  border-bottom: 1px solid var(--am-border);
  vertical-align: middle;
}

.am-usertable tbody tr:last-child td {
  border-bottom: 0;
}

.am-usertable tbody tr:hover {
  background: var(--am-surface-sunk);
}

/* Same selected state as .am-list-item, for the same reason: the directory
   is searched the same way and its ticks travel the same way. */
.am-usertable tbody tr:has(.am-select:checked) {
  background: color-mix(in oklab, var(--am-accent) 9%, transparent);
}

.am-usertable tbody tr:hover:has(.am-select:checked) {
  background: color-mix(in oklab, var(--am-accent) 15%, transparent);
}

/* Dimmed, but never so far that the row cannot be read -- the whole reason to
   look at a suspended account is to read it. The status pill carries the
   meaning; this is only a supporting cue. */
.am-usertable tbody tr.is-suspended td:not(.am-col-actions) {
  opacity: .68;
}

/* --- Contacts ticked under a filter that no longer shows them -----------
 *
 * The directory carries a selection across every search, sort and page turn,
 * and lists whatever the current filters no longer match in a strip at the
 * foot of the table. The strip needs to read as a break in the list rather
 * than as more results -- the rows under it are not matches, they are what
 * somebody chose earlier -- so its heading row is set like a table heading
 * and sits on the sunk surface the real heading row uses.
 *
 * The rows themselves are deliberately NOT receded: they are selected, which
 * is the opposite of receded, and each carries "selected earlier" under the
 * company name for the card layout where this heading is a card of its own.
 * Same reasoning as .am-list-carried-head on the invitation screen. */
.am-usertable tbody tr.am-row-carried-head td {
  padding-top: .9rem;
  font-size: var(--am-text-xs);
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--am-text-subtle);
  background: var(--am-surface-sunk);
  border-top: 1px solid var(--am-border);
}

.am-usertable tbody tr.am-row-carried-head:hover {
  background: transparent;
}

/* An edge down the tick column, so the strip still reads as one block once its
   heading has scrolled past. On the tick column because that is the column the
   strip is about -- these rows are here for what is ticked in them. */
.am-usertable tbody tr.is-carried td.am-col-tick {
  box-shadow: inset 3px 0 0 color-mix(in oklab, var(--am-accent) 55%, transparent);
}

/* --- A region the live filter is refreshing ----------------------------
 *
 * The list dims very slightly while a newer answer is on its way, so that a
 * pause after typing reads as "working" rather than as "no more matches". It
 * stays legible and stays clickable: a tick made during the fetch is folded
 * into the answer -- see americon-livefilter.js -- and freezing the list would
 * make a 200ms round trip feel like a page load, which is the thing this
 * replaced. */
[data-am-live="list"].is-refreshing {
  opacity: .62;
  transition: opacity .12s ease-out;
}

/* --- Wrappers for the regions the rail swaps ---------------------------
 *
 * A console is a grid, so anything wrapped so the rail can replace it becomes
 * a row of that grid. Two shapes, for the two things that costs:
 *
 *   .am-live-contents  the chip strip, which is empty until a filter is set.
 *                      display:contents keeps an empty wrapper from being a
 *                      blank row with a console gap on either side of it.
 *   .am-live-stack     the list, the basket and the pager. This one has to be
 *                      a real box to carry the dimming above, so it re-states
 *                      the console's own gap -- the three were grid rows in
 *                      their own right before they were wrapped, and without
 *                      it the pager sits flush against the last contact.
 */
.am-live-contents {
  display: contents;
}

.am-live-stack {
  display: grid;
  gap: var(--am-gap);
}

/* The two-line cell: a name with its address, its trade or its qualifier under
   it. Both lines are set tighter than the page's 1.55, because that leading is
   for paragraphs -- prose the eye reads along -- and these are two labels the
   eye reads down. At 1.55 the pair is two lines of air taller than it needs to
   be, on the cell that sets the height of the whole row. */
.am-u-name {
  display: block;
  font-weight: 650;
  line-height: 1.3;
  color: var(--am-text);
}

.am-u-sub {
  display: block;
  font-size: var(--am-text-xs);
  line-height: 1.35;
  color: var(--am-text-muted);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}

.am-u-you {
  display: inline-block;
  margin-left: .35rem;
  padding: .05rem .4rem;
  border-radius: var(--am-radius-pill);
  background: color-mix(in oklab, var(--am-accent) 14%, transparent);
  color: var(--am-accent-ink);
  font-size: var(--am-text-xs);
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
}

.am-u-never {
  color: var(--am-text-subtle);
  font-style: italic;
}

.am-col-actions {
  text-align: right;
  white-space: nowrap;
}

/* The tick column. Sized to the box and nothing more: it is the one column
   that carries no information, so every pixel it takes comes off the ones
   that do. In card mode it is td:first-child and inherits that rule's
   display:block with no data-label, which puts a bare checkbox at the head of
   each card -- which is where a checkbox on a card belongs. */
.am-col-tick {
  width: 1%;
  padding-right: 0;
  white-space: nowrap;
  vertical-align: middle;
}

/* --- The contact directory ---------------------------------------------
 *
 * Eight columns on .am-usertable, and the height of every row was being set by
 * the narrowest of them. Auto table layout hands width to the column whose
 * content refuses to fold, and every cell here folds willingly -- so Record,
 * whose value is one short sentence, was given ninety pixels and wrapped
 * "5 invited · never opened" into four stacked lines, making a row half again
 * as tall as the two-line name cell beside it. Four lines of a phrase cost far
 * more height than the phrase costs width.
 *
 * So the two columns whose contents have a known longest form are sized to it,
 * exactly as .am-invitetable sizes its five, and Company and Contact take
 * whatever is left -- they are the ones that can lose a few characters without
 * losing their meaning. The widths are hints on an auto layout, not a fixed
 * grid: a long trade list still pushes back, and .am-tablewrap still scrolls
 * rather than crushes if every column wants its maximum at once.
 *
 * Only above the card breakpoint. Below it each row is a grid of labelled
 * lines, where a column width is not a column width. */
@media (min-width: 768px) {

  /* Company is the column the whole row is read by, and auto layout was
     starving it: its name folds at every space, while the address beside it
     in Contact is one unbreakable mono word, so the browser paid Contact its
     full width first and gave Company what was left -- a hundred pixels, in
     which "Mulcahy Nickolaus (EIFS, Drywall)" stands four lines tall and
     sets the height of the row. A share, not a rem: on a narrow desktop
     window it yields with everything else, and the address still folds
     under the name in Contact the way it was built to. */
  .am-contacttable .am-col-company {
    width: 22%;
  }

  /* Fits "50 invited · 40 opened · 30 bid · 12 bids received" on one line,
     which is more invitations than any contact in the directory has had. */
  .am-contacttable .am-col-record {
    width: 18rem;
  }

  /* The badge and nothing else. Its longest label is "Unsubscribed". */
  .am-contacttable .am-col-reach {
    width: 8rem;
  }

  /* The button and nothing else, so the pixels go to the columns that carry
     information. */
  .am-contacttable .am-col-actions {
    width: 1%;
  }
}

/* One line per contact. The name cell was two lines everywhere -- name over
   email -- and a two-line cell sets a two-line row, which halves what a screen
   shows of a 745-row list. Here the email trails the name on the same line in
   the muted mono it already wears, and folds under only when the pair is
   genuinely too long for the column; a firm with no name on file is just its
   address, not a dash with an address under it. The padding comes down with
   it: the floor .am-usertable sets was chosen for a two-line cell, and a one-
   line row wearing it is a one-line row with a blank line in it. */
.am-contacttable td {
  padding-block: .3rem;
  line-height: 1.3;
}

.am-contacttable .am-u-name,
.am-contacttable .am-u-sub {
  display: inline;
}

.am-contacttable .am-u-name + .am-u-sub {
  margin-inline-start: .45rem;
}

/* --- The sort bar, for the width with no header row --------------------
 *
 * Hidden by default and turned on in the card-layout query below, rather than
 * the other way round: at every width where the table is a table, the headings
 * are the sort control and this would be a second copy of them.
 *
 * Same links, same is-sorted state, same arrow -- it is the same PHP helper
 * rendering both, so a column added to one appears in the other. */
.am-sortbar {
  display: none;
}

@media (max-width: 767.98px) {

  .am-sortbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .35rem .5rem;
    margin-bottom: var(--am-gap-sm);
  }

  .am-sortbar-label {
    font-size: var(--am-text-xs);
    font-weight: 700;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--am-text-subtle);
  }

  /* Chips here rather than bare text. In the header row the surrounding cells
     make it obvious these are columns; loose on the page they need an edge to
     read as things you can press, and a press target bigger than a word of
     11px type. */
  .am-sortbar .am-sort {
    padding: .3rem .6rem;
    min-height: 2rem;
    border: 1px solid var(--am-border-strong);
    border-radius: var(--am-radius-pill);
    font-size: var(--am-text-xs);
    font-weight: 700;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--am-text-muted);
  }

  /* The one that is on, filled rather than merely darker: on a phone this row
     is the only thing saying how the list underneath is ordered, so which chip
     is active has to survive being glanced at in sunlight. */
  .am-sortbar .am-sort.is-sorted {
    background: color-mix(in oklab, var(--am-accent) 12%, transparent);
    border-color: color-mix(in oklab, var(--am-accent) 35%, transparent);
    color: var(--am-accent-ink);
  }

  /* The arrow earns its space on a chip only when it means something -- there
     is no pointer to hover a chip with, so the affordance it provides on
     desktop cannot happen here. */
  .am-sortbar .am-sort:not(.is-sorted) .am-sort-mark {
    display: none;
  }
}

/* Below this the columns stop fitting even with the scroll wrapper, and a
   table you have to drag sideways to read is worse than no table. Each row
   becomes a card, with the column heading carried in as a data-label so the
   values keep their names -- the header row itself is no longer on screen to
   supply them. */
@media (max-width: 767.98px) {

  .am-tablewrap {
    border: 0;
    background: transparent;
    overflow-x: visible;
  }

  .am-usertable thead {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
  }

  /* Except where the headings are LINKS.
     The rule above hides a header row visually while leaving it in the page --
     which is right for headings, and wrong for controls. Clipped links are
     still focusable: a keyboard user on a narrow window would tab through five
     invisible sort links before reaching the list, and a screen reader would
     meet the same five twice, once here and once in .am-sortbar above.
     display:none is the honest version -- the sort bar is on screen saying the
     same thing, and every cell still carries its own name in data-label. */
  .am-usertable--sortable thead {
    display: none;
  }

  .am-usertable tbody tr {
    display: grid;
    gap: .35rem;
    padding: var(--am-gap);
    margin-bottom: var(--am-gap-sm);
    background: var(--am-surface);
    border: 1px solid var(--am-border);
    border-radius: var(--am-radius);
  }

  .am-usertable tbody tr:hover {
    background: var(--am-surface);
    border-color: var(--am-border-strong);
  }

  .am-usertable td {
    display: grid;
    grid-template-columns: 8rem 1fr;
    align-items: baseline;
    gap: var(--am-gap-sm);
    padding: 0;
    border-bottom: 0;
  }

  .am-usertable td::before {
    content: attr(data-label);
    font-size: var(--am-text-xs);
    font-weight: 700;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--am-text-subtle);
  }

  /* The name cell leads the card; a "NAME" label above it states the obvious.
     The actions cell has no label to state at all. */
  .am-usertable td:first-child,
  .am-usertable td.am-col-actions {
    display: block;
  }

  .am-usertable td:first-child::before {
    content: none;
  }

  .am-usertable td.am-col-actions {
    margin-top: .35rem;
    text-align: left;
  }

  /* The "Also selected" divider is a heading, not a contact, so it does not
     become a card: card chrome around three words would read as a row with
     nothing in it. The rows it introduces still say "selected earlier" under
     the company name, which is what carries the meaning once this line has
     scrolled off. */
  .am-usertable tbody tr.am-row-carried-head {
    display: block;
    padding: 0;
    margin: var(--am-gap) 0 var(--am-gap-sm);
    background: transparent;
    border: 0;
  }

  .am-usertable tbody tr.am-row-carried-head td {
    display: block;
    padding: 0;
    background: transparent;
    border-top: 0;
  }

  .am-usertable tbody tr.am-row-carried-head td::before {
    content: none;
  }
}


/* --- Console panels -------------------------------------------------------
   One section of the account editor. Quieter than .am-formpanel, which is a
   whole page's worth of form: these stack three-deep on one screen, and three
   shadowed cards in a column reads as three separate pages. */

.am-panel {
  padding: clamp(1rem, .8rem + .8vw, 1.5rem);
  background: var(--am-surface);
  border: 1px solid var(--am-border);
  border-radius: var(--am-radius);
}

.am-panel-head {
  padding-bottom: var(--am-gap-sm);
  margin-bottom: var(--am-gap);
  border-bottom: 1px solid var(--am-border);
}

/* Delete. The red edge is the warning; the panel is not filled red, because a
   whole block of danger colour makes the text inside it harder to read and
   this is the one place the text genuinely has to be read. */
.am-panel--danger {
  border-color: color-mix(in oklab, var(--am-danger) 40%, var(--am-border));
  box-shadow: inset 3px 0 0 var(--am-danger);
}

.am-panel--danger .am-section-title {
  color: var(--am-danger-ink);
}

/* An action with a consequence: what it does on the left, the button that
   does it on the right. Never a bare button whose label is the only
   explanation of what it will do. */
.am-actionrow {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--am-gap);
  padding-block: var(--am-gap);
  border-bottom: 1px solid var(--am-border);
}

.am-actionrow:last-child {
  padding-bottom: 0;
  border-bottom: 0;
}

.am-actionrow-text {
  display: grid;
  gap: .2rem;
  max-width: 46ch;
}

.am-actionrow-text strong {
  font-size: var(--am-text-md);
}

.am-actionrow-text span {
  font-size: var(--am-text-sm);
  color: var(--am-text-muted);
}

/* The suspend form carries a reason field beside its button. */
.am-actionrow-form {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--am-gap-sm);
  min-width: min(100%, 20rem);
}

.am-actionrow-form .am-field {
  flex: 1 1 12rem;
}

.am-actionrow-form .form-text {
  flex-basis: 100%;
  margin: 0;
}

/* The sentence under a control that says what it does -- "Inactive keeps the
 * contact and its history", "One contact per address".
 *
 * Running prose, and now set as prose. It used to carry the field CAPTION's
 * styling -- uppercase, 600 weight, .03em tracking -- copied here along with
 * the comment that belongs to .am-field-static below it. Uppercase runs about
 * a quarter wider than sentence case and has no ascender line to read by, so
 * every one of these grew by a line or two and shouted while it did: the
 * status note on the contact editor was three lines of capitals under a
 * two-word field, and the import screen carries eight more of them.
 *
 * The measure is capped for the reason .am-formpanel-hint caps its own -- a
 * note stretched across a full-width fieldset loses the reader on the way
 * back to the start of the next line. */
.am-field-caption {
  display: block;
  max-width: 68ch;
  margin: .35rem 0;
  font-size: var(--am-text-sm);
  font-weight: 400;
  letter-spacing: normal;
  text-transform: none;
  line-height: 1.45;
  color: var(--am-text-muted);
}

.am-field-static {
  margin: 0;
  padding: .5rem 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--am-text-md);
  color: var(--am-text);
}


/* =============================================================================
   5. BOOTSTRAP MAP
   =============================================================================
   Bootstrap 5.3 exposes its component internals as CSS custom properties.
   Setting those is how you restyle it — no specificity war, no `!important`,
   and dark mode comes along automatically because our tokens flip.
   ========================================================================== */

:root,
[data-bs-theme="light"],
[data-bs-theme="dark"] {
  --bs-body-bg: var(--am-canvas);
  --bs-body-color: var(--am-text);
  --bs-body-font-size: var(--am-text-md);
  --bs-border-color: var(--am-border);
  --bs-secondary-color: var(--am-text-muted);
  --bs-emphasis-color: var(--am-text);
  --bs-link-color: var(--am-accent-ink);
  --bs-link-hover-color: var(--am-danger-ink);
  --bs-border-radius: var(--am-radius-sm);
  --bs-border-radius-lg: var(--am-radius);
}

/* --- Form controls ---

   --bs-border-color above is the hairline that edges cards and separates
   rows, and it is right for that. Handed to a form control it is wrong for a
   different reason each time: a text input on this site is a white box on a
   white card, so the border is the ONLY thing saying a field is there, and at
   1.2:1 it may as well not be.

   WCAG 1.4.11 asks 3:1 of exactly this. --am-border-strong now measures it in
   both schemes, and these three selectors are the controls it has to apply to.
   Scoped rather than raising --bs-border-color globally, because that would
   also darken every card edge and table rule to input weight. */
.form-control,
.form-select,
.form-check-input {
  border-color: var(--am-field-border);
}

/* --- Buttons --- */

.btn {
  --bs-btn-font-weight: 550;
  --bs-btn-border-radius: var(--am-radius-sm);
  --bs-btn-focus-box-shadow: var(--am-ring);
  transition: background-color var(--am-speed) var(--am-ease),
              border-color var(--am-speed) var(--am-ease),
              color var(--am-speed) var(--am-ease);
}

.btn-primary {
  /* --am-red-cta, not --am-red: this fill has white text on it. See the token. */
  --bs-btn-bg: var(--am-red-cta);
  --bs-btn-border-color: var(--am-red-cta);
  --bs-btn-hover-bg: var(--am-red-dark);
  --bs-btn-hover-border-color: var(--am-red-dark);
  --bs-btn-active-bg: var(--am-red-dark);
  --bs-btn-active-border-color: var(--am-red-dark);
  --bs-btn-disabled-bg: var(--am-red-cta);
  --bs-btn-disabled-border-color: var(--am-red-cta);
  --bs-btn-font-weight: 600;
}

.btn-outline-primary {
  --bs-btn-color: var(--am-text);
  --bs-btn-border-color: var(--am-border-strong);
  --bs-btn-bg: var(--am-surface);
  --bs-btn-hover-color: var(--am-red);
  --bs-btn-hover-bg: var(--am-surface);
  --bs-btn-hover-border-color: var(--am-red);
  --bs-btn-active-color: var(--am-red);
  --bs-btn-active-bg: color-mix(in oklab, var(--am-red) 8%, var(--am-surface));
  --bs-btn-active-border-color: var(--am-red);
}

.btn-outline-secondary {
  --bs-btn-color: var(--am-text-muted);
  --bs-btn-border-color: var(--am-border-strong);
  --bs-btn-hover-color: var(--am-text);
  --bs-btn-hover-bg: var(--am-surface-sunk);
  --bs-btn-hover-border-color: var(--am-border-strong);
}

.btn-danger {
  --bs-btn-bg: var(--am-red);
  --bs-btn-border-color: var(--am-red);
  --bs-btn-hover-bg: var(--am-red-dark);
  --bs-btn-hover-border-color: var(--am-red-dark);
}

/* Reversible destructive actions -- suspending an account. Filled red is
   reserved for the ones that are not (delete), so the two do not read as
   equally final when they sit on the same screen. */
.btn-outline-danger {
  --bs-btn-color: var(--am-danger-ink);
  --bs-btn-border-color: color-mix(in oklab, var(--am-danger) 55%, transparent);
  --bs-btn-bg: var(--am-surface);
  --bs-btn-hover-color: #fff;
  --bs-btn-hover-bg: var(--am-red);
  --bs-btn-hover-border-color: var(--am-red);
  --bs-btn-active-color: #fff;
  --bs-btn-active-bg: var(--am-red-dark);
  --bs-btn-active-border-color: var(--am-red-dark);
  --bs-btn-disabled-color: var(--am-text-subtle);
  --bs-btn-disabled-border-color: var(--am-border);
}

/* --- Forms --- */

.form-control,
.form-select {
  background-color: var(--am-surface);
  border-color: var(--am-border-strong);
  color: var(--am-text);
  border-radius: var(--am-radius-sm);
  padding: .5rem .75rem;
  font-size: var(--am-text-md);
  transition: border-color var(--am-speed) var(--am-ease),
              box-shadow var(--am-speed) var(--am-ease);
}

/* The 16px floor on touch.

   iOS Safari zooms the page in whenever a field smaller than 16px takes
   focus, and it does not zoom back out when the field is left: the visitor is
   dropped on a page they now have to pan sideways to read, having only tapped
   a text box. --am-text-md is a clamp that does not reach 16px until roughly
   a 1200px viewport, so every field on the site was under the threshold on
   every phone and tablet -- the login form included, which is the first one
   anybody meets. Bootstrap's own default is 1rem; the override above is what
   introduced this, so the floor belongs here rather than in the token.

   Gated on `pointer: coarse`, not on a width, because the trigger is the
   touch keyboard rather than the screen: an iPad at 1024px zooms exactly like
   a phone at 390px, while a narrow desktop window does not zoom at all.
   Desktop keeps the fluid scale untouched.

   The bare element selectors are here because Bootstrap's reboot gives
   controls `font-size: inherit`, so anything not carrying .form-control --
   the audit filters, the recover-access box -- reads the body scale and lands
   under the threshold the same way. `.btn` outranks a bare `input`, so
   buttons keep their own size.

   max() rather than a flat 16px so this can only ever raise a value, never
   shrink one that was deliberately larger. */
@media (pointer: coarse) {
  .form-control,
  .form-select,
  input,
  select,
  textarea {
    font-size: max(1rem, var(--am-text-md));
  }
}

.form-control::placeholder {
  color: var(--am-text-subtle);
}

.form-control:focus,
.form-select:focus {
  background-color: var(--am-surface);
  border-color: var(--am-accent);
  color: var(--am-text);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--am-accent) 18%, transparent);
}

/* Native validation feedback, now that the login form no longer suppresses it
   with `novalidate`. :user-invalid only fires after the field has been
   interacted with, so nothing turns red before the user has typed. */
.form-control:user-invalid,
.form-select:user-invalid {
  border-color: var(--am-danger);
}

.form-label {
  font-size: var(--am-text-sm);
  font-weight: 600;
  color: var(--am-text);
  margin-bottom: .3rem;
}

.form-text {
  font-size: var(--am-text-xs);
  color: var(--am-text-subtle);
}

.form-check-input:checked {
  background-color: var(--am-red);
  border-color: var(--am-red);
}

/* --- Cards, alerts, list groups, modal --- */

.card {
  --bs-card-bg: var(--am-surface);
  --bs-card-border-color: var(--am-border);
  --bs-card-border-radius: var(--am-radius-lg);
  --bs-card-cap-bg: var(--am-surface);
  --bs-card-spacer-y: clamp(1rem, .8rem + .6vw, 1.5rem);
  --bs-card-spacer-x: clamp(1rem, .8rem + .8vw, 1.75rem);
  box-shadow: var(--am-shadow);
}

.card-header {
  border-bottom-color: var(--am-border);
  font-weight: 650;
}

.alert {
  --bs-alert-border-radius: var(--am-radius);
  border-width: 1px;
  font-size: var(--am-text-sm);
}

.alert-success {
  --bs-alert-bg: color-mix(in oklab, var(--am-success) 12%, transparent);
  --bs-alert-border-color: color-mix(in oklab, var(--am-success) 30%, transparent);
  --bs-alert-color: var(--am-success-ink);
}

.alert-danger {
  --bs-alert-bg: color-mix(in oklab, var(--am-red) 12%, transparent);
  --bs-alert-border-color: color-mix(in oklab, var(--am-red) 30%, transparent);
  --bs-alert-color: var(--am-danger-ink);
}

/* The same argument as .alert-warning below, for the same reason: Bootstrap's
   info is a cyan, and the only other blue on these screens is the brand's --
   the accent behind the filter chips, the preset pills and every "View
   details". Two blues that nearly match read as one of them being wrong.

   Used by "Bid intake is not live yet" on Closed Projects, the plan-holder
   notices, and the create-project form. */
.alert-info {
  --bs-alert-bg: color-mix(in oklab, var(--am-accent) 12%, transparent);
  --bs-alert-border-color: color-mix(in oklab, var(--am-accent) 30%, transparent);
  --bs-alert-color: var(--am-accent-ink);
}

/* A state, not a failure: "this account is suspended" is the portal working
   as intended. Bootstrap's own amber does flip under data-bs-theme, but it is
   a different amber from the brand's and the two sat side by side. */
.alert-warning {
  --bs-alert-bg: color-mix(in oklab, var(--am-amber) 14%, transparent);
  --bs-alert-border-color: color-mix(in oklab, var(--am-amber) 32%, transparent);
  --bs-alert-color: var(--am-warn-ink);
}

.list-group {
  --bs-list-group-bg: var(--am-surface);
  --bs-list-group-border-color: var(--am-border);
  --bs-list-group-color: var(--am-text);
  --bs-list-group-action-hover-bg: var(--am-surface-sunk);
  --bs-list-group-border-radius: var(--am-radius);
}

.modal {
  --bs-modal-bg: var(--am-surface);
  --bs-modal-border-color: var(--am-border);
  --bs-modal-border-radius: var(--am-radius-lg);
  --bs-modal-header-border-color: var(--am-border);
}

.accordion {
  --bs-accordion-bg: var(--am-surface);
  --bs-accordion-border-color: var(--am-border);
  --bs-accordion-color: var(--am-text);
  --bs-accordion-btn-color: var(--am-text);
  --bs-accordion-active-bg: var(--am-surface-sunk);
  --bs-accordion-active-color: var(--am-text);
  --bs-accordion-btn-focus-box-shadow: var(--am-ring);
  --bs-accordion-border-radius: var(--am-radius);
}

.table {
  --bs-table-bg: transparent;
  --bs-table-color: var(--am-text);
  --bs-table-border-color: var(--am-border);
}


/* === 12. The invitation screen ==============================================
   Built from the components already on the board -- the same list shell, the
   same tick boxes and bulk bar, the same stat tiles as the import summary --
   so that a screen nobody has seen before still looks like somewhere they have
   been. Only two things here are genuinely new: the step indicator, and the
   greyed row that says why somebody is not being written to. */

/* --- 12.1 Choose > Review > Send ------------------------------------------ */

.am-steps {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--am-gap-sm);
  margin: 0 0 var(--am-gap);
  padding: 0;
  list-style: none;
}

.am-step {
  display: inline-flex;
  align-items: center;
  gap: .45rem;
  font-size: var(--am-text-sm);
  font-weight: 600;
  color: var(--am-text-subtle);
}

/* The separator is drawn rather than written, so it is never read out as a
   character between two step names. */
.am-step + .am-step::before {
  content: "";
  width: 1.5rem;
  height: 1px;
  margin-inline-end: .45rem;
  background: var(--am-border-strong);
}

.am-step-n {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 50%;
  border: 1px solid var(--am-border-strong);
  font-size: var(--am-text-xs);
  font-weight: 700;
  line-height: 1;
}

.am-step--on {
  color: var(--am-accent-ink);
}

.am-step--on .am-step-n {
  background: var(--am-accent);
  border-color: var(--am-accent);
  color: #fff;
}

.am-step--done {
  color: var(--am-text);
}

.am-step--done .am-step-n {
  background: color-mix(in oklab, var(--am-success) 11%, transparent);
  border-color: color-mix(in oklab, var(--am-success) 45%, transparent);
  color: var(--am-success-ink);
}

/* --- 12.2 The contact list ------------------------------------------------
   Three columns instead of the board's four. Declared as the same two custom
   properties the board sets, so the caption strip and every row read one
   definition and cannot drift apart. */

.am-list--invite {
  --am-row-cols:  minmax(0, 1fr) 10rem 11rem;
  --am-row-areas: "main trade state";
}

.am-list--invite .am-list-main  { grid-area: main; min-width: 0; }
.am-list--invite .am-list-trade { grid-area: trade; }
.am-list--invite .am-rowstate   { grid-area: state; }

/* The board right-aligns its last caption because its last column is a
   right-aligned date. Here the last column is a phrase -- "Not yet invited",
   "Invited Mar 3" -- set flush left in every row, so the caption over it
   sits flush left too. Otherwise STATUS hangs at the far edge over a column
   whose text starts eleven rem to its left, and the strip reads as though
   the columns had slipped. */
.am-list--invite .am-list-head > :last-child { text-align: left; }

.am-list--invite .am-list-main {
  display: flex;
  flex-direction: column;
  gap: .1rem;
}

.am-list--invite .am-list-sub,
.am-list--invite .am-list-trade {
  font-size: var(--am-text-xs);
  color: var(--am-text-muted);
  overflow-wrap: anywhere;
}

/* --- 12.2a Where the bid stands ------------------------------------------
   The four tiles above the invitation record. The component is the dashboard's
   own; all this does is set the rhythm around it, because .am-stats carries a
   --am-gap-lg bottom margin sized for a page that opens with it, and this one
   has a table two lines below. */

.am-shape {
  display: grid;
  gap: var(--am-gap-xs);
}

.am-shape .am-stats {
  margin-bottom: 0;
}

.am-shape-note {
  margin: 0;
  font-size: var(--am-text-sm);
  color: var(--am-text-muted);
}


/* --- 12.2b Status pills --------------------------------------------------

   ONE VALUE OUT OF A FIXED SET, WITH AN EDGE ROUND IT.

   .am-rowstate above colours a phrase in place, which is right when the value
   is a sentence fragment. These cells are not sentences: each holds one of four
   or five fixed answers, repeated down every row, and that is the shape a badge
   is for. The outline is what lets the eye count pills down a column instead of
   reading each row's words -- which is the whole job of the Delivery, Opened
   and Bidding columns on a project with four hundred firms on it.

   THE TEXT IS THE CARRIER, NOT THE COLOUR.
   Every pill says what it means in words, so nothing here depends on telling
   amber from red. The colour is a second channel for scanning, and it is spent
   only on the states that change what somebody does next -- which is why three
   of these seven tones are deliberately near-neutral. A column of four hundred
   coloured pills is a column nobody reads. */

.am-pill {
  display: inline-flex;
  align-items: center;
  max-width: 100%;
  padding: .1rem .5rem;
  border-radius: var(--am-radius-pill);
  font-size: var(--am-text-xs);
  font-weight: 650;
  line-height: 1.55;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--am-text-muted);
  background: color-mix(in oklab, var(--am-text-muted) 10%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--am-text-muted) 26%, transparent);
}

/* A definite, unremarkable answer -- "Not bidding". Neutral rather than red: a
   firm that has told us it is passing has done the polite thing, and marking
   that in a warning colour would put it in the same visual class as a bounce. */
.am-pill--quiet {
  color: var(--am-text-muted);
  background: color-mix(in oklab, var(--am-text-muted) 10%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--am-text-muted) 26%, transparent);
}

/* THE ABSENCE OF AN ANSWER, which is not the same as a bad one. No fill and a
   dashed-weight edge, so "No answer" and "Not known" read as an empty slot
   rather than as a state somebody put the row into. This is the tone that must
   never be mistaken for a failure: "Not known" is what a firm holding a
   forwarded link shows, and drawing that in red would have somebody chase a
   contractor who has had the drawings for a fortnight. */
.am-pill--ghost {
  color: var(--am-text-subtle);
  background: none;
  box-shadow: inset 0 0 0 1px var(--am-border-strong);
  font-weight: 600;
}

/* Accepted by their mail server: most rows of a healthy project, so it keeps
   ordinary ink and gets the faintest wash in the set. Enough to separate it
   from the empty edge beside it while scrolling; not enough to be a claim. The
   green stops at 8%, which is the tint the import verdict already uses. */
.am-pill--ok {
  color: var(--am-text);
  background: color-mix(in oklab, var(--am-success) 8%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--am-success) 30%, transparent);
}

/* Said they are bidding: a promise, in the accent. Not green -- green is
   reserved below for the bid that actually turned up, and those two states sit
   one row apart on a live project. */
.am-pill--go {
  color: var(--am-accent-ink);
  background: color-mix(in oklab, var(--am-accent) 12%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--am-accent) 30%, transparent);
}

/* The bid arrived. The one finished state on this table, and the only full
   green on it, which is what makes it readable as an ending. */
.am-pill--in {
  color: var(--am-success-ink);
  background: color-mix(in oklab, var(--am-success) 14%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--am-success) 32%, transparent);
}

.am-pill--warn {
  color: var(--am-warn-ink);
  background: color-mix(in oklab, var(--am-amber) 16%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--am-amber) 34%, transparent);
}

.am-pill--danger {
  color: var(--am-danger-ink);
  background: color-mix(in oklab, var(--am-danger) 12%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--am-danger) 32%, transparent);
}

/* A pill that is also a door. Link colour would fight the tone the pill is
   already carrying, so the affordance is the underline that appears under the
   pointer instead -- the same trade .am-sort makes in the header row above. */
.am-pill-link {
  display: inline-block;
  max-width: 100%;
  color: inherit;
  text-decoration: none;
}

.am-pill-link:hover .am-pill,
.am-pill-link:focus-visible .am-pill {
  text-decoration: underline;
  text-underline-offset: .18em;
}

.am-pill-link:focus-visible {
  outline: none;
  border-radius: var(--am-radius-pill);
  box-shadow: var(--am-ring);
}


/* --- 12.2c The invitation record -----------------------------------------

   Six columns on .am-usertable. The widths are hints, not a fixed layout: the
   firm's name takes whatever the other five leave, and the other five are sized
   to the longest value each can actually hold so that none of them wraps.
   Wrapping is what this table's previous incarnation did on every cell of every
   row, and a six-word fragment split across two lines costs more height than
   the column would have cost width. */

.am-invitetable {
  table-layout: auto;
}

/* Two-line cells throughout -- a value with its qualifier under it -- so the
   values line up along their tops rather than floating around a middle that
   each cell computes for itself. */
.am-invitetable td {
  vertical-align: top;
  padding-block: .55rem;
}

.am-invitetable .am-col-firm  { min-width: 12rem; }
.am-invitetable .am-col-when  { width: 8rem; }
.am-invitetable .am-col-sent  { width: 13rem; }
.am-invitetable .am-col-open  { width: 8rem; }
.am-invitetable .am-col-said  { width: 10rem; }

/* The firm's name is the way into its history, so unlike the sort headings
   above it this one is allowed to look like a link. It is the only one in the
   row: the pills carry their own tones and would fight an accent. */
a.am-u-name {
  color: var(--am-accent-ink);
  text-decoration: none;
}

a.am-u-name:hover,
a.am-u-name:focus-visible {
  text-decoration: underline;
  text-underline-offset: .18em;
}

/* The value, and the thing that qualifies it. Two ranks in one cell is how five
   columns hold what used to need eight: the date and where the address came
   from, the delivery outcome and how many times it was sent, the first visit
   and how many there have been. */
.am-cell-main {
  display: block;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.am-cell-sub {
  display: block;
  margin-top: .1rem;
  font-size: var(--am-text-xs);
  line-height: 1.4;
  color: var(--am-text-muted);
  overflow-wrap: anywhere;
}

/* The control that sets the badge directly above it. Deliberately not a .btn:
   three buttons in a row is what the actions column used to look like, and this
   one records a fact rather than sending anything. */
.am-markform {
  margin: .2rem 0 0;
}

.am-markbtn {
  padding: 0;
  border: 0;
  background: none;
  font-size: var(--am-text-xs);
  font-weight: 600;
  color: var(--am-text-subtle);
  text-decoration: underline;
  text-underline-offset: .18em;
  cursor: pointer;
}

.am-markbtn:hover,
.am-markbtn:focus-visible {
  color: var(--am-accent-ink);
}

/* Send again, then Message. Two controls, one line, right-aligned against the
   edge of the table -- and never wrapping mid-label, which is what put "Send
   again" above "Message" above "Bid received" in three ragged rows. */
.am-rowacts {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: .3rem;
}

.am-rowacts form {
  margin: 0;
}

.am-rowacts .btn {
  white-space: nowrap;
}

/* Why this row cannot be sent to, in place of the button that would have done
   it. Wrapped, because it is a sentence and the control beside it is not. */
.am-rowacts-why {
  max-width: 11rem;
  font-size: var(--am-text-xs);
  line-height: 1.4;
  color: var(--am-text-muted);
  text-align: right;
  white-space: normal;
}

/* The "Messaged" mark, third rank in the Delivery cell. It used to inherit its
   size from .am-rowsub, which this section replaced, so it states its own. */
.am-invitetable .am-msgmark {
  display: inline-block;
  margin-top: .15rem;
  font-size: var(--am-text-xs);
  line-height: 1.4;
  color: var(--am-text-muted);
}


/* --- 12.2d One firm's delivery history -----------------------------------
   Reached by clicking a name in the table above, and now the same component --
   only the first column needs a width, because a timestamp that wraps puts the
   time under the date and reads as two rows. */

.am-deliverytable .am-col-when {
  width: 11rem;
  white-space: nowrap;
}

.am-deliverytable code {
  overflow-wrap: anywhere;
}


/* --- 12.2e What became of one send ----------------------------------------

   The last step of the invitation console, under the count. Two columns and no
   more: this table answers "did the forty I just chose go, and did any come
   straight back", and every column the record carries beyond those two -- when
   they were first invited, whether they have opened it, whether they are
   bidding -- is about a project's history rather than about the minute this
   screen is looking at, and would be identically empty on all forty rows. */

.am-runtable {
  table-layout: auto;
  margin-top: var(--am-gap-sm);
}

/* The firm cell is two lines -- name over address -- so the outcome beside it
   aligns to the top of the pair rather than to the middle of the cell. */
.am-runtable td {
  vertical-align: top;
  padding-block: .55rem;
}

.am-runtable .am-col-firm { min-width: 12rem; }
.am-runtable .am-col-sent { width: 13rem; }


/* At card width every cell is already stacked under its own label, so the
   column widths above have nothing to size and the actions stop being a
   right-hand column. */
@media (max-width: 767.98px) {

  .am-runtable .am-col-firm,
  .am-runtable .am-col-sent,
  .am-invitetable .am-col-firm,
  .am-invitetable .am-col-when,
  .am-invitetable .am-col-sent,
  .am-invitetable .am-col-open,
  .am-invitetable .am-col-said,
  .am-deliverytable .am-col-when {
    width: auto;
    min-width: 0;
  }

  .am-rowacts {
    justify-content: flex-start;
  }

  .am-rowacts-why {
    max-width: none;
    text-align: left;
  }
}

/* A row that cannot be sent to. Still legible -- it is being shown precisely
   so it can be read -- so the text keeps its own colour and only the row's
   ground and its tick box recede. Dimming the words would make the answer to
   "why didn't Bob get it" the hardest thing on the screen to read. */
.am-list-item--off {
  background: var(--am-surface-sunk);
}

/* The strip holding contacts ticked under an earlier search or page. Styled
   as the caption strip is -- same padding arithmetic, same sunk ground -- so
   it reads as part of the table's furniture rather than a fourth column
   state, with a top border to close the filtered rows above it. */
.am-list-carried-head {
  padding: .4rem var(--am-row-pad) .4rem
           calc(var(--am-row-pad) + 3px + var(--am-row-gutter));
  background: var(--am-surface-sunk);
  border-top: 1px solid var(--am-border);
  border-bottom: 1px solid var(--am-border);
  font-size: var(--am-text-xs);
  color: var(--am-text-muted);
}

/* The rows under it stay full-strength -- they are selected, which is the
   opposite of receded; only blocked ones take --off as usual. */

.am-list-item--off .am-list-title {
  font-weight: 500;
}

.am-rowstate {
  font-size: var(--am-text-sm);
  color: var(--am-text-muted);
}

.am-rowstate--go     { color: var(--am-text); font-weight: 600; }
.am-rowstate--warn   { color: var(--am-warn-ink); font-weight: 650; }
.am-rowstate--danger { color: var(--am-danger-ink); font-weight: 600; }

/* A bid that actually arrived -- the one state on the invitation record that
   is finished. Green rather than the plain ink --go uses, because it has to
   be separable from "Bidding" at a glance while scanning the column, and
   those two sit one row apart on a live bid. The only green on this table,
   which is what makes it readable as an ending. */
.am-rowstate--in     { color: var(--am-success-ink); font-weight: 650; }

/* The board re-cuts its rows into two lines below this width. This list has
   different columns and different names for them, so it has to re-cut its own
   -- the shared rule would place cells into areas that do not exist here and
   drop them into the implicit grid. */
@media (max-width: 68rem) {
  .am-list--invite .am-list-row {
    grid-template-areas:
      "main  state"
      "trade state";
    grid-template-columns: minmax(0, 1fr) 10rem;
    row-gap: .15rem;
  }
}

/* --- 12.3 Selection, paging and actions ----------------------------------- */

/* "Select all N matching" is a separate control from the header tick box, and
   is revealed by script because without script it cannot do anything. */
.am-selectall {
  margin: var(--am-gap-sm) 0 0;
}

.am-selectall .btn-link {
  font-size: var(--am-text-sm);
  font-weight: 600;
}

.am-pager {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--am-gap-sm);
  margin-top: var(--am-gap-sm);
}

.am-invite-send {
  margin-top: var(--am-gap);
}

.am-invite-actions,
.am-panel-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--am-gap-sm);
  margin-top: var(--am-gap);
}

.am-invite-actions form {
  display: inline;
}

/* The plain submit under the address box. It is not a duplicate of the bulk
   bar's button: with nothing ticked the bar never appears, and typing three
   addresses by hand and having no way to continue is the case it exists for. */
.am-invite-noscript {
  margin-top: var(--am-gap-sm);
}

/* --- 12.4 What was written to one firm ------------------------------------ */

/* A second heading inside a console screen, under the one in the head bar.
   Smaller than .am-section-title on purpose: two headings at the same weight
   read as two screens stacked, and this is one screen with two halves. */
.am-section-title--sub {
  font-size: var(--am-text-md);
  margin-top: var(--am-gap-lg);
}

/* THE "MESSAGED" MARK on the invitation record.

   It shares a cell with the delivery outcome, and the two must not be mistaken
   for each other: that line is what a mail server said, this one is that a
   person wrote something. The tag carries the accent so it is findable while
   scrolling a table of four hundred rows, and the date beside it stays in the
   muted size the rest of the sub-line uses -- the mark is the thing being
   spotted, not the timestamp. */
.am-msgmark {
  text-decoration: none;
}

.am-msgmark:hover,
.am-msgmark:focus-visible {
  text-decoration: underline;
}

.am-msgmark-tag {
  display: inline-block;
  margin-right: .25rem;
  padding: 0 .4rem;
  border-radius: var(--am-radius-pill);
  background: color-mix(in srgb, var(--am-accent) 12%, var(--am-surface));
  color: var(--am-accent-ink);
  font-weight: 650;
}

/* ONE MESSAGE, AS IT WAS SENT.

   A block rather than a table row. This is prose somebody typed in paragraphs
   and it is read as prose -- squeezed into a cell beside three other columns it
   would wrap to a column of single words, which is how a record stops being
   worth opening. */
.am-msg {
  margin: 0 0 var(--am-gap-sm);
  padding: var(--am-gap-sm) var(--am-gap);
  border: 1px solid var(--am-border);
  border-radius: var(--am-radius);
  background: var(--am-surface-sunk);
}

.am-msg-head {
  display: flex;
  flex-wrap: wrap;
  gap: .15rem .6rem;
  margin: 0 0 .3rem;
  font-size: var(--am-text-xs);
  color: var(--am-text-muted);
}

.am-msg-subject {
  margin: 0 0 .35rem;
  font-size: var(--am-text-sm);
}

/* overflow-wrap because a contractor's pasted URL is a single 200-character
   word, and one of those widens the whole screen on a phone. */
.am-msg-body {
  font-size: var(--am-text-sm);
  overflow-wrap: anywhere;
}

/* ---------------------------------------------------------------------------
   Inbound correspondence, on the invitation record

   The panel is a worklist, so it is read down the "What it is" column: every
   row that has not been decided yet carries buttons, and every row that has
   carries a word. That asymmetry is the whole design -- what is left to do is
   the only thing with a control on it, so scanning for work is scanning for
   buttons rather than reading four hundred identical cells.

   Nothing here restates .am-usertable. These rules add the two-line cells that
   table does not have, and stop where it takes over.
   ------------------------------------------------------------------------ */

.am-inbox {
  margin: 0 0 1.25rem;
}

/* Set like the invitation table above it on the same screen, and for the same
   reason: two-line cells throughout, so the date and the sender line up along
   the top of the subject they belong to instead of floating around a middle
   that each cell computes for itself. .am-usertable's vertical-align: middle
   is right for a table of single-line values and wrong for every cell here. */
.am-inbox-table td {
  vertical-align: top;
  padding-block: .55rem;
}

/* Widths are hints. The message takes whatever the other four leave, because
   it is the only column whose length varies down the page.
   Only while there are columns to size: below 768px every one of these cells
   is a grid of its own carrying its heading in ::before, and a width there
   would squeeze the card rather than the column. */
@media (min-width: 768px) {
  .am-inbox-table .am-inbox-when { width: 5.5rem; }
  .am-inbox-table .am-inbox-from { width: 15rem; }
  .am-inbox-table .am-inbox-kind { width: 10rem; }
  .am-inbox-table .am-inbox-dealt { width: 12rem; }
}

/* NOT display: flex, however much these cells want a column.
 *
 * display: flex on a <td> takes it out of the table's column model: the
 * browser wraps it in an anonymous cell, and two flex <td>s in a row collapse
 * into ONE. That is what this panel was doing -- the subject, the snippet, the
 * Reply button and the Undo link all stacked in the Message column, with an
 * empty column headed "What it is" beside them. The stacking they wanted is
 * what block children do on their own.
 *
 * .am-inbox-who is a <span> inside the cell, not the cell, so it may flex. */
.am-inbox-who {
  display: flex;
  flex-direction: column;
  gap: .12rem;
}

/* The subject leads and the snippet follows it, on its own line, dimmer. Both
   in one cell rather than two columns: a snippet column would be empty on
   every out-of-office and full on every real message, which is a column that
   is mostly whitespace and occasionally a paragraph. */
.am-inbox-msg > *      { display: block; }
.am-inbox-msg > * + *  { margin-top: .12rem; }

/* Two lines and no more. The signature is already set apart before the
   snippet reaches the screen, but a contractor who writes four paragraphs is
   still four paragraphs, and the subject the message is filed under is one
   line -- which is the wrong way round on a screen for reading subjects. */
.am-inbox-msg > .am-inbox-snip {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.am-inbox-snip,
.am-inbox-addr,
.am-inbox-note {
  font-size: var(--am-text-xs);
  color: var(--am-text-muted);
  /* A contractor's pasted URL is one 200-character word, and one of those
     widens the whole table on a phone. */
  overflow-wrap: anywhere;
}

/* The count of what came attached. Not muted like the snippet beside it:
   "3 attachments" on a message somebody is deciding about is a fact they are
   deciding ON, and it competes with a paragraph of prose for the same glance. */
.am-inbox-files {
  font-size: var(--am-text-xs);
  font-weight: 600;
  color: var(--am-text);
}

/* Dates line up so a column of them can be compared without reading each one.
   white-space because "3 Sep" wrapping to two lines in a narrow column is the
   one thing that would make this column harder to scan than plain text. */
.am-inbox-when {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: var(--am-text-muted);
  font-size: var(--am-text-xs);
}

/* The verdict on top and what can still be done about it underneath -- the
   same two ranks as .am-cell-main and .am-cell-sub in the table above.
   Stacked, never centred: align-items: center floated the word "Question"
   to the vertical middle of the two-line block of buttons beside it, so it
   read as something sitting between Reply and Acknowledge rather than as
   the answer to the column it is in. */
.am-inbox-kind > *,
.am-inbox-dealt > *     { display: block; }
.am-inbox-kind > * + *,
.am-inbox-dealt > * + * { margin-top: .3rem; }

/* The undecided row's buttons sit on one line and wrap together rather than
   one at a time, so a narrow column never leaves "Question" stranded under
   "Bid" looking like a separate control.
   min-width belongs to the suspend form, which carries a reason field beside
   its button and needs room for it. Inherited into a 13rem column it forces
   every one-button form to its own full-width line. */
.am-inbox-kind .am-actionrow-form,
.am-inbox-dealt .am-actionrow-form {
  display: flex;
  flex-wrap: wrap;
  gap: .3rem;
  margin: 0;
  min-width: 0;
}

/* Reply and Acknowledge are two answers to the same question, so they sit
   together. .am-actionrow is built for a settings page -- what it does on the
   left, the button on the right, a rule under it -- and dragged all three into
   a table cell: the buttons pushed to opposite ends of the column and a stray
   horizontal line drawn across it under the last one. The same goes for the
   thread's row of them under a message. */
.am-inbox-kind .am-actionrow,
.am-inbox-dealt .am-actionrow,
.am-conv-actions.am-actionrow {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-start;
  gap: .3rem .5rem;
  padding-block: 0;
  border-bottom: 0;
}

/* Mail that named no job. Listed rather than tabulated: there are a handful at
   most, and each is one line plus the button that empties it. */
.am-inbox-unrouted {
  list-style: none;
  margin: .6rem 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: .45rem;
}

.am-inbox-unrouted li {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .25rem .6rem;
  font-size: var(--am-text-sm);
}

.am-inbox-unrouted .am-actionrow-form {
  margin: 0;
  min-width: 0;
}

/* --- What the panel's footers are allowed to weigh ----------------------
 *
 * Two blocks sit under the table, and until now both wore an amber edge on a
 * sunk ground and one of them set its first line larger and heavier than
 * anything in the table itself. A panel whose loudest two things are the notes
 * beneath the data does not read as a record; it reads as a warning with a
 * table attached. Both are demoted here rather than restyled: the components
 * are right where they came from, and only their weight in this panel is
 * wrong.
 *
 * The standing note first. .am-panel-note's amber edge marks a finding --
 * something on THIS record somebody has to look at. This paragraph says the
 * same sentence under every job forever, which is the definition of something
 * that is not a finding. */
.am-inbox > .am-panel-note {
  border-inline-start-color: var(--am-border);
}

/* Then the unplaced queue. .am-verdict-line is --am-text-lg because on the
   import screen the verdict IS the answer and the counts under it are only
   the working. Here it is a footnote to a table it must not shout over. */
.am-inbox .am-verdict-line {
  font-size: var(--am-text-md);
}

/* ---------------------------------------------------------------------------
   Unplaced bid mail

   A triage list, so the row is read left to right and ends in a decision. The
   only thing that varies down the page is the message, which is why it is the
   one column allowed to grow.
   ------------------------------------------------------------------------ */

.am-bidmail-head h1 {
  font-size: var(--am-text-xl, 1.4rem);
  margin: 0 0 .3rem;
}

.am-bidmail-head p {
  margin: 0 0 1rem;
  color: var(--am-text-muted);
  max-width: 62ch;
}

/* The expanded message. Pre-wrap because the excerpt is prose that arrived
   with its own line breaks, and collapsing them turns a quoted address block
   into one long line. Bounded height so one rambling message cannot push the
   next twenty rows off the screen. */
.am-bidmail-body {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  max-height: 22rem;
  overflow-y: auto;
  margin: .4rem 0 .3rem;
  padding: .6rem .75rem;
  background: var(--am-surface-sunk);
  border-left: 2px solid var(--am-border);
  font-size: var(--am-text-sm);
}

.am-bidmail-more {
  margin: 0 0 .3rem;
  font-size: var(--am-text-xs);
  color: var(--am-text-muted);
}

/* The sender's signature, folded under their message -- on the mailbox
   screen and on the job thread alike. Kept, because it is usually the only
   place their phone number is written down; folded, because it is the same
   card on every message the firm sends and the question above it is what the
   reader came for. The summary line says what is inside. */
.am-sig {
  margin: 0 0 .4rem;
  font-size: var(--am-text-xs);
  color: var(--am-text-muted);
}

.am-sig > summary {
  cursor: pointer;
  width: fit-content;
}

.am-sig > div {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  margin: .3rem 0 0;
  padding: .4rem .75rem;
  border-left: 2px solid var(--am-border);
}

.am-conv-sig {
  margin-top: .25rem;
}

/* The decision column. The picker and its button sit on one line and wrap
   together, so a narrow screen never strands "Attach" under a select it no
   longer looks attached to. */
.am-bidmail-do {
  display: flex;
  flex-direction: column;
  gap: .4rem;
  align-items: flex-start;
}

.am-bidmail-attach {
  display: flex;
  flex-wrap: wrap;
  gap: .35rem;
  align-items: center;
  margin: 0;
}

.am-bidmail-attach select,
.am-bidmail-rule select {
  max-width: 16rem;
  font-size: var(--am-text-sm);
}

/* The standing answer, on the same [choice][verb] line as Attach above it, so
   the cell reads as two decisions of the same shape rather than as a control
   and an afterthought. */
.am-bidmail-rule {
  display: flex;
  flex-wrap: wrap;
  gap: .35rem;
  align-items: center;
  margin: 0;
}

/* The rules in force. Below the queue because it answers a question asked
   afterwards -- what is being refused on my behalf -- and it is the reason the
   queue above can be short. */
.am-bidmail-rules {
  margin-top: var(--am-gap-lg, 2rem);
}

.am-bidmail-rules h2 {
  font-size: var(--am-text-lg, 1.15rem);
  margin: 0 0 .3rem;
}

.am-bidmail-ruletable td { vertical-align: top; }
.am-bidmail-ruletable form { margin: 0; }

.am-bidmail-do form { margin: 0; }

/* The tick column. Sized to the box and nothing more -- this is the one column
   whose width carries no information, and every character it takes comes off
   the message column, which is the only one anybody reads down. */
.am-bidmail-table .am-bidmail-pick {
  width: 1%;
  white-space: nowrap;
  vertical-align: middle;
}

/* The row being read. A left edge rather than a fill: the row is still part of
   the list, and a filled row reads as selected-for-an-action instead. */
.am-bidmail-table tr.is-open td:first-child {
  box-shadow: inset 3px 0 0 0 var(--am-accent, currentColor);
}

/* ---------------------------------------------------------------------------
   Conversations, on a contact

   A transcript, so it reads down rather than across. The two directions are
   distinguished by which side the rule sits on and by the ground behind them --
   not by colour alone, which would carry no meaning to a screen reader and
   none at all to somebody who prints the page.
   ------------------------------------------------------------------------ */

.am-conv-list {
  list-style: none;
  margin: .6rem 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: .55rem;
}

.am-conv-item {
  padding: .6rem .8rem;
  border: 1px solid var(--am-border);
  border-left-width: 3px;
  background: var(--am-surface);
}

/* Theirs sits flush and quiet; ours is indented and sunk, so a long thread
   reads as two voices without either being shouted. */
.am-conv-item--in {
  border-left-color: var(--am-accent, currentColor);
}

.am-conv-item--out {
  border-left-color: var(--am-border);
  background: var(--am-surface-sunk);
  margin-left: clamp(0px, 4vw, 2.25rem);
}

.am-conv-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .2rem .7rem;
  margin: 0 0 .25rem;
  font-size: var(--am-text-xs);
  color: var(--am-text-muted);
}

.am-conv-dir {
  font-weight: 650;
  color: var(--am-text);
}

.am-conv-when { font-variant-numeric: tabular-nums; }

/* The job is the one piece of metadata somebody scans a thread for -- "which
   of these was about the school?" -- so it is not muted into the row. */
.am-conv-job { color: var(--am-text); }

.am-conv-subject { margin: 0 0 .15rem; font-size: var(--am-text-sm); }

.am-conv-text {
  margin: 0;
  font-size: var(--am-text-sm);
  color: var(--am-text-muted);
  overflow-wrap: anywhere;
}

/* ---------------------------------------------------------------------------
   One firm on one job: the delivery screen.

   Read top to bottom in one screen height on a laptop: who, which job, one
   row of facts, the thread. The two sections that leave the job are folded at
   the foot. Nothing here is a card -- the whole screen is one column of dense
   rows, and its density is the point.
   ------------------------------------------------------------------------ */

/* The head: firm as the title, the person and how to reach them under it,
   the job under that. Three lines, each shorter than the one above. */
.am-delivery-who {
  min-width: 0;
}

.am-delivery-contact {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .1rem 0;
  margin: .15rem 0 0;
  font-size: var(--am-text-sm);
  color: var(--am-text-muted);
}

/* The pieces are separated by a dot drawn by the sheet, so a record with one
   field reads as one word and not as a word between two dots. */
.am-delivery-contact > * + *::before {
  content: "·";
  margin: 0 .45rem;
  color: var(--am-text-subtle);
}

.am-delivery-person {
  color: var(--am-text);
  font-weight: 600;
}

.am-delivery-role {
  font-weight: 400;
  color: var(--am-text-muted);
}

.am-delivery-phone {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.am-delivery-email {
  overflow-wrap: anywhere;
}

.am-delivery-nodir {
  font-style: italic;
}

.am-delivery-job {
  margin: .5rem 0 0;
  font-size: var(--am-text-sm);
  color: var(--am-text-muted);
}

.am-delivery-job strong {
  color: var(--am-text);
  font-size: var(--am-text-md);
}

.am-delivery-job-label {
  text-transform: uppercase;
  letter-spacing: .04em;
  font-size: var(--am-text-xs);
  margin-right: .3rem;
}

.am-delivery-due {
  font-variant-numeric: tabular-nums;
}

/* THE FACT STRIP. A definition list laid out as one row of small labelled
   values, wrapping to two on a phone. The label is the small line, the value
   the readable one -- the same relation as a column heading to its cell,
   which is what these are: one row of the invitation record, turned to face
   the reader. */
.am-facts {
  display: flex;
  flex-wrap: wrap;
  gap: 0;
  margin: 0 0 var(--am-gap);
  border: 1px solid var(--am-border);
  border-radius: var(--am-radius-sm);
  background: var(--am-surface);
  overflow: hidden;
}

.am-fact {
  flex: 1 1 8rem;
  min-width: 0;
  padding: .55rem .85rem .6rem;
  border-right: 1px solid var(--am-border);
  border-top: 1px solid var(--am-border);
  margin-top: -1px;
}

.am-fact:last-child {
  border-right: 0;
}

.am-fact dt {
  margin: 0;
  font-size: var(--am-text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--am-text-muted);
}

.am-fact dd {
  margin: .1rem 0 0;
  font-size: var(--am-text-sm);
  font-weight: 600;
  color: var(--am-text);
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}

/* The second line of a value -- the delivery outcome under its timestamp. */
.am-fact-note {
  display: block;
  font-size: var(--am-text-xs);
  font-weight: 500;
  color: var(--am-text-muted);
}

.am-fact-quiet {
  color: var(--am-text-muted);
  font-weight: 500;
}

/* The outcome cell wears the outcome's colour on its top edge and its note,
   never as a fill: a red block in a row of grey cells reads as an error box,
   and this is a fact, not an error. */
.am-fact--ok     { box-shadow: inset 0 3px 0 0 var(--am-success); }
.am-fact--warn   { box-shadow: inset 0 3px 0 0 var(--am-amber); }
.am-fact--danger { box-shadow: inset 0 3px 0 0 var(--am-danger); }

.am-fact--ok .am-fact-note     { color: var(--am-success-ink); }
.am-fact--warn .am-fact-note   { color: var(--am-warn-ink); }
.am-fact--danger .am-fact-note { color: var(--am-danger-ink); }
.am-fact--warn dd              { color: var(--am-warn-ink); }

/* The copy button sits in the strip as one more cell, so the sentence it
   copies is beside the facts it is made of. */
.am-fact--tool {
  flex: 0 0 auto;
  margin-left: auto;
}

.am-fact--tool dd {
  margin-top: .2rem;
}

/* THE THREAD. Its head names the job, because a thread read against the
   wrong job is a promise made about the wrong drawings. The count beside it
   is the one number a project manager is here for: how many are waiting. */
.am-thread-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: .25rem 1rem;
}

.am-thread-head .am-section-title--sub {
  margin-top: var(--am-gap-sm);
  margin-bottom: 0;
}

.am-thread-job {
  color: var(--am-accent-ink);
}

.am-thread-count {
  margin: 0;
  font-size: var(--am-text-xs);
  color: var(--am-text-muted);
}

.am-thread-list {
  margin-top: .6rem;
  gap: .5rem;
}

/* Tighter than the contact-screen transcript it inherits from: this one is
   read with a deadline running and the words are the point, not the boxes.
   Ours is NOT indented here: the side tag says whose it is, and the indent
   was reading as a reply nested under whatever happened to be above it. */
.am-thread-list .am-conv-item {
  padding: .5rem .8rem .55rem;
}

.am-thread-list .am-conv-item--out {
  margin-left: 0;
}

.am-thread-list .am-conv-meta {
  margin-bottom: .35rem;
}

/* WHOSE MESSAGE. A small capitals tag leading every entry, the same shape as
   the labels on the fact strip: the one thing a reader skimming a thread has
   to be able to tell at a glance, and colour alone would not tell a screen
   reader or a printout. Theirs in the accent, ours plain. */
.am-conv-tag {
  display: inline-block;
  padding: .05rem .4rem;
  border-radius: var(--am-radius-sm);
  font-size: var(--am-text-xs);
  font-weight: 700;
  letter-spacing: .05em;
  text-transform: uppercase;
  line-height: 1.4;
  color: var(--am-text-muted);
  background: color-mix(in oklab, var(--am-text-muted) 12%, transparent);
}

.am-conv-tag--in {
  color: var(--am-accent-ink);
  background: color-mix(in oklab, var(--am-accent) 14%, transparent);
}

/* The labels on the subject and the opening: the same small-capitals voice
   as the tag, without its box, so "Subject" is read as a label and never as
   the first word of the subject. */
.am-conv-label {
  display: inline-block;
  min-width: 4.2rem;
  margin-right: .25rem;
  font-size: var(--am-text-xs);
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--am-text-muted);
}

.am-conv-subject {
  display: flex;
  align-items: baseline;
  gap: .25rem;
}

.am-conv-text .am-conv-label {
  float: left;
}

.am-thread-list .am-conv-text {
  color: var(--am-text);
}

/* A message nobody has dealt with. The edge turns amber, which is the same
   signal the invitation record uses for the same fact, and the only colour
   on a thread that is otherwise grey and blue. */
.am-conv-item--open {
  border-left-color: var(--am-amber);
}

/* Where their message stands, at the end of its meta line. It is the answer
   to "has anybody dealt with this", so it is not muted like the rest of the
   line -- the pill classes it carries colour it. */
.am-conv-status {
  margin-left: auto;
  white-space: nowrap;
}

/* Which question a reply answered. A link back up the thread, in the muted
   size of the line it sits on -- the reader clicks it once to check, and
   otherwise reads past it. */
.am-conv-re {
  color: var(--am-text-muted);
  text-decoration: underline;
  text-decoration-color: var(--am-border-strong);
  text-underline-offset: .15em;
}

.am-conv-re:hover,
.am-conv-re:focus-visible {
  color: var(--am-accent-ink);
}

/* Reply and Acknowledge under the message they act on. The explanation
   beside them is one line in the muted size, because Acknowledge is the one
   button on the screen whose meaning is not the same as its label. */
.am-conv-actions {
  margin-top: .5rem;
  padding-top: .45rem;
  border-top: 1px dashed var(--am-border);
}

.am-conv-hint {
  flex-basis: 100%;
  margin-top: .1rem;
  font-size: var(--am-text-xs);
  color: var(--am-text-muted);
}

.am-thread-note {
  margin-top: .5rem;
}

/* A count beside a heading: "3 messages across 5 jobs". Muted and unbolded,
   so the heading stays the heading. */
.am-panel-count {
  margin-left: .5rem;
  font-size: var(--am-text-xs);
  font-weight: 500;
  color: var(--am-text-muted);
}

.am-delivery-sent-h {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
}

/* THE CROSS-JOB FOLD, one row per message: when, job, who, what -- scanned,
   not read. The whole exchange is on the job's own thread. The same table
   component as the correspondence panel, so it has headings and folds to
   cards below 768px like everything else in the console. */
.am-convx td {
  vertical-align: top;
}

@media (min-width: 768px) {
  .am-convx .am-convx-when { width: 5.5rem; }
  .am-convx .am-convx-job  { width: 14rem; }
  .am-convx .am-convx-who  { width: 12rem; }
}

.am-convx-job {
  overflow-wrap: anywhere;
}

.am-convx-who .am-conv-tag {
  margin-right: .3rem;
}

.am-convx-name {
  font-size: var(--am-text-sm);
  overflow-wrap: anywhere;
}

.am-convx-subject {
  display: block;
  font-size: var(--am-text-sm);
}

/* The opening, one line and no more: this list is for spotting a message,
   and the thread is for reading it. Overrides the two-line clamp
   .am-inbox-msg gives its snippet. */
.am-convx-body > .am-convx-text {
  display: block;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  -webkit-line-clamp: unset;
  line-clamp: unset;
}

/* THE FOLDS. A native <details>, so it opens with the keyboard and with
   script blocked. The summary carries the counts, so the fold closed still
   answers the question; opening it is for the table. */
.am-delivery-folds {
  margin-top: var(--am-gap-lg);
  border-top: 1px solid var(--am-border);
}

.am-fold {
  border-bottom: 1px solid var(--am-border);
}

.am-fold > summary {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .2rem .6rem;
  padding: .6rem 0 .6rem 1.2rem;
  cursor: pointer;
  list-style: none;
  position: relative;
}

.am-fold > summary::-webkit-details-marker {
  display: none;
}

/* The marker is drawn by the sheet so it can turn: a triangle that points
   right when closed and down when open is the one convention every reader
   already has for "there is more under this". */
.am-fold > summary::before {
  content: "";
  position: absolute;
  left: .15rem;
  top: .95rem;
  border: .32rem solid transparent;
  border-left-color: var(--am-text-muted);
  transition: transform var(--am-speed) var(--am-ease);
}

.am-fold[open] > summary::before {
  transform: translateY(.1rem) rotate(90deg);
}

.am-fold > summary:focus-visible {
  outline: none;
  box-shadow: var(--am-ring);
  border-radius: var(--am-radius-sm);
}

.am-fold-title {
  font-weight: 650;
  font-size: var(--am-text-md);
  color: var(--am-text);
}

.am-fold-sub {
  font-size: var(--am-text-xs);
  color: var(--am-text-muted);
}

.am-fold-body {
  padding: .25rem 0 var(--am-gap) 1.2rem;
}

.am-fold-body > .am-panel > .am-panel-note:first-of-type {
  margin-top: 0;
}

/* Inside a fold the panel's own frame would be a box in a box. */
.am-fold-body .am-panel {
  border: 0;
  padding: 0;
  box-shadow: none;
  background: transparent;
}

.am-fold-body .am-panel-head {
  display: none;
}

/* The "Dealt with" cell on the correspondence panel: the sentence and the
   buttons under it, the same stack as the cell to its left. */
.am-inbox-dealt .am-actionrow {
  margin-top: .25rem;
}

/* ---------------------------------------------------------------------------
   OFFERS: one contractor's whole bid.

   The screen a project manager reads a bid on, and the panel on the invitation
   record that leads to it. Two decisions here are about attention rather than
   taste.

   Filenames are LINKS AND LOOK LIKE IT. This is the one screen in the portal
   where the thing somebody came for is a file, and a download styled as body
   text is a download people ask where to find.

   Flags are ORANGE AND ON THE ROW. "This file's contents do not match its
   name" is the sentence that stops somebody double-clicking a bid that is not
   a bid, and it is worth nothing at the bottom of the page.
   ------------------------------------------------------------------------ */

/* An offer is a stack of messages, each one a small document of its own. */
.am-offer-msg {
  padding: .75rem .9rem;
  margin: .7rem 0 0;
  border: 1px solid var(--am-border);
  border-left-width: 3px;
  border-left-color: var(--am-accent, currentColor);
  background: var(--am-surface);
}

.am-offer-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .2rem .7rem;
  margin: 0 0 .3rem;
  font-size: var(--am-text-xs);
  color: var(--am-text-muted);
}

.am-offer-when {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.am-offer-subject {
  margin: 0 0 .25rem;
  font-size: var(--am-text-md);
  line-height: 1.3;
  text-wrap: balance;
}

.am-offer-text {
  margin: 0;
  font-size: var(--am-text-sm);
  color: var(--am-text-muted);
  overflow-wrap: anywhere;
}

.am-offer-files {
  list-style: none;
  margin: .6rem 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: .3rem;
}

.am-offer-file {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .2rem .6rem;
  padding: .35rem .5rem;
  background: var(--am-surface-sunk);
  border: 1px solid var(--am-border);
}

/* The flagged row is the exception, so it is the only one that carries colour.
   A border rather than a fill: the filename still has to be the thing the eye
   lands on, because it is still the thing being opened. */
.am-offer-file--flag {
  border-color: var(--am-warn-ink);
}

.am-offer-filename {
  font-weight: 650;
  overflow-wrap: anywhere;
}

.am-offer-size {
  font-size: var(--am-text-xs);
  color: var(--am-text-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Given the whole width below the name, because it is a sentence to read
   rather than a label to glance at. */
.am-offer-flag {
  flex-basis: 100%;
  font-size: var(--am-text-xs);
  color: var(--am-warn-ink);
  font-weight: 600;
}

/* The document count on the offers table lines up down the column, so "1" next
   to "14" reads as a difference in size rather than as two words. */
.am-offers-table td[data-label]:nth-child(2),
.am-offers-table td[data-label]:nth-child(3) {
  font-variant-numeric: tabular-nums;
}

/* ---------------------------------------------------------------------------
   ICONS ON THE CORRESPONDENCE AND OFFER SURFACES.

   One rule governs every glyph below, and it is the rule that keeps a screen
   readable rather than decorated: COLOUR AND ICONS GO ON EXCEPTIONS. A file
   that arrived draws a muted type glyph and nothing else; a file whose contents
   disagree with its name draws amber. If everything were marked, nothing would
   be.

   Every icon here also sits beside words saying the same thing -- .am-i is
   aria-hidden, so a glyph alone would be a state a screen reader cannot see and
   a printout does not carry.
   ------------------------------------------------------------------------ */

/* Panel headings. Muted, because the heading text is the thing being read and
   the glyph is only there to make the panel findable when scrolling past. */
.am-panel-ico {
  color: var(--am-text-muted);
  margin-inline-end: .35rem;
}

.am-panel-note-ico,
.am-verdict-ico {
  margin-inline-end: .3rem;
}

/* Inherits the verdict's own ink, so the amber box gets an amber glyph and the
   neutral one a neutral glyph without a rule per variant. */
.am-verdict--warn   .am-verdict-ico { color: var(--am-warn-ink); }
.am-verdict--danger .am-verdict-ico { color: var(--am-danger-ink); }
.am-verdict--success .am-verdict-ico { color: var(--am-success-ink); }

.am-rowstate-i {
  margin-inline-end: .25rem;
}

/* --- Offers table ------------------------------------------------------- */

.am-offers-ico {
  color: var(--am-text-muted);
  margin-inline-end: .3rem;
}

/* "None" is a real answer and not a missing value, so it is muted rather than
   left looking like an empty cell -- but it does not get the file glyph, which
   would spend a recognisable mark on nothing being there. */
.am-offers-none {
  color: var(--am-text-muted);
}

/* The one piece of colour on the table, and only on rows somebody has to do
   something about. It runs down the left edge so a job with one firm in
   trouble can be found by scanning an edge rather than reading every cell. */
.am-offers-row--act > td:first-child {
  box-shadow: inset 3px 0 0 var(--am-amber);
}

/* Below 768px .am-usertable folds each row into a card, and a left edge on the
   first cell would mark one line of it rather than the row. The whole card
   takes the edge instead. */
@media (max-width: 767.98px) {
  .am-offers-row--act > td:first-child { box-shadow: none; }

  .am-offers-row--act {
    box-shadow: inset 3px 0 0 var(--am-amber);
  }
}

/* --- The offer screen --------------------------------------------------- */

.am-offer-back {
  margin: 0 0 var(--am-gap);
  font-size: var(--am-text-sm);
}

/* Pointing back the way you came. The glyph is flipped rather than a second
   icon being added to the set for it. */
.am-offer-back-ico {
  transform: rotate(180deg);
  margin-inline-end: .15rem;
}

/* The file type glyph. Muted on purpose: it tells you what KIND of thing this
   is while scanning, and the filename beside it is what gets read and clicked.
   A coloured icon here would compete with the flag below it, which is the one
   thing on this screen that has to be noticed. */
.am-offer-ico {
  color: var(--am-text-muted);
  font-size: 1.1em;
}

.am-offer-dl {
  color: var(--am-text-muted);
  margin-inline-start: auto;
}

/* Muted to full text ink on hover, rather than to the accent. The accent is not
   in the contrast matrix as an INK colour -- it is a border and a rail -- and
   measuring a new one to tint a hover state would be spending a token on
   decoration. Darkening says "this row is live" just as well. */
.am-offer-file:hover .am-offer-dl,
.am-offer-file:focus-within .am-offer-dl {
  color: var(--am-text);
}

/* A document whose bytes were deleted under the retention rule. Struck through
   rather than hidden: that it arrived is still a fact about the bid. */
.am-offer-filename--gone {
  color: var(--am-text-muted);
  text-decoration: line-through;
}

.am-offer-flag-ico {
  margin-inline-end: .25rem;
}

.am-offer-pending {
  margin: .5rem 0 0;
  font-size: var(--am-text-xs);
  color: var(--am-text-muted);
}

.am-offer-pending-ico {
  margin-inline-end: .3rem;
}

/* The standing caution about scanning. Deliberately NOT an alert box: it is
   true on every offer screen forever, and an alarm that is always on is how
   people learn to skip the alarms that are not. */
.am-offer-caution {
  display: flex;
  align-items: start;
  gap: .5rem;
  margin: var(--am-gap) 0 0;
  padding-top: var(--am-gap-sm);
  border-top: 1px solid var(--am-border);
  font-size: var(--am-text-xs);
  color: var(--am-text-muted);
}

.am-offer-caution-ico {
  color: var(--am-text-muted);
  margin-block-start: .15em;
}

/* =============================================================================
   The one way to write to a project
   ========================================================================== */

/* Replaces the two staff mailto: links that used to sit under the contact
   names. The names stay -- a bidder needs to know who is running the job --
   and the address is the project's own, so what they send is on the record
   rather than in somebody's inbox. See the Contacts block in
   plugins/americon-project-details.

   A block rather than an inline link: it is the action on this half of the
   page, and inline beside a name it read as a third contact detail. */
.am-reply-route {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--am-gap-sm) var(--am-gap);
  margin-block-end: var(--am-gap-lg);
}

/* The sentence carries the whole promise the button cannot: who it reaches and
   that it is kept. Muted and small because it is read once -- but it is not
   optional, and it wraps under the button rather than being clipped. */
.am-reply-route-note {
  flex: 1 1 16rem;
  min-width: 0;
  font-size: var(--am-text-xs);
  line-height: 1.5;
  color: var(--am-text-muted);
}

/* ---------------------------------------------------------------------------
   The section rule inside the correspondence table

   Correspondence arrives as two piles with opposite handling rules: what is
   waiting for a person, and what is settled or collecting until the bid date.
   They are two sections of ONE table rather than two tables, because the
   columns are the same columns and a second <thead> would halve the rows a
   coordinator can see at once.

   So the divider has to read as a divider and not as a row of data: sunk
   ground, small caps, and a top rule that is heavier than the row rules around
   it. It is a <th scope="colgroup">, so a screen reader announces it as the
   heading for what follows rather than as a cell.
   ------------------------------------------------------------------------ */

.am-inbox-sect > th {
  background: var(--am-surface-sunk);
  border-top: 2px solid var(--am-border-strong);
  padding: .45rem .6rem;
  font-size: var(--am-text-xs);
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--am-text);
  text-align: left;
}

/* The one-line reason, on the same row. Not a second row: this is a label, and
   a two-row label inside a table of messages is a paragraph in a worklist. */
.am-inbox-sect-note {
  margin-left: .5rem;
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  color: var(--am-text-muted);
}

/* The first section never needs the rule above it -- there is nothing there to
   be divided from. */
.am-inbox-table tbody > .am-inbox-sect:first-child > th { border-top: 0; }

/* A scanned document says so on its own line, muted. Deliberately NOT
   .am-offer-flag: that one is amber and means "take care before you open
   this", and spending a security mark on a fact about file formats is how the
   warning that mattered stops being read. */
.am-offer-scan {
  display: block;
  margin-block-start: .15rem;
  font-size: var(--am-text-xs);
  color: var(--am-text-muted);
}

/* --- "Tell us your number" (contractor, on the project page) -------------
 *
 * Sits at the foot of the page, after the plan files, and is deliberately
 * quiet: a sunk panel rather than a card, and no colour of its own. It is an
 * optional courtesy, and a block competing with the drawings for attention
 * would read as something they had to do before downloading.
 */

.am-figure {
  margin: 0;
  padding: clamp(.75rem, .5rem + .6vw, 1rem);
  background: var(--am-surface-sunk);
  border: 1px solid var(--am-border);
  border-radius: var(--am-radius);
}

.am-figure-head {
  margin: 0 0 .25rem;
  font-size: var(--am-text-base, 1rem);
  font-weight: 700;
}

.am-figure-note,
.am-figure-seal,
.am-figure-have {
  margin: 0 0 .6rem;
  font-size: var(--am-text-xs);
  color: var(--am-text-subtle);
}

.am-figure-seal { margin: .6rem 0 0; }

.am-figure-said {
  margin: 0 0 .6rem;
  font-size: var(--am-text-sm);
  font-weight: 600;
}

.am-figure-said--warn { color: var(--am-danger-ink); }

.am-figure-row {
  display: flex;
  flex-direction: column;
  gap: .25rem;
  margin: 0 0 .6rem;
}

.am-figure-row label {
  font-size: var(--am-text-xs);
  font-weight: 600;
  color: var(--am-text-subtle);
}

.am-figure-amount,
.am-figure-excl {
  width: 100%;
  max-width: 22rem;
  padding: .4rem .5rem;
  font-size: var(--am-text-sm);
  border: 1px solid var(--am-border);
  border-radius: calc(var(--am-radius) / 2);
  background: var(--am-surface);
  color: var(--am-text);
}

.am-figure-amount { font-variant-numeric: tabular-nums; }

/* The dollar sign sits inside the box's outline, so the pair reads as one
   field rather than a symbol beside an input. */
.am-figure-money {
  display: flex;
  align-items: center;
  width: 100%;
  max-width: 22rem;
  border: 1px solid var(--am-border);
  border-radius: calc(var(--am-radius) / 2);
  background: var(--am-surface);
}

.am-figure-money:focus-within {
  outline: 2px solid var(--am-accent);
  outline-offset: 1px;
}

.am-figure-currency {
  padding: 0 0 0 .5rem;
  font-size: var(--am-text-sm);
  color: var(--am-text-subtle);
  user-select: none;
}

.am-figure-money .am-figure-amount {
  max-width: none;
  border: 0;
  border-radius: 0;
  background: transparent;
  padding-left: .25rem;
}

.am-figure-money .am-figure-amount:focus { outline: none; }

.am-figure-excl { max-width: 34rem; }

.am-figure-adds {
  margin: 0 0 .6rem;
  padding: 0;
  border: 0;
}

.am-figure-adds legend {
  padding: 0;
  font-size: var(--am-text-xs);
  font-weight: 600;
  color: var(--am-text-subtle);
}

/* Wrapped so a job with nine addenda does not run off a phone, and each box
   stays joined to its own label when it does. */
.am-figure-add {
  display: inline-flex;
  align-items: center;
  gap: .3rem;
  margin: .25rem .75rem .25rem 0;
  font-size: var(--am-text-sm);
  white-space: nowrap;
}

.am-figure-actions { margin: 0; }

/* The scope questions. One line per item, label left and the two answers
   right, so twelve lines read as a checklist rather than as twelve forms.
   No third option: a blank is recorded as nothing, and a control that says
   "don't know" would only be pressed to make the form look finished. */
.am-figure-scope {
  margin: 0 0 .6rem;
  padding: 0;
  border: 0;
  max-width: 34rem;
}

.am-figure-scope legend {
  padding: 0;
  font-size: var(--am-text-xs);
  font-weight: 600;
  color: var(--am-text-subtle);
}

.am-figure-lines {
  margin: .25rem 0 0;
  padding: 0;
  list-style: none;
}

.am-figure-line {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: .75rem;
  padding: .3rem 0;
  border-bottom: 1px solid var(--am-border);
  font-size: var(--am-text-sm);
}

.am-figure-line:last-child { border-bottom: 0; }

.am-figure-line-opts {
  display: inline-flex;
  gap: .75rem;
  white-space: nowrap;
}

.am-figure-opt {
  display: inline-flex;
  align-items: center;
  gap: .3rem;
}

/* --- The levelling sheet -------------------------------------------------
 *
 * A matrix, so it is the one table on this site that is genuinely wide. It
 * lives inside .am-tablewrap like every other, which scrolls sideways rather
 * than letting the page do it -- the alternative is a body that scrolls
 * horizontally, which breaks every other screen.
 */

.am-level-note,
.am-level-empty,
.am-level-full {
  margin: 0 0 .75rem;
  font-size: var(--am-text-sm);
  color: var(--am-text-subtle);
}

.am-level-sub {
  margin: 1.1rem 0 .4rem;
  font-size: var(--am-text-sm);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--am-text-subtle);
}

.am-level-add {
  display: flex;
  flex-wrap: wrap;
  gap: .4rem;
  margin: 0 0 .9rem;
}

.am-level-add input[type="text"] {
  padding: .35rem .5rem;
  font-size: var(--am-text-sm);
  border: 1px solid var(--am-border);
  border-radius: calc(var(--am-radius) / 2);
  background: var(--am-surface);
  color: var(--am-text);
  min-width: 12rem;
}

.am-level-add .am-level-plugin {
  min-width: 9rem;
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* The firm columns are the comparison, so they hold their width rather than
   squeezing to fit -- three columns crushed to forty pixels each is a sheet
   that technically fits and cannot be read. The wrapper scrolls instead. */
.am-level-firm {
  min-width: 7.5rem;
}

.am-level-line {
  min-width: 11rem;
  font-weight: 600;
}

/* Remove sits under its line rather than beside it: a destructive control on
   the same line as the label is the one people hit while aiming at the label. */
.am-level-drop { margin: .15rem 0 0; }

.am-level-dropbtn {
  padding: 0;
  border: 0;
  background: none;
  font-size: var(--am-text-xs);
  color: var(--am-text-muted);
  text-decoration: underline;
  cursor: pointer;
}

.am-level-dropbtn:hover { color: var(--am-danger-ink); }

.am-level-cell {
  white-space: nowrap;
  text-align: center;
}

.am-level-opt {
  display: inline-flex;
  align-items: center;
  gap: .15rem;
  margin: 0 .25rem;
  font-size: var(--am-text-xs);
}

/* Only the state that moves money is tinted. "In" and "?" are the ordinary
   readings and wear nothing -- if every cell carried colour, the one that
   changes a total would not stand out from the other thirty-nine. */
.am-level-cell--exc { background: var(--am-surface-sunk); }

/* Whose answer a cell is. A cell standing as the firm ticked it says so in
   small muted type under the radios -- provenance, not a warning. A cell
   where somebody here recorded the opposite carries the firm's word in the
   warning colour, because that is the disagreement worth an email. */
.am-level-src {
  display: block;
  margin-block-start: .1rem;
  font-size: var(--am-text-xs);
  color: var(--am-text-muted);
  white-space: nowrap;
}

.am-level-src--disputed { color: var(--am-warn-ink); font-weight: 600; }

.am-level-cell--theirs { border-block-end: 2px dotted var(--am-border); }

.am-level-theirs-n {
  display: block;
  margin-block-start: .2rem;
  font-size: var(--am-text-xs);
  font-weight: 400;
  color: var(--am-text-muted);
  white-space: normal;
}

/* The template controls: one line, two small forms, under the add form. */
.am-level-templates {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .5rem 1rem;
  margin: 0 0 .75rem;
}

.am-level-template {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  margin: 0;
}

.am-level-template-note {
  font-size: var(--am-text-xs);
  color: var(--am-text-muted);
}

.am-level-template-save {
  padding: 0;
  font-size: var(--am-text-xs);
}

.am-level-table tfoot th,
.am-level-table tfoot td {
  border-top: 1px solid var(--am-border);
  font-size: var(--am-text-sm);
}

.am-level-total td { font-size: var(--am-text-base, 1rem); }

.am-level-short,
.am-level-theirs {
  display: block;
  margin-block-start: .2rem;
  font-size: var(--am-text-xs);
  font-weight: 400;
  white-space: normal;
}

/* The one that says the total is wrong, rather than merely incomplete. */
.am-level-short { color: var(--am-danger-ink); }

.am-level-theirs { color: var(--am-text-muted); }

.am-level-back { margin: 0 0 .75rem; font-size: var(--am-text-sm); }

/* --- Trade scoping, on both screens --------------------------------------
 *
 * A job carries one invitation list across every trade it needs, so both the
 * tabulation and the levelling sheet split by trade -- an electrician and a
 * painter are never compared in one column. On the tabulation that is a
 * heading over each group; on the sheet it is a row of links, one sheet per
 * trade, because the baseline is different for each.
 */

.am-tab-trade {
  margin: 1.4rem 0 .35rem;
  font-size: var(--am-text-sm);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--am-text-subtle);
}

.am-tab-trade:first-of-type { margin-top: .25rem; }

.am-level-trades {
  display: flex;
  flex-wrap: wrap;
  gap: .35rem;
  margin: 0 0 .9rem;
}

.am-level-trade {
  padding: .3rem .7rem;
  font-size: var(--am-text-sm);
  border: 1px solid var(--am-border);
  border-radius: 999px;
  background: var(--am-surface);
  color: var(--am-text);
  text-decoration: none;
}

.am-level-trade:hover { background: var(--am-surface-sunk); }

/* The one being shown is filled, not merely underlined: which trade's baseline
   you are editing is the thing most worth not getting wrong on this screen. */
.am-level-trade.is-current {
  background: var(--am-accent);
  border-color: var(--am-accent);
  color: #fff;
}

/* --- Bid tabulation ------------------------------------------------------
 *
 * The table is .am-usertable, so the density, the borders and the fold into
 * cards below 768px all come from there. Only what is peculiar to this screen
 * is below: an editable money column, and a flag column that has to be read
 * rather than glanced at.
 *
 * ONE LINE PER FIRM. This screen is judged by how many bidders fit on it, and
 * an early draft put the provenance note under the amount field and each flag
 * on its own line, so a flagged row was three lines tall. Everything in a row
 * now runs inline -- the note beside the field, the flags along the row with
 * a dot between them -- and the row only wraps when a firm genuinely carries
 * more than fits. Forty firms is forty lines.
 */

.am-tab-table td { padding-block: .35rem; }

.am-tab-summary {
  margin: 0 0 .75rem;
  font-size: var(--am-text-sm);
  color: var(--am-text-subtle);
}

.am-tab-spread,
.am-tab-flagged {
  margin-inline-start: .5rem;
}

/* The count of rows needing attention is the one figure on the summary line
   somebody acts on, so it is the only one that carries colour. */
.am-tab-flagged {
  color: var(--am-warn-ink);
  font-weight: 600;
}

/* NUMBERS RIGHT-ALIGNED AND TABULAR.
   This column exists to be compared down its length, and proportional digits
   put the thousands column in a different place on every row -- which is
   exactly the comparison the column is for. */
.am-tab-num {
  text-align: right;
  white-space: nowrap;
}

.am-tab-amount-form {
  display: inline-flex;
  gap: .35rem;
  justify-content: flex-end;
  align-items: center;
  vertical-align: middle;
  margin: 0;
}

.am-tab-amount {
  width: 9.5rem;
  padding: .3rem .45rem;
  text-align: right;
  font-variant-numeric: tabular-nums;
  font-size: var(--am-text-sm);
  border: 1px solid var(--am-border);
  border-radius: calc(var(--am-radius) / 2);
  background: var(--am-surface);
  color: var(--am-text);
}

/* A NUMBER NOBODY HAS STOOD BEHIND MUST NOT LOOK LIKE ONE THAT SOMEBODY HAS.
   Dashed rather than merely tinted: a colour difference alone disappears for
   anybody who cannot see it, and this is the distinction the whole extraction
   phase rests on. The button beside it reads "Confirm" rather than "Save" for
   the same reason -- the shape of the control says it too. */
.am-tab-amount--unconfirmed {
  border-style: dashed;
  border-color: var(--am-amber, var(--am-border));
  background: var(--am-surface-sunk);
}

.am-tab-save {
  padding: .3rem .6rem;
  font-size: var(--am-text-xs);
  border: 1px solid var(--am-border);
  border-radius: calc(var(--am-radius) / 2);
  background: var(--am-surface-sunk);
  color: inherit;
  cursor: pointer;
}

.am-tab-save:hover { background: var(--am-surface); }

.am-tab-none { color: var(--am-text-muted); }

/* Where an unconfirmed number came from, beside the field it pre-filled.
   Small and muted: it is a provenance note, not a warning -- the flag column
   is where anything needing action is said. */
.am-tab-src {
  display: inline-block;
  margin-inline-start: .4rem;
  font-size: var(--am-text-xs);
  color: var(--am-text-muted);
  vertical-align: middle;
}

/* The flags are words, not chips: "Late", "Not invited", "3 lines undecided"
   need no legend. They run along the row with a dot between them, and each
   carries its full sentence as a title. */
.am-tab-flaglist {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 .35rem;
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: var(--am-text-xs);
  line-height: 1.5;
}

.am-tab-flag { color: var(--am-text-subtle); }

.am-tab-flag + .am-tab-flag::before {
  content: '·';
  margin-inline-end: .35rem;
  color: var(--am-text-muted);
}

/* Only the two that can still be acted on carry colour. If every flag were
   coloured, none of them would be -- and 'noshow' before a deadline is the one
   row on this screen worth interrupting somebody's afternoon for. */
.am-tab-flag--noshow,
.am-tab-flag--late {
  color: var(--am-danger-ink);
}

/* A lapsed certificate is acted on too -- a phone call for the new one --
   but after the deadline, so it takes the warning colour and not the alarm. */
.am-tab-flag--prequal,
.am-tab-flag--disputed {
  color: var(--am-warn-ink);
}

/* Same left edge as the offers table, for the same reason and in the same
   colour: a job with two rows in trouble is found by scanning an edge rather
   than reading every cell. */
.am-tab-row--flag > td:first-child {
  box-shadow: inset 3px 0 0 var(--am-amber);
}

@media (max-width: 767.98px) {
  .am-tab-row--flag > td:first-child { box-shadow: none; }

  .am-tab-row--flag { box-shadow: inset 3px 0 0 var(--am-amber); }

  /* Folded into a card, the money form has no column to be right-aligned
     against and reads better run out to the full width. */
  .am-tab-amount-form { display: flex; justify-content: flex-start; }
  .am-tab-amount { width: 100%; }
  .am-tab-num { text-align: left; }
  .am-tab-src { display: block; margin-inline-start: 0; margin-block-start: .15rem; }
}

/* --- Award ----------------------------------------------------------------
 *
 * Above the tables, because it is the most important fact about a closed
 * bid; compact, because on the day it matters the tables below are what the
 * decision is being made from. One line per trade. The notices fold away in
 * a <details> so the two pre-written messages cost no height until wanted.
 */

.am-award {
  margin: 0 0 1rem;
  padding: .75rem .9rem;
  border: 1px solid var(--am-border);
  border-radius: var(--am-radius);
  background: var(--am-surface-sunk);
}

.am-award-head {
  margin: 0 0 .4rem;
  font-size: var(--am-text-sm);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--am-text-subtle);
}

.am-award-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.am-award-item {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .25rem .6rem;
  padding: .25rem 0;
  font-size: var(--am-text-sm);
}

.am-award-item + .am-award-item { border-top: 1px solid var(--am-border); }

.am-award-trade {
  min-width: 9rem;
  font-weight: 600;
  color: var(--am-text-subtle);
}

.am-award-none { color: var(--am-text-muted); }

.am-award-prequal {
  font-size: var(--am-text-xs);
  color: var(--am-warn-ink);
  font-weight: 600;
}

.am-award-withdraw { display: inline; margin: 0; }

.am-award-withdrawbtn {
  padding: 0;
  border: 0;
  background: none;
  font-size: var(--am-text-xs);
  color: var(--am-text-muted);
  text-decoration: underline;
  cursor: pointer;
}

.am-award-withdrawbtn:hover { color: var(--am-danger-ink); }

.am-award-told,
.am-award-unreachable {
  margin: .5rem 0 0;
  font-size: var(--am-text-xs);
  color: var(--am-text-subtle);
}

.am-award-unreachable,
.am-award-changed { color: var(--am-warn-ink); }

.am-award-notify { margin: .6rem 0 0; font-size: var(--am-text-sm); }

.am-award-notify > summary {
  cursor: pointer;
  font-weight: 600;
  color: var(--am-accent-ink);
}

.am-award-form { margin: .6rem 0 0; }

.am-award-draft { margin: 0 0 .75rem; }

.am-award-draft-to {
  margin: 0 0 .3rem;
  font-size: var(--am-text-xs);
  font-weight: 600;
  color: var(--am-text-subtle);
}

.am-award-draft-note { font-weight: 400; }

.am-award-draft input { margin-block-end: .3rem; }

.am-award-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .5rem;
  margin: 0;
}

.am-award-actions-note {
  font-size: var(--am-text-xs);
  color: var(--am-text-muted);
}

/* On the row: a small outline button under the firm's name once bidding is
   over, and a mark once decided. The rows that lost carry nothing at all --
   forty greyed buttons would be a column of noise beside the one mark that
   matters. */
.am-award-row { display: inline; margin: 0; }

.am-award-btn {
  display: inline-block;
  margin-block-start: .2rem;
  padding: .15rem .5rem;
  font-size: var(--am-text-xs);
  border: 1px solid var(--am-border);
  border-radius: 999px;
  background: var(--am-surface);
  color: var(--am-text);
  cursor: pointer;
}

.am-award-btn:hover {
  border-color: var(--am-accent);
  color: var(--am-accent-ink);
}

.am-award-mark {
  display: inline-block;
  margin-block-start: .2rem;
  padding: .1rem .5rem;
  font-size: var(--am-text-xs);
  font-weight: 700;
  border-radius: 999px;
  background: var(--am-accent);
  color: #fff;
}

/* --- Addenda --------------------------------------------------------------
 *
 * Two places. On the project editor: the issued list and the issue form.
 * On the tabulation: one cell per row, drawn only on a job that has issued
 * one. Both are words in the row's own text sizes -- an addendum is a fact
 * about the bid documents, and the one colour it earns is on the decision
 * that stops an award.
 */

.am-addenda-none { color: var(--am-text-muted); }

.am-addenda-when,
.am-addenda-files {
  display: block;
  font-size: var(--am-text-xs);
  color: var(--am-text-subtle);
}

/* Late is a fact about the timing, in the warning colour and never the
   alarm: it was a decision somebody made and recorded, not a defect. */
.am-addenda-late,
.am-addenda-moved {
  display: inline-block;
  margin-block-start: .2rem;
  padding: .05rem .45rem;
  font-size: var(--am-text-xs);
  font-weight: 600;
  border-radius: 999px;
  background: var(--am-surface-sunk);
  color: var(--am-text-subtle);
}

.am-addenda-late { color: var(--am-warn-ink); }

.am-addenda-told { font-size: var(--am-text-xs); }

/* The issue form. Shut once the job has issued one -- the list is what the
   section is for by then -- and open on a job that has none, so the first
   thing a coordinator sees is the form rather than an empty table. */
.am-addenda-issue {
  margin-block-start: var(--am-gap);
  padding: var(--am-gap-sm) var(--am-gap);
  border: 1px solid var(--am-border);
  border-radius: var(--am-radius);
  background: var(--am-surface);
}

.am-addenda-issue > summary {
  cursor: pointer;
  font-weight: 600;
}

.am-addenda-issue > summary:focus-visible {
  outline: 2px solid var(--am-accent);
  outline-offset: 2px;
}

.am-addenda-form { margin-block-start: var(--am-gap-sm); }

.am-addenda-row { margin: 0 0 var(--am-gap-sm); }

.am-addenda-row label,
.am-addenda-files-pick legend,
.am-addenda-date legend {
  display: block;
  margin-block-end: .25rem;
  font-size: var(--am-text-sm);
  font-weight: 600;
}

.am-addenda-files-pick,
.am-addenda-date {
  margin: 0 0 var(--am-gap-sm);
  padding: 0;
  border: 0;
}

.am-addenda-filelist {
  max-height: 14rem;
  overflow-y: auto;
  margin: 0;
  padding: .25rem 0;
  list-style: none;
  font-size: var(--am-text-sm);
}

.am-addenda-filelist li {
  display: flex;
  align-items: baseline;
  gap: .4rem;
  padding: .1rem 0;
}

.am-addenda-rev,
.am-addenda-fresh {
  margin-inline-start: .35rem;
  font-size: var(--am-text-xs);
  color: var(--am-text-subtle);
}

.am-addenda-fresh { color: var(--am-warn-ink); }

.am-addenda-latenote {
  margin: 0 0 .5rem;
  font-size: var(--am-text-sm);
  color: var(--am-warn-ink);
}

.am-addenda-opt {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .4rem;
  margin: 0 0 .35rem;
  font-size: var(--am-text-sm);
}

.am-addenda-due { max-width: 14rem; }

.am-addenda-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .6rem;
  margin: var(--am-gap-sm) 0 0;
}

.am-addenda-sendnote {
  font-size: var(--am-text-xs);
  color: var(--am-text-subtle);
}

/* The tabulation cell: three lines at most, each in the row's own type. */
.am-addenda-cell {
  display: flex;
  flex-direction: column;
  gap: .25rem;
  font-size: var(--am-text-xs);
  line-height: 1.5;
}

.am-addenda-basis { color: var(--am-text-subtle); }

.am-addenda-basis--staff { color: var(--am-text); }

.am-addenda-ack {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .15rem .4rem;
  margin: 0;
}

.am-addenda-ack-label { color: var(--am-text-subtle); }

.am-addenda-tick {
  display: inline-flex;
  align-items: center;
  gap: .2rem;
}

.am-addenda-decide > summary {
  cursor: pointer;
  color: var(--am-warn-ink);
  font-weight: 600;
}

.am-addenda-decide-form {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .3rem;
  margin: .3rem 0 0;
}

.am-addenda-reason {
  flex: 1 1 14rem;
  min-width: 0;
  padding: .25rem .4rem;
  font-size: var(--am-text-xs);
  border: 1px solid var(--am-border);
  border-radius: calc(var(--am-radius) / 2);
  background: var(--am-surface);
  color: inherit;
}

/* The one act on this screen that stops an award, in the alarm colour on
   the button and on the mark it leaves. A waiver is the quiet answer and
   stays in the row's own ink. */
.am-addenda-setaside { color: var(--am-danger-ink); }

.am-addenda-decision { font-weight: 600; }

.am-addenda-decision--setaside { color: var(--am-danger-ink); }

.am-addenda-reopen { display: inline; margin: 0; }

/* The set-aside notice, on the row: a drafted message behind a summary,
   sent once. Told/not-told is a line in the row's own type; the one colour
   is on the Send button, in the alarm tone the set-aside itself wears. */
.am-addenda-told-firm { color: var(--am-text-subtle); }

.am-addenda-cannot-tell { color: var(--am-warn-ink); }

.am-addenda-notify > summary {
  cursor: pointer;
  font-weight: 600;
}

.am-addenda-notify-form {
  display: flex;
  flex-direction: column;
  gap: .3rem;
  margin: .3rem 0 0;
  min-width: min(100%, 26rem);
}

.am-addenda-notice {
  width: 100%;
  padding: .3rem .4rem;
  font: inherit;
  font-size: var(--am-text-xs);
  line-height: 1.5;
  border: 1px solid var(--am-border);
  border-radius: calc(var(--am-radius) / 2);
  background: var(--am-surface);
  color: inherit;
  resize: vertical;
}

.am-addenda-notify-note {
  margin: 0;
  color: var(--am-text-subtle);
}

.am-addenda-send {
  align-self: flex-start;
  color: var(--am-danger-ink);
}

.am-tab-flag--addenda { color: var(--am-warn-ink); }

.am-tab-flag--nonresponsive { color: var(--am-danger-ink); }

/* On the firm's own form: the sentence under the addenda legend. */
.am-figure-adds-note {
  margin: 0 0 .4rem;
  font-size: var(--am-text-xs);
  color: var(--am-text-subtle);
}

/* --- Prequalification -----------------------------------------------------
 *
 * On the contact form: four short fields on one row. On the directory list
 * and the invitation basket: a word under the name, only when something has
 * lapsed. A column for it would be a dash on nearly every row.
 */

.am-prequal-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr));
  gap: var(--am-gap-sm) var(--am-gap);
  align-items: start;
}

.am-prequal-emr-pair {
  display: flex;
  gap: .4rem;
}

.am-prequal-emr-pair input:last-child { max-width: 6rem; }

.am-prequal-state {
  margin-inline-start: .35rem;
  font-size: var(--am-text-xs);
  font-weight: 600;
  color: var(--am-warn-ink);
}

.am-prequal-state--lapsed { color: var(--am-danger-ink); }

.am-u-prequal,
.am-list-prequal {
  color: var(--am-warn-ink);
  font-weight: 600;
}

.am-list--invite .am-list-prequal {
  display: block;
  font-size: var(--am-text-xs);
}

/* The counts beside the link on the invitation record. This is how a project
   manager learns a figure arrived without an email being sent about it, so it
   sits next to the link rather than under it -- one line, read in passing. */
.am-tab-counts {
  margin-inline-start: .4rem;
  font-size: var(--am-text-xs);
  color: var(--am-text-subtle);
}

.am-tab-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .5rem;
  margin: .9rem 0 0;
}

.am-tab-export-note {
  font-size: var(--am-text-xs);
  color: var(--am-text-muted);
}

.am-tab-foot,
.am-tab-coverage {
  margin: .75rem 0 0;
  font-size: var(--am-text-xs);
  color: var(--am-text-muted);
}

.am-tab-empty {
  margin: 0;
  color: var(--am-text-subtle);
}

/* --- Arriving from the cross-job worklist -------------------------------
 *
 * That screen links a row straight at one message here, so somebody lands
 * mid-panel with no idea which of these forty rows they were sent to. The
 * highlight answers that, and it is the same left edge the worklist marks a
 * row with -- the gesture and its destination look like the same thing.
 *
 * scroll-margin-top so the row does not stop flush against the top of the
 * viewport with its own section heading scrolled off above it. */
.am-inbox-table tbody tr:target {
  background: var(--am-surface-sunk);
  scroll-margin-top: 5rem;
}

.am-inbox-table tbody tr:target > td:first-child {
  box-shadow: inset 3px 0 0 0 var(--am-accent);
}

/* Below 768px each row is a card, and an edge on the first cell would mark one
   line of it. The card takes the edge instead -- the same correction
   .am-offers-row--act makes. */
@media (max-width: 767.98px) {
  .am-inbox-table tbody tr:target > td:first-child { box-shadow: none; }

  .am-inbox-table tbody tr:target {
    box-shadow: inset 3px 0 0 0 var(--am-accent);
  }
}

/* ---------------------------------------------------------------------------
   The cross-job worklist

   One screen listing everything waiting for an answer across every open job.
   It is a dense table on purpose: the measure of it is how many rows fit above
   the fold, because the row somebody has not seen is the one that costs a bid.
   ------------------------------------------------------------------------ */

.am-bidwork-head h1 {
  display: flex;
  align-items: center;
  gap: .4rem;
  margin: 0;
}

.am-bidwork-head p {
  margin: .35rem 0 1rem;
  color: var(--am-text-muted);
  max-width: 68ch;
}

/* The deadline under the job name. Muted by default -- every row on this
   screen is waiting for something, so tinting all of them would spend the one
   signal that separates "closes tomorrow" from "closes in May". */
.am-bidwork-due { display: block; }

.am-bidwork-due--today,
.am-bidwork-due--past  { color: var(--am-danger-ink); font-weight: 650; }
.am-bidwork-due--soon  { color: var(--am-warn-ink);   font-weight: 650; }

/* A bid nobody has opened. A left edge rather than a fill: the row is still an
   ordinary row of the list, and a filled row reads as selected-for-an-action.
   The same decision, for the same reason, as .am-bidmail-table tr.is-open. */
.am-bidwork-row--watch td:first-child {
  box-shadow: inset 3px 0 0 0 var(--am-warn-ink);
}

/* The whole row opens the message it names. The cursor is the only thing that
   says so, and it is scoped to .am-rowlink-ready -- a class set by
   americon-rowlink.js itself -- so a row promises the click only where the
   script that handles it actually loaded. Without it the row is still opened
   by the job name and by "Open the job", and looks like exactly that. */
.am-rowlink-ready .am-bidwork-row--go {
  cursor: pointer;
}

/* The second table on the screen: trades with nobody on them. Headed like a
   section and not like a second page, because it is the same list -- things
   owed before a bid date -- of a different kind. Same edge treatment as an
   unopened bid, in the danger colour: a trade nobody was invited for is the
   one row here that will not fix itself. */
.am-bidwork-subhead {
  display: flex;
  align-items: center;
  gap: .4rem;
  margin: 1.5rem 0 .5rem;
  font-size: 1.05rem;
}

.am-bidwork-row--gap td:first-child {
  box-shadow: inset 3px 0 0 0 var(--am-danger-ink);
}

/* ============================================================
 * The records strip
 *
 * Three links under the stat tiles: bounce log, bid mailbox, audit trail.
 * They are links rather than tiles because the tiles above are all counts of
 * jobs needing a visit, and these three have no such number -- see the comment
 * in page-calendar-dashboard.php. Putting them in the tile row would have read
 * as three broken tiles.
 *
 * Quiet on purpose. This is a way back to a record somebody came looking for,
 * not a call to action competing with the figures above it.
 * ========================================================== */
.americon-records {
  display: flex;
  flex-wrap: wrap;
  gap: var(--am-gap-sm);
  align-items: center;
  margin: 0 0 var(--am-gap);
}

.americon-records__link {
  display: inline-flex;
  align-items: center;
  padding: 0.35rem 0.75rem;
  border: 1px solid var(--am-border);
  border-radius: var(--am-radius-pill);
  background: var(--am-surface);
  color: var(--am-text-muted);
  font-size: var(--am-text-sm);
  line-height: 1.2;
  text-decoration: none;
  transition: color var(--am-ease), border-color var(--am-ease);
}

.americon-records__link:hover,
.americon-records__link:focus-visible {
  color: var(--am-text);
  border-color: var(--am-border-strong);
  text-decoration: none;
}

/* The three staff records share the panel chrome; these are only the bits the
   shared classes do not already cover. */
.am-bounce-screen .am-bounce-status,
.am-bounce-screen .am-bounce-subj,
.am-bounce-screen .am-bounce-noaddr {
  display: block;
  color: var(--am-text-muted);
  font-size: var(--am-text-xs);
}

.am-bounce-screen .am-bounce-addr,
.am-audit-screen .am-audit-who {
  word-break: break-word;
}

.am-bounce-screen .am-bounce-err,
.am-bidbox-screen .am-bidbox-err {
  color: var(--am-danger-ink);
}

.am-bounce-screen .am-bounce-when,
.am-bidbox-screen .am-bidbox-when,
.am-audit-screen .am-audit-count {
  color: var(--am-text-muted);
  font-size: var(--am-text-sm);
}

/* The refresh control sits on the heading's own line rather than in a band of
   its own -- the same reasoning as the calendar's view switch. */
.am-bounce-screen .am-bounce-refresh,
.am-bidbox-screen .am-bidbox-refresh {
  display: flex;
  align-items: center;
  gap: var(--am-gap-sm);
  flex-wrap: wrap;
}

.am-bounce-screen .am-bounce-add,
.am-audit-screen .am-audit-filters {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--am-gap-sm);
  margin: 0 0 var(--am-gap);
}

.am-bounce-screen .am-inline {
  display: flex;
  flex-wrap: wrap;
  gap: var(--am-gap-sm);
  align-items: center;
}

.am-bounce-screen .am-bounce-foot,
.am-bidbox-screen .am-bidbox-foot,
.am-audit-screen .am-audit-foot {
  margin: var(--am-gap) 0 0;
  color: var(--am-text-muted);
  font-size: var(--am-text-sm);
}

/* The invitation record's rows open the firm they name, the same way the
   worklist's do. Scoped to .am-rowlink-ready so a row only looks clickable
   where americon-rowlink.js actually ran, and applied only to rows that got
   the attribute -- a firm with nothing recorded behind it is not a link and
   must not offer the cursor of one. */
.am-rowlink-ready .am-invite-row--go {
  cursor: pointer;
}
