/* ==========================================================================
   DZT OFFICE — the application.

   Owner, 2026-08-09, four times of asking: "everything still looks like crap
   amateur", "LOOKS PATHETIC AMATUER". The previous three attempts moved
   spacing, split a card into four tasks and put a sidebar on it, and every one
   of them left the same three things in place. Those three things are what
   "amateur" was pointing at, and this file changes all of them:

   1. IT WAS A PAGE, IN A COLUMN, ON A DESK MONITOR. `#office` was capped at
      78rem and sat inside the body padding app.css writes for a phone, so a
      27-inch screen showed a 1250px strip of cards floating in slate. An
      application does not have a page width. It has a rail, a header, and a
      content pane that scrolls under them.

   2. IT WORE THE MARKETING TYPE SCALE. `--step-0` is `clamp(1rem … 1.1rem)`
      and `--step-1` reaches 1.46rem — a scale built for a headline read from a
      sofa. At those sizes a table row is 44px tall and eleven of them fill a
      viewport. The palette is the owner's ruling and is untouched; the SIZES
      are not the ruling, and a desk tool needs its own, which is what
      `--o-t-*` below is: a fixed 11/13/15/20/32 ramp, no clamping, tuned for a
      mouse at 60cm rather than a thumb at arm's length.

   3. EVERY SURFACE WAS THE SAME SURFACE. The palette gives the dark side two
      values, `--slate-deep` and `--slate`, and app.css derives one more. Three
      greys cannot express ground / rail / card / inset / well, so everything
      was the same card with the same 1px border and nothing had any weight.
      The `--o-*` ramp below is SIX steps on the brand's own slate-to-ink axis
      — no new hue, nothing borrowed from outside the token file — and that is
      what lets a card sit ON something instead of beside it.

   WHAT IS NOT NEGOTIABLE, AND IS UNCHANGED: the hues (`--sage`, `--forest*`,
   `--slate*`, `--ink`, the `#C4703A` warn family), the two faces, and the rule
   that a number on this screen comes from a row somebody can open.

   THE FIELD APP IS DELIBERATELY UNTOUCHED. Its 56px targets are correct for a
   phone in a glove in July sun. This is a desk with a mouse.

   ---------------------------------------------------------------------------
   MATTE BLACK, NOT GLASS. Owner, 2026-08-09: "keep it minimalistic but
   futuristic pro like Elon Musk's X app for admin experience/office dash.
   glass looks cheap/half assed" — and then, on seeing it: "too glossy".

   The first attempt ported the marketing site's frosted header onto every
   surface: blurred translucent panels, a five-layer ring, a 102 degree
   specular streak, and an ambient radial field behind them for the blur to
   refract. That is the right treatment for a MARKETING header, which is one
   floating element over a photograph. Applied to thirty surfaces at once it is
   thirty competing highlights, and the verdict was correct — a diagonal shine
   on a data panel is decoration pretending to be depth.

   What replaced it is the opposite discipline, and it is X's:

     · TRUE BLACK, #000000. Not a dark grey pretending to be black. On an OLED
       panel the ground is genuinely off, so a hairline reads as a hairline.
     · NO ELEVATION AT ALL. No shadow, no blur, no gradient, no inner
       highlight. A panel is the ground plus a one-pixel border. Hierarchy
       comes from type weight, spacing and position — the things that survive
       being screenshotted, printed, or looked at for eight hours.
     · ONE ACCENT, USED WHEN IT MEANS SOMETHING. The brand sage marks the
       current section and a good state; the warn amber marks the one number
       that is costing money. Everywhere else is a three-step grey.
     · HAIRLINES DO THE DIVIDING. #2F3336 at one pixel, and a 1px grid gap
       over a line-coloured ground where a run of cells needs separating.

   THE PALETTE IS STILL THE RULING: --sage, --forest-soft and the #C4703A warn
   family are unchanged. What went is the MATERIAL, not the colour.

   THE SIGN-IN SCREEN IS UNTOUCHED, by owner instruction. Everything here is
   scoped to the three signed-in roots, and app.css's #signin block is not read
   by this file.
   ========================================================================== */

/* ==========================================================================
   1. THE PALETTE AND SCALE

   Owner, 2026-08-09: "make the entire /admin logged-in experience dark black
   glass". THREE ROOTS, NOT ONE — and finding that out is what stopped this
   being a regression. `owner.html` and `portal.html` both `<link>` this
   stylesheet and build themselves out of its classes: .ofc-head, .ofc-lede,
   .ofc-tally, .ofc-card--wide and .ofc-list are all theirs as much as they
   were ever the office's. Rewriting this file around `#office` alone deleted
   nine classes those two pages depend on and scoped the new buttons where
   neither could reach them. Both pages would have rendered with an unstyled
   header, no lede band and 56px field-app buttons.

   So the scope is the three signed-in roots. `#signin` is deliberately NOT
   among them — the owner asked for the sign-in screen to stay exactly as it
   is, and it is styled entirely by app.css, which this file never touches.
   ========================================================================== */

#office, #owner, #portal {
  /* ---- surfaces ----
     THE GROUND IS BLACK AND THE THINGS ON IT ARE NOT. This used to read "there
     is one surface and it is black", with `--o-panel: #000000` — a panel was
     the ground plus a border. On an all-black page that makes every card,
     column and stat band invisible except for its 1px edge, so the screen reads
     as a wireframe that was given colours rather than as a set of objects. The
     owner's note was about the GROUND, and the ground is untouched: true
     #000000, the pixel genuinely off on an OLED panel, and design-check pins it
     there.

     What changed is that a panel now has a surface of its own. Four flat steps,
     no elevation of any kind — this system bans shadow, blur and gradient on
     every signed-in surface because "glass looks cheap" and "too glossy" were
     rulings, twice. Depth here is a fill, not a lighting effect.

     EVERY INK CLEARS AA ON EVERY ONE OF THESE, and that is measured rather than
     asserted: `tools/ai/contrast-check.mjs` computes all twenty pairs on each
     run. Lifting the surfaces cost the faint step, which used to be #71767B and
     sat at 4.25:1 on the old inset — under AA on the very surface it was most
     used on, while the comment beside it claimed "even the faintest clears AA
     for body text". It does now, on all four. */
  /* MATCHED TO THE MARK, 2026-09-08. This palette was the site's PRE-STEEL one:
     a mint accent the badge does not contain (a light spring green), a warm
     bone ink, and
     12px radii. Three things were making it read as a generic dashboard rather
     than as DZT's, and none of them was the layout:
       THE ACCENT WAS A HUE THE BRAND DOESN'T HAVE. Steel marks, green fills —
       the same rule the site runs: you press green, you read steel.
       THE RULES WERE INVISIBLE. --o-line at #1B1F1B is 1.1:1 on the panel. A
       dense UI is read by its rules; if they don't resolve, every table looks
       like a list of floating text. #232A30 is 1.5:1 — still quiet, actually
       there.
       THE RADII WERE BUBBLY. 12px on every card is the house style of every
       template dashboard on earth. The site is square. 4px is a machined edge,
       not a pill.
   */
  --o-bg:     #101418;   /* the ground, and only the ground */
  --o-panel:  #161A1D;   /* a card, a stat band, a board column */
  --o-inset:  #1D2226;   /* a well, a chip, a nested block */
  --o-raised: #262D33;   /* a card ON a panel — a board tile, a list row */
  --o-hover:  rgba(255, 255, 255, 0.05);
  --o-press:  rgba(255, 255, 255, 0.09);
  /* The scrim behind a dialog. A TOKEN rather than a literal in the rule: it
     was written as `rgba(11, 13, 14, 0.62)` — the old `--o-inset` typed out by
     hand — so the moment that step moved, the backdrop silently kept the
     retired colour. design-check caught it, which is the whole reason it
     refuses a hue that resolves to nothing. */
  --o-scrim:  rgba(8, 10, 12, 0.74);

  /* ---- lines. Solid hex, not alpha: over a near-black ground an alpha line
     is a different colour on every surface it crosses, and the whole point of
     a hairline system is that one weight looks like one weight. ---- */
  --o-line:    #232A30;   /* a divider inside a panel */
  --o-line-2:  #333C44;   /* the edge of a panel */

  /* ---- ink. Three steps, and the claim is about the WHOLE LADDER rather than
     about the ground alone: on the ground they measure 16.1:1, 7.4:1 and
     6.2:1, and on the deepest surface any of them lands on they still measure
     11.9:1, 5.5:1 and 4.6:1. So the faintest clears AA for body text wherever
     it is actually used. Three steps and no fourth: a fourth is how a design
     starts meaning nothing. ---- */
  --o-text:    #FFFFFF;
  /* THE LINK BLUE, AND WHY IT IS NOT THE WEB'S CLASSIC ONE. A link inside a
     customer's email has to announce itself as a link before it is read, and
     the steel this used to use never did — it is the same family as the body
     text and only the underline distinguished it. Blue is the one colour a
     reader already knows means "this goes somewhere".
     #1A0DAB, the browser default, measures 1.49:1 on this ground and #0066CC
     measures 3.32:1 — both unreadable on a dark office. This is 8.61:1 on the
     ground and 7.47:1 in an inset well, AAA on both, and still plainly blue. */
  --o-link:    #6CB6FF;
  --o-link-hi: #A8D6FF;
  --o-dim:     #A8B2B8;
  --o-faint:   #8A949B;

  /* ---- accent: the brand green ---- */
  --o-accent:      #9EA9B3;   /* STEEL — the ring. 7.5:1 on the ground; every mark and link */
  --o-accent-deep: #33703C;   /* THE STAND'S GREEN — a fill, with a white label at 6.0:1 */
  --o-accent-hi:   #26552E;   /* that fill, hovered — a step DOWN the stand; up fails AA */
  --o-accent-wash: rgba(158, 169, 179, 0.11);

  /* ---- status. Reserved meanings, never used as a series colour. ---- */
  --o-warn:     #B4682F;   /* the marketing site's own warn — a MARK colour */
  --o-warn-ink: #E8A87C;   /* the same family, for TEXT only — 10.3:1, see §7 */
  --o-warn-bg:  #1A1008;   /* a warn ground on black is nearly black */
  --o-neutral:  #6E7A82;   /* "not applicable", cancelled, unknown */

  /* ---- type. Fixed, not clamped: see the header. ---- */
  --o-t-xs:  0.6875rem;   /* 11px — micro labels, chips, badges */
  --o-t-sm:  0.8125rem;   /* 13px — tables, lists, form text */
  --o-t-md:  0.875rem;    /* 14px — body, card titles */
  --o-t-lg:  1rem;        /* 16px — section headings */
  --o-t-xl:  1.25rem;     /* 20px — a pane title, a detail heading */
  /* 28px — THE ONE H1 PER VIEW. It shared the 20px step with pane headings and
     detail titles, so the page had no confident entry point: "Dashboard" was
     set at the same size as a card heading three inches below it, and the eye
     had nowhere to land first. A view title is not a heading among headings. */
  --o-t-title: 1.75rem;
  --o-t-num: 2rem;        /* 32px — a stat value */
  --o-t-hero: 3.25rem;    /* 52px — the one hero figure per view */
  --o-num: 'Archivo', 'Helvetica Neue', Arial, sans-serif;

  /* ---- geometry. 16px panels and fully-round controls: the two radii that
     read as an application rather than as a web page. ---- */
  --o-r:     4px;
  --o-r-sm:  3px;
  --o-r-lg:  6px;
  --o-pill:  999px;
  --o-rail-w: 15.5rem;
  --o-pad:   1.25rem;

  /* ---- THE OLD NAMES, POINTED AT THE NEW VALUES ----
     owner.css and portal.css are written against `--app-*` and `--step-*`, and
     they are not wrong — they were written against the palette that existed.
     Re-pointing the names inside these three roots brings both files onto the
     new ground and the new density without editing either, and without any
     risk of the two drifting: there is one definition, here.

     The alternative was find-and-replace across 236 lines of two stylesheets,
     which is how a colour gets missed and one card keeps the old slate. */
  --app-bg:      var(--o-bg);
  --app-surface: var(--o-panel);
  --app-raised:  var(--o-inset);
  --app-text:    var(--o-text);
  --app-mute:    var(--o-dim);
  --app-line:    var(--o-line);
  --app-accent:  var(--o-accent);

  /* The marketing type scale is built for a headline read from a sofa. Same
     reasoning as --o-t-* above, applied to the two pages that ask for the
     steps by their old names. */
  --step--1: 0.8125rem;
  --step-0:  0.875rem;
  --step-1:  1rem;
  --step-2:  1.25rem;
  --step-3:  2rem;
  --step-4:  3.25rem;
}

/* ==========================================================================
   2. THE FRAME

   Full viewport, three panes, and the CONTENT is the only thing that scrolls.
   A dashboard whose rail scrolls away is a dashboard where navigation is a
   scroll-to-top away, which is a menu bar with extra steps.
   ========================================================================== */

/* app.css writes phone padding onto <body> and it is wrong here. Same :has()
   mechanism the sign-in split already uses, so the field app is unaffected. */
body:has(#office), body:has(#owner), body:has(#portal) {
  padding: 0;
  /* THE BRAND PLATE, NOT TRUE BLACK — owner, 2026-09-05: match the portal to
     the site. #0B0E0C is the same near-black the mark sits on and the same one
     the marketing site paints, so the two origins read as one company at the
     moment a customer is handed a login. It is 1.16:1 off the hairline, which
     still draws as a line rather than a smudge.

     This reverses the 2026-08-09 ruling ("true black, the pixel genuinely off
     on an OLED panel"), and the reversal is the owner's. design-check pinned
     #000000 and has been updated to pin this instead, rather than deleted —
     the point of that check is that the ground is a decision, not a drift. */
  background: #0B0E0C;
  /* THE BASE IS NOT SET HERE, AND IT LOOKED AS THOUGH IT WERE.
     `font-size: var(--o-t-md)` sat on this rule from `2b5dcd8` (2026-08-09)
     until 2026-08-15 and was dead every one of those six days — the date came
     from `git log -S`, not from an impression. The `--o-*` tokens are declared
     on `#office, #owner, #portal`, and custom properties inherit DOWNWARDS, so
     the parent of those roots cannot see a token its own child defines. The
     declaration resolved to nothing and was dropped, exactly as `--o-t-s` was
     in `.ofc-board__money`.
     Everything else in this rule worked, which is what made it invisible —
     `line-height: 1.45` computed against the browser's 16px and the background
     painted, so the rule plainly matched and only the one declaration died.
     The base now lives on the roots below, where the token is in scope.

     AND REMOVING IT MOVED `body` ITSELF, which is worth stating because it is
     not obvious and was measured rather than predicted: 16px → 17.6px. A
     `var()` naming an undefined property is invalid at computed-value time,
     and for an inherited property that means the declaration computes to
     `inherit` — it does NOT fall through to the next rule in the cascade. So
     the dead line was still MASKING `app.css`'s `body { font-size:
     var(--step-0) }` and pinning body to the browser default. With it gone,
     app.css applies, which is what every other page in this app already gets.
     Nothing visible rides on it: these three pages carry only their root
     `<main>`, a `<noscript>` and two `<script>`s, nothing is ever appended to
     `body`, and `rem` is relative to `html` regardless — so the whole
     observable difference is the size of "This app needs JavaScript." on a
     page with JavaScript turned off. */
  line-height: 1.45;
}

#office, #owner, #portal {
  /* 14px, not --step-0's clamp. Every size in this file is relative to it —
     true here, and not true where this line used to be. Moving it is a no-op
     on today's markup and was measured rather than assumed: 447 visible text
     elements across the six office views, and the static chrome of the owner
     and portal pages, all resolve their own size from the rem ramp, so 0 of
     them changed. It matters for the NEXT element that does not set a size —
     which is what `.ofc-board__money` turned out to be. `app-tokens --check`
     now fails any `html`/`body` rule that references an `--o-*` token, so this
     particular hole cannot be dug again. */
  font-size: var(--o-t-md);
}

#office {
  display: grid;
  grid-template-columns: var(--o-rail-w) minmax(0, 1fr);
  /* dvh, so the rail is not cut off by a mobile browser's own chrome. */
  height: 100dvh;
  max-width: none;
  margin: 0;
  padding: 0;
  color: var(--o-text);
  overflow: hidden;
}

/* THE OTHER TWO ARE PAGES. They have no rail and no section router — the
   owner screen is one long read and the portal is one customer's project — so
   they get a measure and the document scrolls, which is what they had before
   body.padding went to zero for the office's sake. */
#owner, #portal {
  max-width: 78rem;
  margin: 0 auto;
  padding: 1.25rem clamp(1rem, 3vw, 2rem) 4rem;
  color: var(--o-text);
}

/* The content pane. `minmax(0, 1fr)` above and `min-width: 0` here are both
   load-bearing: without them a wide table inside a grid child refuses to
   shrink and pushes the whole layout sideways. */
.ofc-main {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  min-width: 0;
  overflow: hidden;
}

.ofc-scroll {
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 1.15rem clamp(1rem, 2.2vw, 1.9rem) 3rem;
  scrollbar-color: var(--o-line-2) transparent;
}

/* ---- the rail ---------------------------------------------------------- */

/* One hairline is the whole treatment. A rail that needs a shadow to separate
   itself from the content is a rail whose content is not black. */
.ofc-rail {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  min-height: 0;
  background: var(--o-bg);
  border-right: 1px solid var(--o-line-2);
}

.ofc-rail__brand {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.85rem 0.9rem;
  border-bottom: 1px solid var(--o-line);
  color: var(--o-text);
  text-decoration: none;
}

.ofc-logo {
  width: 30px;
  height: 30px;
  flex: none;
  border-radius: 8px;
  padding: 3px;
  background: rgba(0, 0, 0, 0.32);
  border: 1px solid var(--o-line-2);
  object-fit: contain;
}

.ofc-rail__name {
  display: grid;
  gap: 0.05rem;
  min-width: 0;
}
.ofc-rail__name b {
  font-family: var(--display);
  font-size: var(--o-t-md);
  font-weight: 750;
  letter-spacing: -0.01em;
  line-height: 1.1;
}
.ofc-rail__name span {
  font-size: var(--o-t-xs);
  font-weight: 650;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--o-faint);
}

.ofc-rail__nav {
  min-height: 0;
  overflow-y: auto;
  padding: 0.6rem 0.55rem 1rem;
}

.ofc-side__g {
  margin: 0.9rem 0 0.3rem;
  padding: 0 0.6rem;
  font-size: var(--o-t-xs);
  font-weight: 750;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--o-faint);
}
.ofc-side__g:first-child { margin-top: 0.15rem; }

/* A HAIRLINE, NOT A SECOND COLOUR. The break between what is finished and what
   is not has to be visible at a glance, and the palette has one accent that is
   spent on things that cost money — an "unfinished" tint would be a second one
   competing with it. The rule does the separating; the label does the saying. */
.ofc-side__g--soon {
  margin-top: 1.15rem;
  padding-top: 0.85rem;
  border-top: 1px solid var(--o-line);
}

.ofc-side__a {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  padding: 0.5rem 0.8rem;
  border-radius: var(--o-pill);
  color: var(--o-dim);
  text-decoration: none;
  font-size: var(--o-t-sm);
  font-weight: 600;
  /* The highlight is drawn with an inset shadow rather than a border so the
     row does not change size when it becomes current — a menu whose items
     jump 2px on selection is a menu that feels broken. */
  transition: background-color .12s ease, color .12s ease;
}
.ofc-side__a:hover { background: var(--o-hover); color: var(--o-text); }

/* The current section is a filled pill, not a tinted strip with a coloured
   edge. One shape change reads at a glance and survives being colour-blind —
   it also carries aria-current, which is what a screen reader gets. */
.ofc-side__a.is-on {
  background: var(--o-press);
  color: var(--o-text);
  font-weight: 700;
}
/* Stated, not implied by colour — it also carries aria-current. A third of
   people cannot rely on the accent, and a screen reader gets nothing at all
   from a box-shadow. */
.ofc-side__a.is-on .ofc-i { color: var(--o-accent); }

/* The sprite itself is a definitions block, not a picture. Taken out of flow
   rather than `display: none`, because a `<use>` referencing a symbol inside a
   display:none tree still resolves but some engines skip painting it. */
.ofc-sprite { position: absolute; width: 0; height: 0; overflow: hidden; }

.ofc-i {
  width: 15px;
  height: 15px;
  flex: none;
  color: var(--o-faint);
  stroke: currentColor;
  fill: none;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.ofc-side__a:hover .ofc-i { color: var(--o-dim); }

.ofc-side__t { flex: 1 1 auto; min-width: 0; }

/* The count beside a section. Only ever the number that COSTS something —
   see the badge comments in page-office.mjs. */
.ofc-side__n {
  min-width: 1.35rem;
  padding: 0.05rem 0.34rem;
  border-radius: 999px;
  background: var(--o-inset);
  color: var(--o-dim);
  font-size: var(--o-t-xs);
  font-weight: 750;
  text-align: center;
  font-variant-numeric: tabular-nums;
}
.ofc-side__n:empty { display: none; }
/* `--o-warn-bg`, not a second near-black: #17100B differed from the token by a
   luminance of 0.0003 — invisible, and a separate value to keep in step. */
.ofc-side__n.is-hot { background: var(--o-warn); color: var(--o-warn-bg); }

.ofc-rail__foot {
  padding: 0.75rem 0.8rem;
  border-top: 1px solid var(--o-line-2);
  display: grid;
  gap: 0.6rem;
}
/* One row, evenly weighted, no wrap: three controls on two ragged lines is
   what a footer looks like when nobody laid it out. */
.ofc-rail__foot .ofc-actions {
  gap: 0.15rem;
  margin-inline: -0.55rem;
}
.ofc-rail__foot .ofc-actions > * { flex: 0 1 auto; }

.ofc-me { display: flex; align-items: center; gap: 0.55rem; min-width: 0; }

/* Initials, not a photograph nobody has uploaded. */
.ofc-me__mark {
  width: 28px;
  height: 28px;
  flex: none;
  display: grid;
  place-items: center;
  border-radius: 999px;
  background: var(--o-accent-wash);
  border: 1px solid rgba(154, 203, 178, 0.24);
  color: var(--o-accent);
  font-family: var(--display);
  font-size: var(--o-t-xs);
  font-weight: 750;
  letter-spacing: 0.02em;
}

.ofc-me__id { display: grid; min-width: 0; gap: 0.05rem; }
.ofc-me__id b {
  font-size: var(--o-t-sm);
  font-weight: 650;
  /* An email is longer than a 15rem rail and must not wrap it wider. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ofc-me__id span {
  font-size: var(--o-t-xs);
  font-weight: 650;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--o-faint);
}

/* ---- the header -------------------------------------------------------- */

.ofc-top {
  position: relative;
  z-index: 2;
  display: flex;
  align-items: center;
  gap: 0.9rem;
  padding: 0.7rem clamp(1rem, 2.2vw, 1.9rem);
  background: var(--o-bg);
  border-bottom: 1px solid var(--o-line-2);
}

.ofc-top__where { min-width: 0; flex: 1 1 auto; }
.ofc-top__where h1 {
  margin: 0;
  font-family: var(--display);
  font-size: var(--o-t-xl);
  font-weight: 750;
  letter-spacing: -0.018em;
  line-height: 1.15;
}
.ofc-top__where p {
  margin: 0.1rem 0 0;
  font-size: var(--o-t-sm);
  font-weight: 500;
  color: var(--o-faint);
}

.ofc-top__acts { display: flex; align-items: center; gap: 0.5rem; flex: none; }

/* ⌘K. A section list you aim at with a mouse is a menu; one you type at is a
   tool. */
.ofc-jump {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  min-height: 32px;
  padding: 0 0.6rem 0 0.85rem;
  border: 1px solid var(--o-line-2);
  border-radius: var(--o-pill);
  background: transparent;
  background-image: none;
  box-shadow: none;
  color: var(--o-faint);
  font: 600 var(--o-t-sm)/1 var(--body);
  cursor: pointer;
}
.ofc-jump:hover { color: var(--o-text); border-color: var(--o-line-2); background: var(--o-hover); }
.ofc-jump:active { transform: none; }
.ofc-jump kbd {
  padding: 0.12rem 0.32rem;
  border: 1px solid var(--o-line-2);
  border-radius: 5px;
  background: var(--o-inset);
  font: 650 var(--o-t-xs)/1 var(--body);
  color: var(--o-faint);
}

/* ==========================================================================
   3. CONTROLS

   app.css gives every <button> a 56px filled green treatment, which is right
   for a thumb in a glove and wrong for a toolbar. Everything below is scoped
   to #office so the field app keeps its targets.
   ========================================================================== */

:is(#office, #owner, #portal) .ofc-primary,
:is(#office, #owner, #portal) .ofc-ghost,
:is(#office, #owner, #portal) .ofc-quiet {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  min-height: 34px;
  padding: 0 1rem;
  border-radius: var(--o-pill);
  font: 650 var(--o-t-sm)/1 var(--body);
  letter-spacing: 0.005em;
  cursor: pointer;
  /* These are class-shaped, not element-shaped, and the mailbox's Reply and
     Forward are <a> because they hand off to a mail client rather than doing
     something here. Without this they arrive underlined: button geometry
     wearing link decoration, which reads as neither. */
  text-decoration: none;
  transition: background-color .12s ease, border-color .12s ease, color .12s ease;
}
:is(#office, #owner, #portal) .ofc-primary:active,
:is(#office, #owner, #portal) .ofc-ghost:active,
:is(#office, #owner, #portal) .ofc-quiet:active { transform: none; }

/* Filled, in the brand green, with the site's own gradient — the one flourish
   worth carrying, because it is what makes a DZT button look like one. */
:is(#office, #owner, #portal) .ofc-primary {
  justify-self: start;
  border: 1px solid var(--o-accent-deep);
  background: var(--o-accent-deep);
  /* FLAT. The site's button gradient is right on a photograph and wrong here:
     it is the same specular idea as the glass, one control at a time. */
  background-image: none;
  box-shadow: none;
  /* THE GROUND, NOT WHITE — AND THE PALETTE CHANGE IS WHY.
     This was #FFFFFF and measured 5.34:1 on the old forest #0E7A54, which was
     dark enough to carry a white label. The cedar green is a lighter colour:
     white on it is 3.33:1 and --o-text is 2.75:1, so BOTH of the inks that
     looked obvious here now fail, on the one control every form ends with.
     The plate is 5.83:1 on it. Same relationship the marketing site's buttons
     use: on this green the colour is the field and the label is the hole
     punched in it. */
  color: var(--o-bg);
  font-weight: 700;
}
/* HOVER IS BRIGHTER NOW, AND THAT IS A CONSEQUENCE OF THE LABEL FLIPPING.
   Under the old palette the label was white, so hover had to go DARKER to keep
   the label readable — going lighter dropped it to 4.14:1. With a plate label
   the relationship inverts: brighter green means MORE contrast, not less. It
   goes 5.83:1 -> 7.24:1 on hover, so the control is at its most legible at the
   moment somebody is about to click it, which is the right way round. */
:is(#office, #owner, #portal) .ofc-primary:hover:not(:disabled) {
  background-color: var(--o-accent-hi);
  border-color: var(--o-accent-hi);
}
:is(#office, #owner, #portal) .ofc-primary:disabled {
  background: var(--o-inset);
  background-image: none;
  border-color: var(--o-line);
  box-shadow: none;
  color: var(--o-faint);
  cursor: default;
}

:is(#office, #owner, #portal) .ofc-ghost {
  border: 1px solid var(--o-line-2);
  background: transparent;
  background-image: none;
  box-shadow: none;
  color: var(--o-dim);
}
:is(#office, #owner, #portal) .ofc-ghost:hover:not(:disabled) { background: var(--o-hover); color: var(--o-text); }
:is(#office, #owner, #portal) .ofc-ghost:disabled { opacity: 0.5; cursor: default; }

:is(#office, #owner, #portal) .ofc-quiet {
  min-height: 28px;
  padding: 0 0.7rem;
  border: 1px solid transparent;
  background: transparent;
  background-image: none;
  box-shadow: none;
  color: var(--o-dim);
  font-size: var(--o-t-xs);
  font-weight: 650;
}
:is(#office, #owner, #portal) .ofc-quiet:hover:not(:disabled) { background: var(--o-hover); color: var(--o-text); }
:is(#office, #owner, #portal) .ofc-quiet:disabled { opacity: 0.5; cursor: default; }

:is(#office, #owner, #portal) .ofc-link {
  display: inline-flex;
  align-items: center;
  min-height: 28px;
  padding: 0 0.55rem;
  border-radius: var(--o-pill);
  color: var(--o-accent);
  font-size: var(--o-t-sm);
  font-weight: 650;
  text-decoration: none;
}
:is(#office, #owner, #portal) .ofc-link:hover { background: var(--o-hover); }

:where(#office, #owner, #portal) :focus-visible {
  outline: 2px solid var(--o-accent);
  outline-offset: 2px;
  border-radius: 4px;
}

/* ==========================================================================
   4. CARDS AND GRID
   ========================================================================== */

.ofc-grid {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: 0.9rem;
  align-items: start;
}
/* One column until there is room for real columns. */
@media (max-width: 62rem) {
  .ofc-grid { grid-template-columns: minmax(0, 1fr); }
  .ofc-grid > * { grid-column: 1 / -1 !important; }
}

/* A PANEL IS THE GROUND PLUS ONE HAIRLINE. No fill, no shadow, no gradient —
   the panel is not a different material from the page, it is a boundary drawn
   on it. That is the whole difference between this and the version before it. */
.ofc-card {
  grid-column: span 12;
  min-width: 0;
  padding: var(--o-pad);
  border: 1px solid var(--o-line-2);
  border-radius: var(--o-r-lg);
  background: var(--o-panel);
}
.ofc-card--half { grid-column: span 6; }

/* The "show more" row under a capped list. Centred and quiet: it is a way out
   of the cap, not a call to action. */
.ofc-more { margin: 0.7rem 0 0; display: flex; justify-content: center; }

/* The files a message carried. A list, because that is what it is — and the
   type and size sit under the name the way `.ofc-tot dd small` does. */
.ofc-attach { list-style: none; margin: 0.5rem 0 0; padding: 0; display: grid; gap: 0.35rem; }
.ofc-attach li { padding: 0; border: 0; background: none; }
.ofc-attach b { display: block; font-size: var(--o-t-sm); color: var(--o-text); }
.ofc-attach small { display: block; margin-top: 0.1rem; font-size: var(--o-t-xs); color: var(--o-faint); }
.ofc-more[hidden] { display: none; }

/* ---- MOTION, AND ONLY WHERE SOMETHING IS ACTUALLY INTERACTIVE ------------
   The design ruling this file is built around banned gloss, blur and
   elevation — "glass looks cheap", "too glossy", twice. It did not ban
   MOVEMENT, and the absence of any was part of why the screen read as a
   drawing of an application rather than one: a row you can open and a row you
   cannot looked and behaved identically until you clicked.

   120ms, colour and border only. Nothing moves position, nothing scales:
   a list that shifts under the cursor is harder to hit, and this screen is
   used with a mouse all day. And it is all inside prefers-reduced-motion. */
@media (prefers-reduced-motion: no-preference) {
  .ofc-board__card,
  .ofc-split__item,
  .ofc-feed__row,
  .ofc-addr__b,
  .ofc-ph-filter__b,
  .ofc-side__a {
    transition: background-color .12s ease, border-color .12s ease, color .12s ease;
  }
}

/* A tile you can open says so on hover. The board card was the only control on
   the CRM that gave no feedback at all — it is a <button>, and it read as a
   sheet of paper. */
.ofc-board__card:hover { border-color: var(--o-line-2); background: var(--o-raised); }
.ofc-board__card:hover .ofc-board__who { color: var(--o-text); }
.ofc-card--third { grid-column: span 4; }
.ofc-card--twothirds { grid-column: span 8; }
/* The honest half of the screen: a note ABOUT the screen rather than a panel
   of it, so it gets the dashed edge and no fill. */
.ofc-card--muted { border-style: dashed; }

.ofc-card__bar {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  margin-bottom: 0.85rem;
}
.ofc-card__bar > div { min-width: 0; }

.ofc-card h2 {
  margin: 0;
  font-family: var(--display);
  font-size: var(--o-t-lg);
  font-weight: 700;
  letter-spacing: -0.012em;
  line-height: 1.2;
}

.ofc-sub {
  margin: 0.25rem 0 0.9rem;
  max-width: 68ch;
  color: var(--o-faint);
  font-size: var(--o-t-sm);
  font-weight: 500;
  line-height: 1.5;
}
.ofc-card__bar .ofc-sub { margin-bottom: 0; }

/* A refusal is a sentence, not an empty box: "your role cannot read leads" and
   "no leads yet" are opposite facts and the screen says which. */
.ofc-state {
  margin: 0 0 0.75rem;
  color: var(--o-dim);
  font-size: var(--o-t-sm);
  font-weight: 600;
}
.ofc-state:empty { display: none; }

.ofc-note {
  /* BLOCK, ALWAYS. A note carries a left border and vertical margin, which an
     inline box cannot lay out — as a <span> in a table cell three of them drew
     on top of one another, the permit flags column overlapping itself into an
     unreadable stack. It renders as a <p> in the markup and as a <span> from
     the renderers, and it has to behave the same either way. */
  display: block;
  margin: 0.85rem 0 0;
  padding: 0 0 0 0.65rem;
  border-left: 2px solid var(--o-line-2);
  color: var(--o-dim);
  font-size: var(--o-t-sm);
  font-weight: 500;
}
/* Stacked in a cell, the first needs no lead-in and the rest need less. */
.ofc-lines td > .ofc-note { margin-top: 0.35rem; }
.ofc-lines td > .ofc-note:first-child { margin-top: 0; }
/* A note that is a PROBLEM has to read differently from one that is an
   observation. It was toggled by four renderers and styled by none. */
.ofc-note.is-warn {
  padding: 0.55rem 0.75rem;
  border-radius: var(--o-r-sm);
  background: var(--o-warn-bg);
  border-left: 2px solid var(--o-warn);
  /* `--o-warn-ink`, not a second cream. This was #F3D8C4 in two rules — a
     fourth warn colour with no token and no measured contrast, which drifts the
     moment one of the two is edited. The token is 9.20:1 on this ground. */
  color: var(--o-warn-ink);
  font-weight: 600;
}

/* ==========================================================================
   5. THE STAT ROW

   Four numbers, one of them the hero. Exactly one hero figure per view: the
   unanswered enquiry is the only thing on this screen still costing money
   while it is being read — everything else is history.
   ========================================================================== */

/* ONE PANEL, NOT FOUR. Four separate boxes with four gaps between them is four
   objects; one panel divided by hairlines is an instrument, and it is what the
   row actually is — four readings of the same morning.

   THE GAP IS THE DIVIDER. `gap: 1px` over a line-coloured ground draws a
   hairline between every pair of cells and keeps drawing them correctly when
   the row wraps, which is the case a set of `border-right` rules always gets
   wrong on the last cell of a line. */
.ofc-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13.5rem, 1fr));
  gap: 1px;
  margin-bottom: 0.9rem;
  background: var(--o-line-2);
  border: 1px solid var(--o-line-2);
  border-radius: var(--o-r-lg);
  overflow: hidden;
}

.ofc-stat {
  display: grid;
  align-content: start;
  gap: 0.3rem;
  padding: 1rem 1.15rem 1.1rem;
  /* the stat tiles ARE the band; ground made them holes in it */
  background: var(--o-panel);
}

.ofc-stat__k {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: var(--o-t-xs);
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--o-faint);
}

/* PROPORTIONAL figures, deliberately. tabular-nums gives every digit the width
   of a zero, which at 32px makes "121" read loose and badly spaced. Tabular is
   for COLUMNS of numbers that have to line up — see the tables below. */
.ofc-stat__n {
  margin: 0.1rem 0 0;
  font-family: var(--display);
  font-size: var(--o-t-num);
  font-weight: 750;
  line-height: 1;
  letter-spacing: -0.03em;
  color: var(--o-text);
}

.ofc-stat__s {
  margin: 0;
  font-size: var(--o-t-sm);
  font-weight: 500;
  color: var(--o-faint);
  line-height: 1.4;
}

/* THE HERO. Spans two cells. "4 people are waiting" and "nobody is waiting"
   are not the same news and must not look alike — but the difference is
   carried by the FIGURE and a dot beside its label, not by painting a quarter
   of the screen amber. A tinted panel is what a dashboard does when it does
   not trust its own typography. */
.ofc-stat--hero { grid-column: span 2; }
.ofc-stat--hero .ofc-stat__k::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--o-warn);
  flex: none;
}
.ofc-stat--hero .ofc-stat__n {
  font-size: var(--o-t-hero);
  color: var(--o-warn-ink);
}
.ofc-stat--hero .ofc-stat__k { color: var(--o-warn-ink); }

/* Clear is its own state and is SAID, not left as an absence: the dot turns
   green and the figure with it. */
.ofc-stat--hero.is-clear .ofc-stat__k::before { background: var(--o-accent); }
.ofc-stat--hero.is-clear .ofc-stat__n { color: var(--o-accent); }
.ofc-stat--hero.is-clear .ofc-stat__k { color: var(--o-faint); }

@media (max-width: 62rem) {
  .ofc-stat--hero { grid-column: span 1; }
  .ofc-stat--hero .ofc-stat__n { font-size: var(--o-t-num); }
}

/* ---- the dashboard's own two columns -------------------------------------
   A 12-column grid put Pipeline (8) beside Waiting on us (4) and then dropped
   the variance chart onto a second row — and because a grid row is as tall as
   its tallest cell, the short Pipeline card left a half-screen of dead space
   above the chart. Two real columns that each stack their own cards is the
   shape a dashboard actually wants: nothing waits for the column beside it. */
.ofc-dash { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0.9rem; }
.ofc-dash__main, .ofc-dash__side { display: grid; gap: 0.9rem; align-content: start; min-width: 0; }
@media (min-width: 62.0625rem) {
  .ofc-dash { grid-template-columns: minmax(0, 2.1fr) minmax(0, 1fr); align-items: start; }
}

/* ==========================================================================
   6. THE PIPELINE BAR

   This replaced a wall of tiles. Nine stages rendered as nine equal boxes is
   nine facts with no shape; a stacked bar is one fact — how work is
   distributed — and it is the same nine numbers.

   THE COLOUR CARRIES THE ORDER, because the stages ARE an order: sold →
   scheduled → in progress → punch → substantially complete → complete →
   closed. That makes it an ORDINAL scale, so it is one hue in monotone
   lightness steps rather than nine unrelated colours. The hue is the brand
   green (OKLCH ~163°, the hue of --forest-soft and --sage); the seven steps
   were generated at even lightness and validated: monotone L, every adjacent
   gap ≥ 0.06, light end 2.21:1 on the card, hue spread 9°.

   ON HOLD AND CANCELLED ARE NOT STAGES, they are states, so they wear the
   reserved status colours and sit outside the ramp. A held job is not "further
   along" than a scheduled one and must not be coloured as though it were.
   ========================================================================== */

/* RETUNED TO THE NEEDLE GREEN, 2026-09-08. The ramp was a mint family
   (#b0eed6 -> #007146) from the palette the site stopped using, so the one
   chart on the dashboard was the last mint left in the office. Rebuilt on the
   badge's own green at the same seven steps, and every property this section
   claims still holds — monotone lightness, adjacent gaps >= 0.06, the dark end
   >= 3:1 on the plate. Recomputed, not eyeballed; chart-check.mjs is the
   instrument, run it after any change here.

   THE RAMP RUNS BRIGHT-TO-DARK ALONG THE STAGES, and the first version had it
   backwards. Built light-end-last, it made `closed` the palest, widest band on
   a dark ground — so the eye landed on the nine finished jobs and slid past the
   nineteen live ones, which is the opposite of what an office opens this screen
   for. Seen in a screenshot, not reasoned about.

   Reversing costs nothing: the validated property is that the steps are one hue
   with monotone lightness and ≥ 0.06 between them, and a monotone ramp read
   backwards is still monotone.

   THE DARK END MOVED WITH THE GROUND, 2026-09-05. Lifting the page off true
   black to the brand plate took #006b45 from 3.00:1 to 2.95:1 — under the 3:1
   floor chart-check enforces for a chart step, by five hundredths, which is
   exactly the kind of regression nobody sees and a checker does. Raising the
   last step alone would have closed the gap to the one above it to 0.059 and
   broken the other rule, so the two darkest steps moved together: #008856 and
   #007652, which is 3.43:1 on the plate with every adjacent gap still ≥ 0.062
   and the hue spread at 10.2°. */
#office {
  --st-sold:      #E6F1E8;
  --st-scheduled: #C1DEC5;
  --st-progress:  #97CCA1;
  --st-punch:     #67BB78;
  --st-subst:     #44A55A;
  --st-complete:  #368C4D;
  --st-closed:    #2B7341;
  --st-hold:      var(--o-warn);
  --st-cancelled: var(--o-neutral);
}

.ofc-bar {
  display: flex;
  align-items: stretch;
  /* THE 2px GAP IS THE SEPARATOR, not a border. A stroke around each segment
     would add ink that is not data; the ground showing through does the same
     job and stays quiet. */
  gap: 2px;
  height: 14px;
  margin: 0.2rem 0 0.9rem;
  border-radius: 4px;
  overflow: hidden;
  background: var(--o-inset);
}
.ofc-bar__seg {
  /* Set by page-office.mjs as a flex-grow weight — the count itself, so the
     widths are the numbers and not a rounded percentage of them. */
  flex: var(--seg, 1) 1 0;
  min-width: 3px;
  background: var(--seg-c, var(--o-neutral));
}
/* THE BREAK BETWEEN THE RAMP AND THE STATES. `on_hold` and `cancelled` are not
   points on the journey, and drawing them as segments of an ordinal ramp put
   the reserved warn colour between two green steps — the lightness progression
   reversed in the middle of the bar and resumed after it. This is the gap that
   says the run has ended: fixed width, no growth, the ground showing through.
   The ground, not a border — a 1px rule inside a rounded, clipped track is the
   one place a hairline goes astray. */
.ofc-bar__break {
  flex: 0 0 6px;
  /* the deliberate gap in the pipeline bar — it shows the card through */
  background: var(--o-panel);
}
/* And the same break in the legend, in words, because the bar's gap is not
   available to a screen reader reading a list. */
.ofc-legend__sep {
  width: 100%;
  margin-top: 0.15rem;
  padding-top: 0.35rem;
  border-top: 1px solid var(--o-line);
}
.ofc-legend__sep .ofc-legend__t { color: var(--o-faint); }
.ofc-bar--empty { background: var(--o-inset); }

/* The legend is always present: identity is never colour alone. */
.ofc-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem 1rem;
  margin: 0;
  padding: 0;
  list-style: none;
}
.ofc-legend li {
  display: flex;
  align-items: baseline;
  gap: 0.45rem;
  font-size: var(--o-t-sm);
}
.ofc-legend__d {
  width: 9px;
  height: 9px;
  flex: none;
  border-radius: 3px;
  transform: translateY(1px);
  background: var(--seg-c, var(--o-neutral));
}
/* Text wears TEXT tokens, never the series colour — a light green is
   illegible as type. The swatch beside it carries the identity. */
.ofc-legend__t { color: var(--o-dim); font-weight: 500; }
.ofc-legend__n { color: var(--o-text); font-weight: 700; font-variant-numeric: tabular-nums; }

/* The stages with nothing in them, as one line rather than nine empty boxes.
   Still REPORTED — "no jobs on hold" is a fact, and hiding it would be the
   screen deciding what you may know — just reported at what a zero is worth. */
.ofc-none {
  margin: 0.65rem 0 0;
  font-size: var(--o-t-sm);
  font-weight: 500;
  color: var(--o-faint);
}
.ofc-none:empty { display: none; }

/* ==========================================================================
   7. THE VARIANCE CHART

   Estimated against actual, by cost code. DIVERGING, because the question is
   which side of the estimate a code fell on: two hues away from a neutral
   centre line, equal steps per arm.

   THE PALE PEACH IS NOT USED AS A FILL. #E8A87C against the green arm
   separates at ΔE 4.8 under protanopia — below the floor of 6 — so it is text
   only, and the fill is #C4703A, which clears at 9.5. Measured, not judged.

   Direction is also encoded by SIDE and by a signed number on every row, so
   the chart still reads with no colour at all.
   ========================================================================== */

.ofc-var { display: grid; gap: 0.15rem; }

.ofc-var__row {
  display: grid;
  grid-template-columns: minmax(7rem, 1.4fr) minmax(9rem, 2fr) 4.5rem;
  align-items: center;
  gap: 0.75rem;
  padding: 0.32rem 0.5rem;
  border-radius: var(--o-r-sm);
}
.ofc-var__row:hover { background: var(--o-hover); }

.ofc-var__k {
  font-size: var(--o-t-sm);
  font-weight: 650;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ofc-var__k small {
  display: block;
  font-size: var(--o-t-xs);
  font-weight: 500;
  color: var(--o-faint);
  font-variant-numeric: tabular-nums;
}

/* Two half-tracks either side of a hairline centre. The bar grows FROM the
   centre, so an overrun and an underrun of the same size are mirror images. */
.ofc-var__track {
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-items: center;
  height: 10px;
  border-left: 0;
  position: relative;
}
.ofc-var__track::before {
  content: '';
  position: absolute;
  left: 50%;
  top: -2px;
  bottom: -2px;
  width: 1px;
  background: var(--o-line-2);
}
.ofc-var__under, .ofc-var__over { height: 10px; }
.ofc-var__under {
  justify-self: end;
  width: var(--w, 0%);
  background: var(--st-punch);
  border-radius: 4px 0 0 4px;   /* rounded data-end, square at the baseline */
}
.ofc-var__over {
  justify-self: start;
  width: var(--w, 0%);
  background: var(--o-warn);
  border-radius: 0 4px 4px 0;
}

.ofc-var__n {
  text-align: right;
  font-size: var(--o-t-sm);
  font-weight: 700;
  /* TABULAR here: this is a column of numbers that has to line up. */
  font-variant-numeric: tabular-nums;
}
.ofc-var__n.over  { color: var(--o-warn-ink); }
.ofc-var__n.under { color: var(--o-accent); }

.ofc-var__head {
  display: grid;
  grid-template-columns: minmax(7rem, 1.4fr) minmax(9rem, 2fr) 4.5rem;
  gap: 0.75rem;
  padding: 0 0.5rem 0.4rem;
  margin-bottom: 0.25rem;
  border-bottom: 1px solid var(--o-line);
  font-size: var(--o-t-xs);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--o-faint);
}
.ofc-var__head span:last-child { text-align: right; }
.ofc-var__head span:nth-child(2) { text-align: center; }

/* ==========================================================================
   8. LISTS AND TABLES
   ========================================================================== */

.ofc-table { width: 100%; border-collapse: collapse; font-size: var(--o-t-sm); }

.ofc-table th {
  position: sticky;
  top: 0;
  z-index: 1;
  text-align: left;
  padding: 0.4rem 0.6rem;
  /* a sticky header the rows scroll under — it must match its card */
  background: var(--o-panel);
  border-bottom: 1px solid var(--o-line-2);
  color: var(--o-faint);
  font-size: var(--o-t-xs);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.ofc-table td {
  padding: 0.42rem 0.6rem;
  border-bottom: 1px solid var(--o-line);
  font-weight: 500;
  color: var(--o-dim);
}
.ofc-table tbody tr:hover td { background: var(--o-hover); color: var(--o-text); }
.ofc-table tbody tr:last-child td { border-bottom: 0; }
.ofc-table td:first-child { color: var(--o-text); font-weight: 650; }
.ofc-table td.num { text-align: right; font-variant-numeric: tabular-nums; }
/* SAID, NOT BLANK. An empty cell reads as "not loaded yet"; a customer with no
   current property is a customer nothing can be attached to. */
.ofc-table td.is-missing { color: var(--o-faint); font-style: italic; }
/* A SECOND LINE IN A CELL, on the model of `.ofc-tot dd small`. Inline it read
   "none now77 Industrial Way, Lincoln — owned until 2026-03-01" — two facts
   with no space between them, which is worse than the omission it replaced.
   Upright, because the qualifier is the concrete half: the cell above it is
   italic to mark an absence, and a date is not an absence. */
.ofc-table td small {
  display: block;
  margin-top: 0.15rem;
  font-size: var(--o-t-xs);
  font-style: normal;
  color: var(--o-faint);
}

.ofc-tablewrap { overflow-x: auto; margin: 0 -0.35rem; padding: 0 0.35rem; }

/* ---- the waiting list --------------------------------------------------
   `.ofc-feed`, NOT `.ofc-list`. This is a three-column row — monogram, who,
   age chip — and `.ofc-list` was already spoken for by the owner's waiver list
   and the portal's invoices, which are `<b><span><em>` stacked. Taking the
   name would have laid a 3-column grid over their 3 children and shuffled
   every one of those rows sideways. */
.ofc-feed { list-style: none; margin: 0; padding: 0; display: grid; }

.ofc-feed li {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 0.7rem;
  padding: 0.5rem 0.55rem;
  border-radius: var(--o-r-sm);
  border-bottom: 1px solid var(--o-line);
}
.ofc-feed li:last-child { border-bottom: 0; }
.ofc-feed li:hover { background: var(--o-hover); }

.ofc-mono {
  width: 26px;
  height: 26px;
  flex: none;
  display: grid;
  place-items: center;
  border-radius: 8px;
  background: var(--o-inset);
  color: var(--o-dim);
  font-family: var(--display);
  font-size: var(--o-t-xs);
  font-weight: 750;
}

.ofc-feed__who { display: grid; gap: 0.05rem; min-width: 0; }
.ofc-feed__who b {
  font-size: var(--o-t-sm);
  font-weight: 650;
  color: var(--o-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ofc-feed__who span {
  font-size: var(--o-t-xs);
  font-weight: 500;
  color: var(--o-faint);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---- chips ------------------------------------------------------------- */

.ofc-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.14rem 0.45rem;
  border-radius: 999px;
  background: var(--o-inset);
  color: var(--o-dim);
  font-size: var(--o-t-xs);
  font-weight: 650;
  letter-spacing: 0.02em;
  white-space: nowrap;
}
/* Age, as severity. A status colour ships with a word beside it, never alone:
   the chip always says the age too. */
.ofc-chip--hot  { background: var(--o-warn-bg); color: var(--o-warn-ink); }
/* `--o-warn-ink` rather than a fourth cream: #D8A184 measured 8.40:1 on this
   chip and the token measures 9.27:1, so one warn ink costs nothing. */
.ofc-chip--warm { background: rgba(196, 112, 58, 0.13); color: var(--o-warn-ink); }
.ofc-chip--ok   { background: var(--o-accent-wash); color: var(--o-accent); }

/* ---- the roster -------------------------------------------------------- */

.ofc-roster { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.3rem; }
.ofc-roster li {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  padding: 0.45rem 0.7rem;
  border: 1px solid var(--o-line);
  border-radius: var(--o-r-sm);
  background: var(--o-inset);
  font-size: var(--o-t-sm);
  font-weight: 600;
}
.ofc-roster button:first-of-type { margin-left: auto; }

/* A waiver's reason sits under its count rather than beside it — a sentence
   in a numeric column pushes every cell after it out of line. */
.ofc-lines td > small {
  display: block;
  margin-top: 0.15rem;
  font-size: var(--o-t-xs);
  font-weight: 400;
  color: var(--o-dim);
  line-height: 1.35;
}

/* An armed control states what the next click does. The BORDER takes the mark
   colour and the LABEL takes the ink one — §7 above: --o-warn is a mark, and
   putting it on text is the contrast failure that section exists to stop. */
.ofc-ghost.is-armed {
  border-color: var(--o-warn);
  color: var(--o-warn-ink);
}

/* THE SAMPLE-CONTENT BANNER.
 *
 * This is the one place on the screen where the warn colour is spent on
 * something that is not a number costing money, and it earns it: every figure
 * below it is fabricated, and the entire risk of the feature is somebody
 * reading one as real. It sits above the content rather than in a corner, it
 * is solid rather than a hairline, and it cannot be dismissed while the mode
 * is on — the button turns the MODE off, not the banner.
 */
.ofc-sample-banner {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin: 0 0 1rem;
  padding: 0.7rem 0.95rem;
  border-radius: var(--o-r-sm);
  background: var(--o-warn-bg);
  border: 1px solid var(--o-warn);
  color: var(--o-warn-ink);
  font-size: var(--o-t-sm);
  font-weight: 600;
}
.ofc-sample-banner strong { color: var(--o-warn-ink); }
.ofc-sample-banner button { margin-left: auto; }

/* ---- the CDTFA split, under the line editor ----------------------------
 *
 * A SUMMARY, NOT A SECOND TABLE. The lines above already carry the treatment
 * per row; this answers the one question the rows cannot — what each side is
 * worth — so it is a short list rather than a grid competing with the table it
 * sits under.
 */
.ofc-taxsplit { margin-top: 1rem; }
.ofc-taxsplit h4 {
  margin: 0 0 0.5rem;
  font-size: var(--o-t-xs);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--o-faint);
}
.ofc-taxsplit__l { list-style: none; margin: 0; padding: 0; display: grid; gap: 1px;
  background: var(--o-line); border-radius: var(--o-r-sm); overflow: hidden; }
.ofc-taxsplit__l li {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  flex-wrap: wrap;
  padding: 0.45rem 0.7rem;
  background: var(--o-inset);
  font-size: var(--o-t-sm);
  font-weight: 600;
}
.ofc-taxsplit__l li b { margin-left: auto; font-variant-numeric: tabular-nums; }
.ofc-taxsplit__l li small {
  flex: 1 0 100%;
  font-size: var(--o-t-xs);
  font-weight: 500;
  color: var(--o-dim);
}
/* An unclassified line is the one that costs something. */
.ofc-taxsplit__l li.is-unpriced { color: var(--o-warn-ink); }

/* A select inside a table row: no field-grid label above it, and it must not
   force the column wider than the words it holds. */
.ofc-inline {
  max-width: 100%;
  min-width: 0;
  padding: 0.2rem 0.35rem;
  border: 1px solid var(--o-line-2);
  border-radius: var(--o-r-sm);
  background: var(--o-inset);
  color: var(--o-text);
  font-size: var(--o-t-xs);
  font-weight: 600;
}
.ofc-inline:disabled { opacity: 0.55; }

/* A LIST THAT IS NOT SHOWING EVERYTHING SAYS SO, and it is not a warning.
   Nothing is wrong when a table is capped — the screen is doing what it was
   asked. Warn amber here would make an ordinary large table look like a fault
   and would spend the one accent this design reserves for a number that is
   costing money. Dim ink and the same hairline every other note uses. */
.ofc-state.is-partial {
  color: var(--o-dim);
  border-left: 2px solid var(--o-line-2);
  padding-left: 0.65rem;
}

/* A checkbox reads left-to-right, not stacked. `.ofc-fields label` puts its
   text above the control, which is right for a text field and wrong here — the
   box belongs beside the words it qualifies. */
.ofc-fields label.ofc-check {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  text-transform: none;
  letter-spacing: 0;
  font-size: var(--o-t-sm);
  font-weight: 600;
  color: var(--o-text);
  align-self: end;
  min-height: 2.4rem;
}
.ofc-fields label.ofc-check input {
  width: 1.05rem;
  height: 1.05rem;
  min-height: 0;
  accent-color: var(--o-accent);
  flex: 0 0 auto;
}

/* ---- a magnitude bar inside a table row --------------------------------
 *
 * ONE FILL FOR EVERY ROW. Length carries the magnitude; a ramp would be a
 * second encoding of the same number and the reader would hunt for a meaning
 * that is not there. The colour is a step from the stage ramp rather than the
 * accent, because the accent means "the current section" and "a good state"
 * and a landing page booking less than another one is neither.
 *
 * THE ROUNDED END IS ON THE DATA END ONLY. A bar rounded at both ends starts
 * somewhere other than the baseline, which reads as a floating range.
 */

.ofc-mag { min-width: 7rem; }
.ofc-mag__track {
  display: block;
  height: 10px;
  border-radius: 2px;
  background: var(--o-inset);
}
.ofc-mag__fill {
  display: block;
  height: 10px;
  width: var(--w, 0%);
  min-width: 2px;                 /* a real, tiny value still draws something */
  background: var(--st-progress);
  border-radius: 2px 4px 4px 2px;
}
.ofc-mag__none {
  font-size: var(--o-t-xs);
  color: var(--o-faint);
  font-style: italic;
}

/* ---- photographs of what got buried ------------------------------------
 *
 * A FIXED ASPECT AND A CONTAINED FIT. Field photographs come off phones in
 * both orientations and at wildly different sizes; letting each set its own
 * height gives a ragged wall, and `cover` would crop the very thing being
 * evidenced — the edge of the trench, the depth mark on the sleeve. So the
 * frame is fixed and the image is contained inside it, letterboxed rather
 * than cut.
 */

.ofc-shots {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
  gap: 0.7rem;
}
.ofc-shot { min-width: 0; }
.ofc-shot__f {
  position: relative;
  aspect-ratio: 4 / 3;
  display: grid;
  place-items: center;
  overflow: hidden;
  border: 1px solid var(--o-line);
  border-radius: var(--o-r-sm);
  background: var(--o-inset);
}
.ofc-shot__f img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
}
.ofc-shot__ph {
  padding: 0 0.5rem;
  color: var(--o-faint);
  font-size: var(--o-t-xs);
  text-align: center;
  overflow-wrap: anywhere;
}
.ofc-shot__t {
  margin: 0.35rem 0 0;
  font-size: var(--o-t-sm);
  font-weight: 600;
  color: var(--o-text);
  overflow-wrap: anywhere;
}
.ofc-shot__m {
  margin: 0.1rem 0 0;
  font-size: var(--o-t-xs);
  color: var(--o-dim);
  overflow-wrap: anywhere;
}

/* ---- counted rows (Database) ------------------------------------------- */

.ofc-counts {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  gap: 0.3rem;
}
.ofc-counts li {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
  padding: 0.4rem 0.65rem;
  border: 1px solid var(--o-line);
  border-radius: var(--o-r-sm);
  background: var(--o-inset);
  font-size: var(--o-t-sm);
  font-weight: 600;
}
.ofc-counts span { color: var(--o-dim); }
.ofc-counts b {
  font-family: var(--display);
  font-size: var(--o-t-md);
  font-variant-numeric: tabular-nums;
  color: var(--o-text);
}
/* REFUSED IS NOT ZERO. Three different answers, never one dash. */
.ofc-counts li.is-empty b { color: var(--o-faint); }
.ofc-counts li.is-refused b { font-family: var(--body); font-size: var(--o-t-xs); color: var(--o-warn-ink); }

/* ---- what this cannot tell you ----------------------------------------- */

.ofc-cannot { margin: 0; display: grid; gap: 0.2rem; }
.ofc-cannot dt {
  margin-top: 0.9rem;
  font-family: var(--display);
  font-size: var(--o-t-md);
  font-weight: 700;
}
.ofc-cannot dt:first-child { margin-top: 0; }
.ofc-cannot dd {
  margin: 0;
  max-width: 70ch;
  color: var(--o-faint);
  font-size: var(--o-t-sm);
  font-weight: 500;
  line-height: 1.5;
}
.ofc-cannot dd.ofc-needs { color: var(--o-accent); margin-top: 0.15rem; }

/* ==========================================================================
   8b. THE SPLIT — a list, and the one thing you are looking at.

   Built for the mailbox and then wanted immediately by the estimate editor,
   which is the same shape for the same reason: you work a RUN of these, and a
   screen that opens each one on its own page is a screen where you lose your
   place every time you go back.

   `.ofc-split` is the layout; `.ofc-mail__*` and `.ofc-est__*` are what goes
   INSIDE it. Generalised at the second caller rather than the fifth, while it
   was still two rules and a rename.
   ========================================================================== */

.ofc-split {
  display: grid;
  grid-template-columns: minmax(17rem, 22rem) minmax(0, 1fr);
  gap: 0;
  align-items: start;
  /* The panes scroll independently and the page does not: a reading pane that
     pushes the list off the bottom is a single pane with extra steps. */
  height: min(62vh, 40rem);
}
@media (max-width: 68rem) {
  .ofc-split { grid-template-columns: minmax(0, 1fr); height: auto; }
  /* Stacked, the divider has to turn ninety degrees with the layout. */
  .ofc-split__list {
    max-height: 24rem;
    border-right: 0;
    border-bottom: 1px solid var(--o-line-2);
  }
  .ofc-split__pane { padding: 1rem 0 0; }
}

.ofc-split__list {
  list-style: none;
  margin: 0;
  padding: 0.25rem;
  height: 100%;
  overflow-y: auto;
  /* The only edge either pane gets. */
  border-right: 1px solid var(--o-line-2);
  scrollbar-color: var(--o-line-2) transparent;
}

.ofc-split__item {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 0.1rem 0.6rem;
  width: 100%;
  padding: 0.5rem 0.6rem;
  border: 0;
  border-radius: var(--o-r-sm);
  background: transparent;
  background-image: none;
  box-shadow: none;
  color: inherit;
  text-align: left;
  font: inherit;
  min-height: 0;
  cursor: pointer;
}
.ofc-split__item:hover { background: var(--o-hover); }
.ofc-split__item:active { transform: none; }
.ofc-split__item.is-on { background: var(--o-press); }

/* ==========================================================================
   §  THE MAILBOX ROW                                                2026-09-08
   Rebuilt to the shape every mail client has converged on, because it is the
   shape that works: ONE ROW PER MESSAGE, fixed height, and four fields in a
   grid that never reflows — avatar, sender, subject·snippet, date. The old row
   was three stacked lines (sender / subject / "to daniel@…"), which is three
   lines to say what one says, made a 40-message list eight screens long, and
   put the recipient — identical on every row in a single-address mailbox — on
   its own line.
   ========================================================================== */
.ofc-split__item {
  display: grid;
  grid-template-columns: 2rem minmax(7rem, 13rem) minmax(0, 1fr) auto;
  align-items: center;
  column-gap: 0.75rem;
  row-gap: 0;
  padding: 0 0.75rem;
  min-height: 2.75rem;
  border-radius: 0;
  border-bottom: 1px solid var(--o-line);
  text-align: left;
}
.ofc-split__item:last-child { border-bottom: 0; }

/* THE AVATAR. A monogram disc, tinted from the address — see mailTint(). */
.ofc-mail__av {
  display: grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  border-radius: 50%;
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: #FFFFFF;
  user-select: none;
}

.ofc-mail__from {
  grid-column: 2;
  font-size: var(--o-t-sm);
  font-weight: 500;
  color: var(--o-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* SUBJECT AND SNIPPET SHARE ONE CELL and one line. The subject takes the
   weight; the snippet trails after an em dash in the faint ink and is the
   first thing the ellipsis eats when the column narrows, which is the correct
   order to lose them in. */
.ofc-mail__subj {
  grid-column: 3;
  min-width: 0;
  font-size: var(--o-t-sm);
  font-weight: 500;
  color: var(--o-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ofc-mail__snip { color: var(--o-faint); font-weight: 400; }
.ofc-mail__when {
  grid-column: 4;
  justify-self: end;
  font-size: var(--o-t-xs);
  font-weight: 500;
  color: var(--o-faint);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.ofc-mail__thread {
  margin-left: 0.4rem;
  padding: 0.05rem 0.35rem;
  border: 1px solid var(--o-line-2);
  border-radius: 2px;
  font-size: 0.625rem;
  font-weight: 600;
  color: var(--o-faint);
}

/* UNREAD IS WEIGHT AND A DOT, never colour alone. In an unread row the SENDER
   and the SUBJECT go to full white and full weight; the snippet does not,
   because if everything is bold nothing is. */
.ofc-split__item.is-unread .ofc-mail__from { color: var(--o-text); font-weight: 700; }
.ofc-split__item.is-unread .ofc-mail__subj { color: var(--o-text); font-weight: 650; }
.ofc-split__item.is-unread .ofc-mail__snip { color: var(--o-dim); font-weight: 400; }
.ofc-split__item.is-unread .ofc-mail__when { color: var(--o-text); font-weight: 650; }
.ofc-mail__dot {
  display: inline-block;
  width: 6px;
  height: 6px;
  margin-right: 0.4rem;
  border-radius: 50%;
  background: var(--o-accent);
  transform: translateY(-1px);
}


/* Which @dztlandscaping.com address it landed on. info@ and ap@ are different
   queues to a person even when they share one screen. */
.ofc-mail__to {
  grid-column: 1 / -1;
  margin-top: 0.15rem;
  font-size: var(--o-t-xs);
  font-weight: 650;
  color: var(--o-faint);
  font-variant-numeric: tabular-nums;
}

.ofc-split__pane {
  height: 100%;
  overflow-y: auto;
  padding: 0.2rem 0 0.2rem 1.25rem;
  scrollbar-color: var(--o-line-2) transparent;
}
.ofc-split__pane:focus-visible { outline: 2px solid var(--o-accent); outline-offset: -2px; }

.ofc-mail__h { margin: 0 0 0.15rem; font-family: var(--display); font-size: var(--o-t-lg); font-weight: 700; letter-spacing: -0.012em; }
.ofc-mail__meta {
  display: grid;
  gap: 0.1rem;
  margin: 0 0 0.9rem;
  padding-bottom: 0.75rem;
  border-bottom: 1px solid var(--o-line);
  font-size: var(--o-t-sm);
  color: var(--o-faint);
}
.ofc-mail__meta b { color: var(--o-dim); font-weight: 650; }

/* The body is a stranger's text. Pre-wrap so the sender's own line breaks
   survive, and a measure so a 200-character line is still readable. */
.ofc-mail__body {
  margin: 0;
  max-width: 78ch;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  font-size: var(--o-t-sm);
  font-weight: 450;
  line-height: 1.6;
  color: var(--o-dim);
}

/* ---- which enquiry this message belongs to (0044) -------------------------
   ABOVE THE ACTIONS AND BELOW THE BODY, because it is a statement about the
   message rather than a thing to do with it — and it is read after the text
   that tells you which enquiry it is. Separated by a rule so it does not read
   as the last line of a stranger's email. */
.ofc-mail__filing {
  margin-top: 1rem;
  padding-top: 0.85rem;
  border-top: 1px solid var(--o-line-2);
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.35rem 0.75rem;
  font-size: var(--o-t-xs);
  color: var(--o-dim);
}
.ofc-mail__filing label {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
}
.ofc-mail__lead {
  font: inherit;
  color: var(--o-text);
  background: var(--o-bg-2, transparent);
  border: 1px solid var(--o-line-2);
  border-radius: 6px;
  padding: 0.28rem 0.5rem;
  max-width: 26ch;
}
.ofc-mail__lead:disabled { opacity: 0.5; cursor: not-allowed; }
.ofc-mail__filing-msg { margin: 0; }

/* ---- the messages filed against one enquiry (the other half of 0044) ------
   A LIST OF BUTTONS, because each one goes to the message. Styled as text
   rather than as controls: five buttons stacked in a detail pane read as a
   toolbar, and these are a thread. */
.ofc-lead__mail {
  margin: 0.4rem 0 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 0.4rem;
  font-size: var(--o-t-xs);
}
.ofc-lead__mail li { line-height: 1.45; }
.ofc-linkish {
  appearance: none;
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: var(--o-text);
  text-align: left;
  cursor: pointer;
  border-bottom: 1px solid var(--o-line-2);
  /* SECOND VICTIM OF THE SAME RULE. app.css sets `button { min-height: 56px }`
     — correct for the field app, where a control is pressed with a glove on a
     phone in a yard, and wrong for anything that is a <button> only so the
     keyboard can reach it. Measured at 164×56 with the text on the first line,
     which put this underline TWENTY PIXELS BELOW THE WORDS it was underlining
     and made it read as a stray rule across the row. `.ofc-why` had it too. */
  min-height: 0;
}
.ofc-linkish:hover { border-bottom-color: var(--o-accent-deep); }
.ofc-linkish:focus-visible { outline: 2px solid var(--o-accent-deep); outline-offset: 2px; }

/* ONE ROW, NOT THREE. The address tabs, the toolbar and a paragraph of prose
   were three stacked bands between the card heading and the first message —
   five rows of chrome before any mail, which is how a screen reads as assembled
   rather than designed. The tabs choose WHICH mail; the buttons narrow and act
   on it. Same job, same line: tabs left, actions right. */
.ofc-mail__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  margin: 0.9rem 0 0.25rem;
}
.ofc-mail__bar .ofc-addr { margin: 0; flex: 1 1 auto; }

.ofc-mail__tools {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex: 0 0 auto;
  margin-left: auto;
}
/* The help toggle is a single glyph, so it wants to be square rather than a
   pill with one character rattling around in it. */
.ofc-mail__keys {
  min-width: 34px;
  padding: 0;
  font-size: 0.95rem;
  font-weight: 700;
}
.ofc-mail__help-note {
  grid-column: 1 / -1;
  border-top: 1px solid var(--o-line-2);
  padding-top: 0.6rem;
  margin-top: 0.15rem;
}
.ofc-mail__help-note dd { margin: 0; color: var(--o-faint, var(--o-dim)); }
/* The pressed state of "Unread only" has to be visible without colour alone —
   it changes what the list contains, and a filter you cannot tell is on is how
   somebody concludes the mailbox has lost their mail. */
.ofc-mail__tools .ofc-ghost.is-on {
  background: var(--o-inset);
  border-color: var(--o-accent-deep);
  color: var(--o-text);
}
.ofc-mail__hint {
  margin-left: auto;
  font-size: var(--o-t-xs, 0.75rem);
  color: var(--o-faint, var(--o-dim));
  letter-spacing: 0.02em;
}
/* Below the rail's collapse the hint is the first thing worth losing: it
   describes keys a phone does not have. */
@media (max-width: 62rem) { .ofc-mail__hint { display: none; } }

/* M-04 — WHAT THE FIRST SCREEN IS SPENT ON. At 390px the card's orientation
   paragraph and four address pills wrapping onto two rows pushed the first
   message most of the way down the viewport: three-quarters of a phone screen
   explaining the mailbox to somebody who opened the mailbox.

   The paragraph is orientation and orientation belongs where there is room for
   it. The pills stay — they are a filter, not decoration — but they scroll on
   one row instead of stacking, which is the same trade the rail makes. */
@media (max-width: 48rem) {
  .ofc-view[data-view="mail"] .ofc-card__bar .ofc-sub { display: none; }
  .ofc-mail__bar { gap: 0.5rem; }
  .ofc-addr {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
    scroll-snap-type: x proximity;
  }
  .ofc-addr::-webkit-scrollbar { display: none; }
  .ofc-addr > * { flex: none; scroll-snap-align: start; }
  .ofc-mail__tools { width: 100%; margin-left: 0; }
}

/* A-06 — off-canvas until focused, then a real target. Moved with a transform
   and never `display:none` or `visibility:hidden`, both of which take it out
   of the tab order — a skip link that cannot be reached by tabbing is a skip
   link that does not exist. */
.ofc-skip {
  position: absolute;
  left: 0.6rem; top: 0.6rem;
  z-index: 100;
  padding: 0.6rem 0.9rem;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  border-radius: 8px;
  background: var(--o-accent-deep);
  /* The ground, for the same reason .ofc-primary uses it: white on this green
     is 3.33:1. This is the first control a keyboard user reaches. */
  color: var(--o-bg);
  font: 650 var(--o-t-sm)/1 var(--body);
  text-decoration: none;
  transform: translateY(-160%);
}
/* BOTH, and `:focus` is the load-bearing one. `:focus-visible` alone leaves
   the link off-canvas for anything that focuses it without the browser
   deciding a keyboard did it — which is how a screen-reader user gets a
   control that is announced and not drawn. */
.ofc-skip:focus,
.ofc-skip:focus-visible {
  transform: none;
  outline: 2px solid var(--o-accent);
  outline-offset: 2px;
}
@media (prefers-reduced-motion: no-preference) {
  .ofc-skip { transition: transform .14s ease; }
}

/* WHERE IT LANDS YOU HAS TO SHOW. The target is a container the size of the
   screen, so this is the one place an outline is drawn round everything on
   purpose: pressing the link otherwise looks like nothing happened, and the
   only proof would be the next tab press. `:focus-visible` so it appears for
   the keyboard and not for a script. */
#ofc-grid:focus { outline: none; }
#ofc-grid:focus-visible {
  outline: 2px solid var(--o-accent);
  outline-offset: 6px;
  border-radius: 10px;
}

/* D-01 — the waiting list is now a list of doors, so it has to look like one.

   `grid-column: 1 / -1` IS THE LOAD-BEARING LINE. The row is a three-column
   grid — monogram, who, chip — and those three moved inside the link, so the
   link became a single grid item sitting in column one: 237px of a 361px row
   at phone width, with the rest of the row looking exactly as pressable as the
   part that was. Spanning the whole track is what makes the target the row.
   Measured in the browser at 390, not reasoned about. */
.ofc-feed__open {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: 0.7rem;
  width: 100%;
  padding: 0.5rem 0.6rem;
  margin: -0.5rem -0.6rem;
  border-radius: 9px;
  color: inherit;
  text-decoration: none;
}
.ofc-feed__open:hover { background: var(--o-hover); }
.ofc-feed__open:focus-visible { outline: 2px solid var(--o-accent-deep); outline-offset: 1px; }
.ofc-feed__open .ofc-feed__who { flex: 1 1 auto; min-width: 0; }
.ofc-feed__more { padding-top: 0.5rem; }
.ofc-feed__more a { color: var(--o-accent); font-size: var(--o-t-sm); font-weight: 620; }

/* Day headings inside the list. Sticky so the run you are scrolling through
   keeps saying which day it is — the whole point is not having to scroll back
   up to find out. */
.ofc-mail__day {
  position: sticky;
  top: 0;
  z-index: 1;
  padding: 0.5rem 0.9rem 0.3rem;
  /* the sticky day divider in the mail list */
  background: var(--o-panel);
  font-size: var(--o-t-xs, 0.72rem);
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--o-faint, var(--o-dim));
}
.ofc-mail__day:first-child { padding-top: 0.2rem; }

/* The conversation count, on the subject line where the eye already is. */
.ofc-mail__thread {
  margin-left: 0.5rem;
  padding: 0.05rem 0.4rem;
  border: 1px solid var(--o-line-2);
  border-radius: var(--o-pill);
  font-size: var(--o-t-xs, 0.7rem);
  font-weight: 650;
  color: var(--o-faint, var(--o-dim));
  white-space: nowrap;
}

.ofc-mail__help {
  margin: 0.35rem 0 0;
  padding: 0.9rem 1rem;
  border: 1px solid var(--o-line-2);
  border-radius: var(--o-r, 10px);
  background: var(--o-inset);
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: 0.45rem 1.5rem;
  font-size: var(--o-t-sm);
}
.ofc-mail__help > div { display: flex; align-items: baseline; gap: 0.6rem; }
.ofc-mail__help dt { flex: 0 0 auto; display: flex; gap: 0.25rem; }
.ofc-mail__help dd { margin: 0; color: var(--o-dim); }
.ofc-mail__help kbd {
  min-width: 1.5rem;
  padding: 0.1rem 0.35rem;
  border: 1px solid var(--o-line);
  border-bottom-width: 2px;
  border-radius: 5px;
  background: var(--o-bg);
  font: 650 0.72rem/1.5 var(--mono, ui-monospace, monospace);
  color: var(--o-text);
  text-align: center;
}

/* Compose sits between the toolbar and the list rather than over them: a modal
   would cover the message you are half-replying to, and this screen's whole
   shape is list-beside-message. */
.ofc-compose {
  display: grid;
  gap: 0.7rem;
  margin: 0.6rem 0 0.2rem;
  padding: 1rem 1.1rem 1.1rem;
  border: 1px solid var(--o-line);
  border-radius: var(--o-r, 10px);
  background: var(--o-inset);
}
.ofc-compose__head {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
}
.ofc-compose__head b { font-size: var(--o-t-sm); }
.ofc-compose__from {
  color: var(--o-faint, var(--o-dim));
  font-size: var(--o-t-xs, 0.75rem);
}
.ofc-compose__head .ofc-quiet { margin-left: auto; }
.ofc-compose label {
  display: grid;
  gap: 0.3rem;
  font-size: var(--o-t-xs, 0.75rem);
  font-weight: 650;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--o-faint, var(--o-dim));
}
.ofc-compose input,
.ofc-compose textarea {
  width: 100%;
  padding: 0.55rem 0.7rem;
  border: 1px solid var(--o-line);
  border-radius: 8px;
  background: var(--o-bg);
  color: var(--o-text);
  font: 450 var(--o-t-sm)/1.55 var(--body);
  text-transform: none;
  letter-spacing: 0;
}
.ofc-compose textarea { resize: vertical; min-height: 8rem; }
.ofc-compose input:focus-visible,
.ofc-compose textarea:focus-visible {
  outline: 2px solid var(--o-accent-deep);
  outline-offset: 1px;
}
.ofc-compose .ofc-actions { align-items: center; }
.ofc-compose .ofc-actions p { margin: 0; font-size: var(--o-t-sm); color: var(--o-dim); }

.ofc-mail__clip {
  width: 0.85em; height: 0.85em;
  margin-right: 0.3rem;
  vertical-align: -0.08em;
  fill: currentColor;
  opacity: 0.75;
}

/* Named for a screen reader, removed from sight. The icon above carries the
   same fact visually. */
.ofc-sr {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* A rule above the actions, because a pre-wrap body ends wherever the sender
   stopped typing and a button sitting directly under the last line reads as
   part of the message. */
/* FOUR BUTTONS IN FOUR WEIGHTS, IN A ROW, READ AS FOUR UNRELATED CONTROLS.
   They are not: two of them act on the message and two are utilities. Reply is
   the primary and Forward sits beside it; the utilities are pushed to the far
   end so the eye lands on the thing you came here to do. */
.ofc-mail__actions {
  margin-top: 1.5rem;
  padding-top: 1rem;
  border-top: 1px solid var(--o-line-2);
  gap: 0.5rem;
}
.ofc-mail__actions .ofc-quiet:first-of-type { margin-left: auto; }

.ofc-split__empty {
  display: grid;
  place-content: center;
  gap: 0.3rem;
  height: 100%;
  text-align: center;
  color: var(--o-faint);
  font-size: var(--o-t-sm);
}

/* ==========================================================================
   9. THE CRM BOARD
   ========================================================================== */

.ofc-board {
  display: grid;
  grid-auto-flow: column;
  /* SIX COLUMNS, NOT FIVE — new, contacted, quoted, won, lost AND spam. Six at
     a width where a name, a phone number and a line of scope are all readable
     does not fit a laptop, so this genuinely has to scroll. It always did; what
     it lacked was any sign of it, so the sixth column sat sliced down its
     middle by the card edge and read as broken rather than as more. Hence the
     fade below rather than columns squeezed until they fit. */
  grid-auto-columns: minmax(12rem, 1fr);
  gap: 0.7rem;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  padding-bottom: 0.4rem;
}
/* THE EDGE SAYS THERE IS MORE. Applied only when the board actually overflows
   and removed once it is scrolled to the end, so the fade is information rather
   than decoration — a permanent gradient on a board with nothing past it is a
   promise of content that is not there. */
.ofc-board.is-overflowing {
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 3rem), transparent);
  mask-image: linear-gradient(to right, #000 calc(100% - 3rem), transparent);
}
.ofc-board.is-overflowing.is-scroll-end {
  -webkit-mask-image: none;
  mask-image: none;
}
@media (prefers-reduced-motion: reduce) { .ofc-board { scroll-behavior: auto; } }

.ofc-board__col {
  display: grid;
  align-content: start;
  gap: 0.35rem;
  padding: 0.5rem;
  border-radius: var(--o-r);
  min-height: 6rem;
  /* A column is a shallower pane than a card — it sits INSIDE one, and two
     full-strength rings nested is what makes a UI look bevelled. */
  /* A WELL, NOT A PANEL. The tiles inside are --o-inset, so the column
     has to read BELOW them or the board is one flat sheet again. */
  background: var(--o-bg);
  border: 1px solid var(--o-line-2);
}
.ofc-board__h {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  margin: 0 0 0.2rem;
  padding: 0 0.15rem;
  font-size: var(--o-t-xs);
  font-weight: 750;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--o-faint);
}
/* THE COLUMN THAT COSTS MONEY WHILE YOU READ IT. */
.ofc-board__col.is-hot { border-color: rgba(196, 112, 58, 0.34); }
.ofc-board__col.is-hot .ofc-board__h { color: var(--o-warn-ink); }

.ofc-board__card {
  display: grid;
  gap: 0.15rem;
  padding: 0.5rem 0.6rem;
  border: 1px solid var(--o-line);
  border-radius: var(--o-r-sm);
  background: var(--o-inset);
}
.ofc-board__card strong { font-size: var(--o-t-sm); font-weight: 650; }
.ofc-board__card em {
  font-style: normal;
  font-size: var(--o-t-xs);
  font-weight: 500;
  color: var(--o-faint);
}
.ofc-board__empty { margin: 0; padding: 0.2rem 0.15rem; color: var(--o-faint); font-size: var(--o-t-xs); }

/* ==========================================================================
   10. FORMS

   A task with a name, a rule above it, and its own button. Four unlabelled
   forms in one card was a wall of controls where nothing said where one job
   ended and the next began.
   ========================================================================== */

.ofc-task {
  display: grid;
  gap: 0.7rem;
  margin-top: 1.1rem;
  padding-top: 1rem;
  border-top: 1px solid var(--o-line);
}
/* The first task follows the card's own subtitle, which is already a
   separator; a second rule there reads as a mistake. */
.ofc-card > .ofc-sub + .ofc-task,
.ofc-card > .ofc-state + .ofc-task,
.ofc-card > .ofc-card__bar + .ofc-task { border-top: 0; padding-top: 0; margin-top: 0.4rem; }

.ofc-task__h {
  margin: 0;
  font-family: var(--display);
  font-size: var(--o-t-md);
  font-weight: 700;
  letter-spacing: -0.005em;
}
.ofc-task__why {
  margin: -0.45rem 0 0;
  max-width: 64ch;
  color: var(--o-faint);
  font-size: var(--o-t-sm);
  font-weight: 500;
}

.ofc-fields {
  display: grid;
  /* A field is as wide as its content needs, not as wide as the card: a single
     "Crew A" box stretched across 1300px was one of the things that read as
     unfinished. */
  grid-template-columns: repeat(auto-fit, minmax(11rem, 18rem));
  gap: 0.65rem 0.8rem;
}
.ofc-fields label {
  display: grid;
  gap: 0.25rem;
  font-size: var(--o-t-xs);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--o-faint);
}
/* The pairs that genuinely need the room keep it — a customer-and-property
   line truncated into a half-width control is how somebody picks the wrong
   one. */
.ofc-field--wide { grid-column: 1 / -1; max-width: 38rem; }

.ofc-fields input,
.ofc-fields select {
  min-height: 34px;
  padding: 0 0.6rem;
  border: 1px solid var(--o-line-2);
  border-radius: var(--o-r-sm);
  /* An input is a WELL: darker than the card behind it. A pale raised box on a
     dark ground reads as a button. */
  background: var(--o-inset);
  color: var(--o-text);
  font: 500 var(--o-t-sm)/1 var(--body);
  text-transform: none;
  letter-spacing: normal;
}
/* THREE INK STEPS, ALL MEASURED ON BLACK — so a placeholder is `--o-faint`
   and not an alpha wash. `rgba(237,241,239,0.28)` resolves over black to
   #424343, which is 2.12:1: a fourth step, invented, and less legible than the
   dimmest sanctioned one at 4.58:1. */
.ofc-fields input::placeholder { color: var(--o-faint); }
/* A SELECT SIZES TO ITS WIDEST OPTION, AND THAT PUSHED THE PAGE SIDEWAYS.
   Found at 390px the first time this screen was ever rendered that narrow:
   "INV-2026-0007 — Vance — rear yard, phase 1 — $14,400 owing" made the
   control 437px in a 390px viewport, and the whole document scrolled
   horizontally. `min-width: 0` is the half people forget — a grid item's
   default `min-width: auto` refuses to shrink below its content, so
   `max-width: 100%` alone does nothing here.

   The option text is not shortened: a payment posted against the wrong invoice
   is worse than a truncated line, so the control keeps the whole label and
   clips it. */
.ofc-fields select {
  padding-right: 0.4rem;
  min-width: 0;
  max-width: 100%;
  text-overflow: ellipsis;
}
.ofc-fields label { min-width: 0; }
.ofc-fields select:disabled { opacity: 0.55; }
.ofc-fields input:focus,
.ofc-fields select:focus {
  outline: none;
  border-color: rgba(154, 203, 178, 0.65);
  box-shadow: 0 0 0 3px rgba(154, 203, 178, 0.18);
}

.ofc-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }

/* A form's own status line, everywhere rather than per-id. #nc-msg had this
   and the five forms added after it did not. */
.ofc-task p[role="status"] {
  margin: 0;
  min-height: 1.25em;
  font-size: var(--o-t-sm);
  font-weight: 550;
  color: var(--o-dim);
}
.ofc-task p[role="status"].is-warn { color: var(--o-warn-ink); }

.ofc-filter {
  min-height: 30px;
  min-width: 15rem;
  padding: 0 0.6rem;
  border: 1px solid var(--o-line-2);
  border-radius: var(--o-r-sm);
  background: var(--o-inset);
  color: var(--o-text);
  font: 500 var(--o-t-sm)/1 var(--body);
}
.ofc-filter::placeholder { color: var(--o-faint); }
.ofc-filter:focus {
  outline: none;
  border-color: rgba(154, 203, 178, 0.65);
  box-shadow: 0 0 0 3px rgba(154, 203, 178, 0.16);
}

/* ==========================================================================
   11. BANNERS AND THE PALETTE
   ========================================================================== */

#ofc-boot, #ofc-stale {
  margin: 0 0 0.9rem;
  padding: 0.7rem 0.9rem;
  border-radius: var(--o-r);
  background: var(--o-warn-bg);
  border-left: 3px solid var(--o-warn);
  color: var(--o-warn-ink);
  font-size: var(--o-t-sm);
  font-weight: 600;
}
#ofc-stale {
  background: var(--o-inset);
  border-left-color: var(--o-line-2);
  color: var(--o-dim);
}

.ofc-palette {
  width: min(32rem, 92vw);
  margin: 12vh auto auto;
  padding: 0.45rem;
  border: 1px solid var(--o-line-2);
  border-radius: var(--o-r-lg);
  /* a dialog sits above every surface it covers */
  background: var(--o-raised);
  color: var(--o-text);
}
/* ONE RULE. There were two of these on consecutive lines, so the first was
   dead the moment the second was written — and the second used rgb(10,14,18),
   a near-black sitting 0.0003 in luminance from `--o-inset`. A fourth
   near-black nobody can see is still a fourth value to keep in step. It is a
   token now, for the same reason. */
.ofc-palette::backdrop { background: var(--o-scrim); }
.ofc-palette input {
  width: 100%;
  min-height: 38px;
  padding: 0 0.7rem;
  border: 1px solid var(--o-line-2);
  border-radius: var(--o-r-sm);
  background: var(--o-inset);
  color: var(--o-text);
  font: 500 var(--o-t-md)/1 var(--body);
}
.ofc-palette input:focus { outline: none; border-color: rgba(154, 203, 178, 0.65); }
.ofc-palette ul { list-style: none; margin: 0.4rem 0 0; padding: 0; display: grid; gap: 0.1rem; }
.ofc-palette li {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  padding: 0.45rem 0.6rem;
  border-radius: var(--o-r-sm);
  font-size: var(--o-t-sm);
  font-weight: 650;
  cursor: pointer;
}
.ofc-palette li.is-on { background: var(--o-press); }
.ofc-palette li em { font-style: normal; color: var(--o-faint); font-weight: 500; }

.ofc-view[hidden] { display: none; }

/* ==========================================================================
   12. NARROW

   Below 62rem the rail stops being a rail. It becomes a scrolling strip under
   the header, the frame stops owning the viewport height, and the page scrolls
   the way a page does — because a 15rem fixed column on a 380px screen is
   40% of the screen spent on navigation.
   ========================================================================== */

@media (max-width: 62rem) {
  #office {
    display: block;
    height: auto;
    overflow: visible;
  }
  .ofc-main { display: block; overflow: visible; }
  .ofc-scroll { overflow: visible; padding: 0.9rem 0.9rem 3rem; }

  .ofc-rail {
    display: block;
    border-right: 0;
    border-bottom: 1px solid var(--o-line);
  }
  /* THE STRIP SCROLLS AND NOTHING SAID SO. Measured at 390px: 1028px of nav
     inside a 390px window with the scrollbar hidden, so three of the six work
     sections were visible and Clients, Estimates, Contracts, Database and
     Settings sat off the right edge with no arrow and no fade. Half the
     application was unreachable on a phone and looked complete.
  
     The fade is the device the CRM board already uses, and for the same
     reason: it is information, so it is removed at the end of the scroll
     rather than sitting there promising content that is not there. */
  .ofc-rail__nav {
    display: flex;
    gap: 0.3rem;
    overflow-x: auto;
    padding: 0.5rem 0.7rem;
    scrollbar-width: none;
    scroll-snap-type: x proximity;
    overscroll-behavior-x: contain;
  }
  .ofc-rail__nav::-webkit-scrollbar { display: none; }
  .ofc-rail__nav.is-overflowing {
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 2.5rem), transparent);
    mask-image: linear-gradient(to right, #000 calc(100% - 2.5rem), transparent);
  }
  .ofc-rail__nav.is-overflowing.is-scroll-end {
    -webkit-mask-image: none;
    mask-image: none;
  }
  .ofc-side__a { scroll-snap-align: start; }
  
  /* 44px, NOT 35. This is the width at which the rail becomes a thumb
     target, on the app a foreman opens on site in gloves, on a strip where
     a mis-tap lands on the wrong section. The desktop rail is pointer-driven
     and keeps its tighter rhythm. */
  .ofc-side__a,
  .ofc-side__g--soon { min-height: 44px; }
  .ofc-side__g { display: none; }
  /* EXCEPT THE ONE THAT DIVIDES FINISHED FROM UNFINISHED. The rail becomes a
     horizontal scroller here and every group label is dropped, which at 390px
     left Mailbox and Warranty sitting in one undifferentiated row — the exact
     confusion the grouping was added to remove. This one stays, inline, as the
     marker you scroll past. */
  .ofc-side__g--soon {
    display: inline-flex;
    align-items: center;
    margin: 0 0.15rem;
    padding: 0 0.6rem 0 0.75rem;
    border-top: 0;
    border-left: 1px solid var(--o-line);
    white-space: nowrap;
    flex: none;
  }
  .ofc-side__a { flex: none; white-space: nowrap; }
  .ofc-side__a.is-on { background: var(--o-press); }
  .ofc-rail__foot { display: none; }

  .ofc-top { position: sticky; top: 0; z-index: 20; }
  .ofc-jump { display: none; }

  .ofc-var__row, .ofc-var__head { grid-template-columns: minmax(6rem, 1.2fr) minmax(6rem, 1.6fr) 3.6rem; }
}

/* The sign-out in the rail foot is the one on wide screens; the header carries
   the other, for when the foot is hidden. */
/* THE CHROME'S SIGN OUT, AND WHO ELSE IS OFFERING ONE.
 *
 * Above 62rem the rail is a column with its own Sign out in the foot, so the
 * top-bar one is redundant and hidden. Below that the rail collapses to a
 * strip with no foot, so the top-bar one is the only chrome copy and stays.
 *
 * EXCEPT ON SETTINGS, which offers Sign out inside its own card because that
 * is what the card is for — "this session, and what this screen cannot tell
 * you". At phone width that made TWO identical pills visible a few hundred
 * pixels apart, which is the same defect this project already shipped once
 * when `#office .ofc-ghost` beat `.ofc-top__signout { display: none }` on
 * specificity. Found this time by sweeping every section at both widths and
 * counting visible controls by label, not by looking at a screenshot — two
 * pills that far apart do not read as a duplicate until you count them.
 *
 * The desktop pair — rail foot and settings card — is left alone. They are
 * persistent chrome and page content, at opposite corners, which is the
 * ordinary shape of a settings page. */
@media (min-width: 62.0625rem) { #office .ofc-top__signout { display: none; } }
#office[data-view="settings"] .ofc-top__signout { display: none; }

@media (prefers-reduced-motion: reduce) {
  :is(#office, #owner, #portal) *,
  :is(#office, #owner, #portal) *::before,
  :is(#office, #owner, #portal) *::after { transition: none !important; }
}

/* ==========================================================================
   13. THE OWNER SCREEN AND THE CUSTOMER PORTAL

   Both `<link>` this stylesheet and build themselves out of its classes. The
   rewrite above gave the office a rail, a header and a router; these two have
   none of that and should not — the owner screen is one long read, the portal
   is one customer looking at one project. What they share is the MATERIAL:
   the same glass, the same ink, the same controls, the same density.

   Every class below is one those two pages were already using and the rewrite
   dropped. They are restored as the new design, not as the old one.
   ========================================================================== */

/* ---- the page header ----
   A masthead is wrong for the office, which is why it became a rail — but it
   is right here: neither page has sections to move between, so the header's
   whole job is to say whose screen this is and offer the way out.

   ONE HAIRLINE UNDER IT, nothing else — the same rule the office's top bar
   gets. A masthead that is also a floating panel is two ideas. */
.ofc-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  padding: 0.9rem 1.1rem;
  margin-bottom: 1.1rem;
  padding-inline: 0;
  border-bottom: 1px solid var(--o-line-2);
}

.ofc-head__id { display: flex; align-items: center; gap: 0.75rem; min-width: 0; }
.ofc-head h1 {
  margin: 0;
  font-family: var(--display);
  font-size: var(--o-t-title);
  font-weight: 750;
  /* Tighter at 28px than it was at 20px: tracking that reads as confident on a
     small heading reads as loose on a large one. */
  letter-spacing: -0.022em;
  line-height: 1.1;
}
.ofc-head__id p {
  margin: 0.1rem 0 0;
  color: var(--o-faint);
  font-size: var(--o-t-sm);
  font-weight: 500;
}

/* The header's own links and buttons. `.ofc-nav` was the name before the
   office grew a rail, and it stays the name here — a class is an API, and
   these two pages are its callers. */
.ofc-nav { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.ofc-who { color: var(--o-faint); font-size: var(--o-t-sm); font-weight: 500; }

/* The header logo is the one place a 40px mark is right — it is the only
   brand element on the page, where the rail's 30px sits above a wordmark. */
.ofc-head .ofc-logo { width: 38px; height: 38px; border-radius: 10px; }

/* ---- the lede band ----
   One number and the decision it implies, at the top of the screen: lien
   clocks needing an answer on the owner screen, the project's stage on the
   portal. The office's equivalent is `.ofc-stat--hero` and they are
   deliberately the same figure size — it is the same idea. */
.ofc-lede {
  display: flex;
  align-items: center;
  gap: 1.1rem;
  padding: 0.95rem 1.2rem;
  margin-bottom: 1.1rem;
  border-radius: var(--o-r-lg);
  border: 1px solid var(--o-line-2);
  /* The one place a colour edge earns its keep: this band exists to say
     something needs a decision today. One 2px rule, not a tinted panel. */
  border-left: 2px solid var(--o-warn);
}

.ofc-lede__n {
  margin: 0;
  font-family: var(--display);
  font-size: var(--o-t-hero);
  font-weight: 750;
  line-height: 0.95;
  letter-spacing: -0.03em;
  /* Proportional figures: tabular gives every digit the width of a zero, which
     at 52px makes a number like 121 read loose. Tabular is for COLUMNS. */
  color: var(--o-warn-ink);
}
.ofc-lede__label { margin: 0; font-size: var(--o-t-lg); font-weight: 700; color: var(--o-text); }
.ofc-lede__sub { margin: 0.15rem 0 0; color: var(--o-faint); font-size: var(--o-t-sm); font-weight: 500; }

/* CLEAR IS ITS OWN STATE AND IS SAID, not left as an absence — and it is worth
   a line rather than a headline, because "nothing needs you" is good news and
   good news does not need 52 pixels. */
.ofc-lede.is-clear {
  padding: 0.6rem 1rem;
  border-left-color: var(--o-accent);
}
.ofc-lede.is-clear .ofc-lede__n { font-size: var(--o-t-lg); color: var(--o-accent); }
.ofc-lede.is-clear .ofc-lede__label { font-size: var(--o-t-sm); font-weight: 650; }

/* ---- the tally ----
   A TALLY SHOWS WHAT EXISTS; the zeros go in a sentence underneath. Seventeen
   equal tiles, fifteen of them reading 0, is what the owner called amateur on
   the office dashboard — the office answered it with a stacked bar, and these
   two pages answer it with tiles that size to their own labels. A tile is only
   here because there is something in it. */
.ofc-tally {
  list-style: none;
  margin: 0 0 0.75rem;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}
.ofc-tally li {
  display: flex;
  align-items: baseline;
  gap: 0.45rem;
  padding: 0.35rem 0.65rem;
  border-radius: var(--o-r-sm);
  background: var(--o-inset);
  border: 1px solid var(--o-line);
}
/* The number and its name on one line, so a long stage name simply makes a
   longer tile instead of overrunning its neighbour. */
.ofc-tally b {
  font-family: var(--display);
  font-size: var(--o-t-lg);
  font-weight: 750;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--o-text);
}
.ofc-tally li span {
  font-size: var(--o-t-xs);
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--o-faint);
  white-space: nowrap;
}
/* The zeros, as one quiet line rather than fifteen loud boxes. */
.ofc-tally__none {
  flex: 1 1 100%;
  padding: 0.2rem 0 0;
  background: none;
  box-shadow: none;
  color: var(--o-faint);
  font-size: var(--o-t-sm);
  font-weight: 500;
}
.ofc-tally li.is-zero { opacity: 0.45; }

/* ---- a plain stacked list ----
   The owner's released-waiver rows and the portal's invoices and permits: a
   line of `<b><span><em>`, read down. Not the office's `.ofc-feed`, which is a
   three-column row built around a monogram. */
.ofc-list { list-style: none; margin: 0.6rem 0 0; padding: 0; display: grid; gap: 0.35rem; }
.ofc-list li {
  display: grid;
  gap: 0.1rem;
  padding: 0.5rem 0.7rem;
  border-radius: var(--o-r-sm);
  background: var(--o-inset);
  border: 1px solid var(--o-line);
  border-left: 2px solid var(--o-accent);
}
.ofc-list b { font-size: var(--o-t-sm); font-weight: 700; color: var(--o-text); }
.ofc-list span { font-size: var(--o-t-sm); font-weight: 500; color: var(--o-dim); }
.ofc-list em {
  font-style: normal;
  font-size: var(--o-t-xs);
  font-weight: 650;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--o-faint);
}

/* ---- the full-width card ----
   `--wide` is what owner.html and portal.html say; the office says `--half`,
   `--third` and `--twothirds`. Both spellings land on the same 12-column grid. */
.ofc-card--wide { grid-column: 1 / -1; }

/* The owner screen is a single column of wide cards on any screen that can
   hold two, so its grid does not need the office's twelve tracks — but it uses
   the same class, so the tracks are what it gets, and `--wide` is what makes
   that correct rather than accidental. */
#owner .ofc-grid, #portal .ofc-grid { gap: 0.9rem; }


/* ==========================================================================
   14. PRICING AN ESTIMATE  (roadmap phase 1)

   The catalogue measured 0% reachable — the only area in the schema with no
   reader at all — and that one gap is why every estimate in this system is
   unpriced. Nothing below needed a migration: 0004 granted the office insert
   and update on estimate_lines the day it created the table.
   ========================================================================== */

/* The totals strip. Four figures across the top of the pane, divided by the
   gap over a line-coloured ground — the same instrument treatment as the
   dashboard's stat row, at a smaller size. */
.ofc-tot {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
  gap: 1px;
  margin: 0.85rem 0;
  background: var(--o-line-2);
  border: 1px solid var(--o-line-2);
  border-radius: var(--o-r);
  overflow: hidden;
}
.ofc-tot > div { background: var(--o-panel); padding: 0.6rem 0.75rem; }
.ofc-tot dt {
  margin: 0 0 0.15rem;
  font-size: var(--o-t-xs);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--o-faint);
}
.ofc-tot dd {
  margin: 0;
  font-family: var(--display);
  font-size: var(--o-t-xl);
  font-weight: 750;
  letter-spacing: -0.02em;
  line-height: 1.05;
  color: var(--o-text);
}
.ofc-tot dd small {
  display: block;
  margin-top: 0.1rem;
  font-family: var(--body);
  font-size: var(--o-t-xs);
  font-weight: 500;
  letter-spacing: 0;
  color: var(--o-faint);
}
/* A margin that has gone negative is not a small number, it is a different
   fact. It is the one figure here allowed a colour. */
.ofc-tot dd.is-loss { color: var(--o-warn-ink); }

/* ---- the lines ---- */
.ofc-lines { width: 100%; border-collapse: collapse; font-size: var(--o-t-sm); }
.ofc-lines th {
  position: sticky;
  top: 0;
  z-index: 1;
  text-align: left;
  padding: 0.35rem 0.5rem;
  /* same, on the estimate line table */
  background: var(--o-panel);
  border-bottom: 1px solid var(--o-line-2);
  color: var(--o-faint);
  font-size: var(--o-t-xs);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
}
.ofc-lines td {
  padding: 0.35rem 0.5rem;
  border-bottom: 1px solid var(--o-line);
  color: var(--o-dim);
  font-weight: 500;
  vertical-align: top;
}
.ofc-lines tbody tr:hover td { background: var(--o-hover); }
.ofc-lines td:first-child { color: var(--o-text); font-weight: 600; }
.ofc-lines td.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
/* A LINE NOBODY HAS PRICED IS NOT A LINE WORTH ZERO, and it must not read as
   one — the same rule the variance report follows with unpaired clock events. */
.ofc-lines td.is-unpriced { color: var(--o-warn-ink); font-style: italic; }
.ofc-lines tr.is-optional td:first-child::after {
  content: 'optional';
  display: inline-block;
  margin-left: 0.45rem;
  padding: 0.02rem 0.35rem;
  border-radius: var(--o-pill);
  background: var(--o-inset);
  color: var(--o-faint);
  font-size: var(--o-t-xs);
  font-weight: 650;
  font-style: normal;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.ofc-lines td.act { width: 1%; text-align: right; }

/* ---- the catalogue picker ---- */
.ofc-pick { display: grid; gap: 0.4rem; margin-top: 0.5rem; }
.ofc-pick__results {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: 14rem;
  overflow-y: auto;
  display: grid;
  gap: 1px;
  scrollbar-color: var(--o-line-2) transparent;
}
.ofc-pick__results li { display: grid; }
.ofc-pick__b {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 0.2rem 0.75rem;
  width: 100%;
  min-height: 0;
  padding: 0.4rem 0.55rem;
  border: 0;
  border-radius: var(--o-r-sm);
  background: transparent;
  background-image: none;
  box-shadow: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.ofc-pick__b:hover { background: var(--o-hover); }
.ofc-pick__b:active { transform: none; }
.ofc-pick__b b { font-size: var(--o-t-sm); font-weight: 650; color: var(--o-text); }
.ofc-pick__b em {
  grid-column: 1 / -1;
  font-style: normal;
  font-size: var(--o-t-xs);
  font-weight: 500;
  color: var(--o-faint);
}
.ofc-pick__b span {
  font-size: var(--o-t-sm);
  font-weight: 650;
  font-variant-numeric: tabular-nums;
  color: var(--o-dim);
  white-space: nowrap;
}
/* An item with no cost on file is the one thing in this list that must not
   look like a price of nothing. */
.ofc-pick__b span.is-unpriced { color: var(--o-warn-ink); font-weight: 500; }

/* ---- the estimate row in the list ---- */
.ofc-est__n { font-size: var(--o-t-sm); font-weight: 700; color: var(--o-text); font-variant-numeric: tabular-nums; }
.ofc-est__who {
  grid-column: 1 / -1;
  font-size: var(--o-t-sm);
  font-weight: 500;
  color: var(--o-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ofc-est__meta {
  grid-column: 1 / -1;
  margin-top: 0.15rem;
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: var(--o-t-xs);
  font-weight: 650;
  color: var(--o-faint);
}

/* The pane's own header: what this document is, and what state it is in. */
.ofc-est__h { margin: 0 0 0.1rem; font-family: var(--display); font-size: var(--o-t-xl); font-weight: 750; letter-spacing: -0.018em; }
.ofc-est__sub { margin: 0 0 0.5rem; font-size: var(--o-t-sm); color: var(--o-faint); }
.ofc-est__bar { display: flex; align-items: center; flex-wrap: wrap; gap: 0.4rem; margin-bottom: 0.5rem; }

/* The row of things that can happen to this estimate version next. Spaced off
   the table above it, because "Send" is the one irreversible control on this
   screen and it must not sit flush against the last line of data. */
.ofc-actions--next { margin-top: 0.8rem; }

/* ---- the change-order chain --------------------------------------------
   A change order is an AMENDMENT, so it is drawn as a block hanging off the
   contract rather than a table row: it carries prose (what changed), money and
   time, and a row would flatten the first into a cell nobody reads. */
.ofc-co {
  display: grid;
  gap: 0.3rem;
  margin: 0.5rem 0 0;
  padding: 0.7rem 0.85rem;
  border: 1px solid var(--o-line-2);
  border-radius: var(--o-r);
}
.ofc-co__h { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.ofc-co__h b { font-size: var(--o-t-sm); font-weight: 750; color: var(--o-text); }
.ofc-co__n {
  margin-left: auto;
  font-family: var(--display);
  font-size: var(--o-t-lg);
  font-weight: 750;
  font-variant-numeric: tabular-nums;
  color: var(--o-text);
}
.ofc-co__scope { margin: 0; font-size: var(--o-t-sm); color: var(--o-dim); line-height: 1.5; }
.ofc-co__meta {
  margin: 0;
  font-size: var(--o-t-xs);
  font-weight: 650;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--o-faint);
}
.ofc-co .ofc-actions { margin-top: 0.3rem; }

/* A SENTENCE inside a roster row, not a label.
   `.ofc-co__meta` uppercases, which is right for "CSLB LICENCE IS EXPIRED" and
   wrong for the two paragraphs the Subcontracts card has to say — a statute
   explained in twenty-six words of block capitals is a statute nobody finishes
   reading, which defeats the point of saying it before the database does.
   Same faint ink, same size; sentence case and normal tracking. */
.ofc-co__why {
  display: block;
  margin: 0.25rem 0 0;
  font-size: var(--o-t-xs);
  line-height: 1.45;
  color: var(--o-faint);
  text-wrap: pretty;
}

/* One date, so one column — the change-order card is already narrow and the
   default two-column field grid would leave the box stranded beside a gap. */
.ofc-co__start {
  grid-template-columns: minmax(0, 12rem);
  margin-top: 0.6rem;
}

/* THE UNSIGNED CASE IS NOT AN OBSERVATION. Faint ink is right for "signed
   first, which is the order the statute requires" and wrong for "nothing may
   start on this — work before the signature cannot be billed", which is money
   the owner will not recover. Same rule `.ofc-note.is-warn` follows, and the
   same tokens, because a second warning colour is how a palette stops meaning
   anything. */
.ofc-co__why.is-warn {
  padding: 0.55rem 0.75rem;
  margin-top: 0.5rem;
  border-radius: var(--o-r-sm);
  background: var(--o-warn-bg);
  border-left: 2px solid var(--o-warn);
  color: var(--o-warn-ink);
}

/* A qualifier under a figure in a table cell — "$450 disputed" beneath the
   amount it is part of. Without `display:block` it ran straight on from the
   number and read as one value: "$3,100$450 disputed".
 *
 * IT WRAPS. This carried `white-space: nowrap`, which is right for a two-word
 * qualifier under a number and catastrophic for a sentence: the lien-waiver
 * exposure column put one explanation on one line, forced the cell to 941px,
 * the table to 1463px inside an 1100px card, and clipped every explanation
 * mid-word — "if that money never ar…". Several cells already carried
 * sentences and were one narrower card away from the same thing.
 *
 * Numbers still do not wrap: `.ofc-lines td.num` sets nowrap on the cell
 * itself, which is where it belongs — a figure broken across two lines is
 * unreadable, a sentence broken across two lines is a sentence. */
.ofc-lines td small {
  display: block;
  margin-top: 0.1rem;
  font-size: var(--o-t-xs);
  font-weight: 500;
  color: var(--o-faint);
  line-height: 1.35;
  /* Long enough for a clause, short enough that the column cannot run away. */
  max-width: 34rem;
  overflow-wrap: anywhere;
}
/* A qualifier under a FIGURE is short by nature and reads better on one line. */
.ofc-lines td.num small { white-space: nowrap; max-width: none; }

/* UNKNOWN IS A THIRD STATE, and it must look like neither of the other two.
   A refused read wearing the warn colour says "people are waiting"; wearing
   the clear colour says "nobody is". Both are claims the screen cannot make,
   so the figure goes to the muted ink and the dot goes grey. */
.ofc-stat--hero.is-unknown .ofc-stat__n { color: var(--o-faint); }
.ofc-stat--hero.is-unknown .ofc-stat__k { color: var(--o-faint); }
.ofc-stat--hero.is-unknown .ofc-stat__k::before { background: var(--o-neutral); }

/* An expansion header: the row the CUSTOMER sees, above the components the
   office sees. Not a heavier row — a quieter one, because the components under
   it are the data and this is a label for them. */
.ofc-lines__group td {
  padding-top: 0.7rem;
  border-bottom: 0;
  color: var(--o-text);
  font-weight: 700;
}
.ofc-lines__group td small {
  display: block;
  margin-top: 0.1rem;
  font-size: var(--o-t-xs);
  font-weight: 500;
  color: var(--o-faint);
}
/* Its components are indented so the recipe reads as belonging to it. */
.ofc-lines tr.is-expansion td:first-child { padding-left: 1.4rem; color: var(--o-dim); }

/* ==========================================================================
   15. KEEPING THE CATALOGUE CURRENT

   A CONTROL INSIDE A TABLE CELL IS A DIFFERENT PROBLEM FROM A CONTROL IN A
   FORM. The form control has a label above it, a whole row of width, and one
   job. This one sits in a column beside six others, is repeated once per row,
   and must not make the table wider than the screen — so it is sized to the
   number it holds rather than to a comfortable minimum.
   ========================================================================== */

.ofc-mini {
  min-height: 26px;
  padding: 0.15rem 0.45rem;
  border: 1px solid var(--o-line-2);
  border-radius: var(--o-r-sm);
  background: var(--o-inset);
  color: var(--o-text);
  font: 600 var(--o-t-xs)/1.2 var(--body);
}
/* SIZED TO THE FIGURE, not to a default input width. A text input defaults to
   about twenty characters, and seven of those across one row is a table that
   scrolls sideways on a laptop for no reason — the widest thing any of these
   holds is `12345.67`. */
input.ofc-mini { width: 6.5rem; max-width: 100%; }
input.ofc-mini.num { text-align: right; font-variant-numeric: tabular-nums; }
button.ofc-mini {
  display: block;
  margin-top: 0.25rem;
  border-radius: var(--o-pill);
  cursor: pointer;
}
button.ofc-mini:hover { background: var(--o-hover); color: var(--o-text); }
button.ofc-mini:disabled { opacity: 0.5; cursor: default; }
.ofc-mini:focus-visible {
  outline: none;
  border-color: rgba(154, 203, 178, 0.65);
  box-shadow: 0 0 0 3px rgba(154, 203, 178, 0.16);
}

/* RETIRED IS NOT DELETED, and it must not read as an error either. The row is
   still true — it is what a past estimate was priced from — so it recedes
   rather than going amber. Amber is reserved for a number costing money now. */
.ofc-lines tr.is-retired td { color: var(--o-faint); }
.ofc-lines tr.is-retired td:first-child { color: var(--o-dim); font-weight: 500; }

/* The standalone version of the checkbox row. `.ofc-fields label.ofc-check`
   only applies inside a field grid; this one sits alone under a card bar. */
label.ofc-check--loose {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0 0 0.6rem;
  font-size: var(--o-t-sm);
  font-weight: 600;
  color: var(--o-dim);
  cursor: pointer;
}
label.ofc-check--loose input {
  width: 1.05rem;
  height: 1.05rem;
  accent-color: var(--o-accent);
  flex: 0 0 auto;
}

/* A RETIRED RECIPE IS STILL TRUE — it is what past estimates were expanded
   from — so it recedes rather than going amber, the same rule the retired
   catalogue row follows. */
.ofc-roster li.is-retired { color: var(--o-faint); }
.ofc-roster li.is-retired > .ofc-co__meta { color: var(--o-faint); }


/* A DEAD CONTROL HAS TO LOOK DEAD. Choosing "lost" disables the revisit date,
   and the box was styled identically to a live one — so it read as a field that
   simply would not open, which is indistinguishable from a broken page. */
.ofc-fields input:disabled,
.ofc-fields select:disabled,
.ofc-fields textarea:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* ---- the CRM board card, which carried a name and an age and nothing else -- */
.ofc-board__what {
  display: block;
  margin-top: 0.3rem;
  font-size: var(--o-t-xs);
  line-height: 1.4;
  color: var(--o-dim);
}
.ofc-board__reach {
  margin: 0.35rem 0 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.15rem 0.6rem;
  font-size: var(--o-t-xs);
}
.ofc-board__reach a { color: var(--o-text); text-decoration: none; border-bottom: 1px solid var(--o-line-2); }
.ofc-board__reach a:hover { border-bottom-color: var(--o-accent-deep); }

/* ---- the outcome, which the board never drew ------------------------------
   MONEY IS THE LOUDEST THING ON A WON CARD, because it is the only figure on
   this screen that is revenue. It gets weight and the full text colour, which
   nothing else on the card has — the name is bold, but this is bold AND
   bright, and there is exactly one per card so it cannot become noise. */
.ofc-board__money {
  display: block;
  margin-top: 0.4rem;
  font-size: var(--o-t-sm);
  font-weight: 650;
  font-variant-numeric: tabular-nums;
  color: var(--o-text);
}
/* AND THE ABSENCE OF IT IS A DIFFERENT THING FROM A SMALL NUMBER. Drawn as a
   prompt rather than as a figure: no tabular numerals, no weight, and the
   dimmed colour the rest of the card's secondary text uses, so the eye reads
   it as "still to do" rather than misreading it as an amount. */
.ofc-board__money--none {
  font-weight: 400;
  font-style: italic;
  color: var(--o-dim);
}
.ofc-board__why {
  display: block;
  margin-top: 0.4rem;
  font-size: var(--o-t-xs);
  color: var(--o-dim);
}
/* The lost reason reads as a label rather than as more description, so it is
   set apart from `__what` above it by a rule rather than by size alone. */
.ofc-board__why::before {
  content: "Lost: ";
  color: var(--o-dim-2, var(--o-dim));
}
/* THE COLUMN TOTAL sits under the heading and above the cards, so it is read
   before the column rather than discovered after scrolling it. */
.ofc-board__sum {
  margin: 0 0 0.5rem;
  font-size: var(--o-t-xs);
  font-variant-numeric: tabular-nums;
  color: var(--o-text);
}
.ofc-board__sum .ofc-dim { font-variant-numeric: normal; }
/* "AT LEAST" IS A QUALIFIER, NOT PART OF THE FIGURE, so it takes the dim step
   and leaves the money the brightest thing on the line. It sits INSIDE the
   total rather than in a note underneath on purpose: the truncation is already
   stated on the card's state line, and that is exactly where it failed to be
   read — a caveat one element away from the number it qualifies is a caveat
   nobody connects to it. `--o-dim` rather than `--o-dim-2`, which is not
   defined anywhere and only ever resolves to its fallback. */
.ofc-board__atleast {
  font-variant-numeric: normal;
  color: var(--o-dim);
}

/* PARKED IS QUIET, DUE IS NOT. A revisit date in the future is a note; one that
   has arrived is the enquiry asking to be picked up today, which is the single
   thing this board exists to surface. */
.ofc-board__revisit {
  display: block;
  margin-top: 0.35rem;
  font-size: var(--o-t-xs);
  color: var(--o-faint);
}
.ofc-board__revisit.is-due { color: var(--o-warn-ink); font-weight: 650; }
.ofc-board__card.is-due { border-left: 2px solid var(--o-warn); }


/* WHAT THE PICKER IS NOT SHOWING. Twenty results out of what was found, out of
   what the catalogue holds — a list that shows some of the rows must say so,
   and the same rule the tables follow applies to a dropdown of results. */
.ofc-pick__more {
  padding: 0.4rem 0.6rem;
  font-size: var(--o-t-xs);
  color: var(--o-faint);
  border-top: 1px solid var(--o-line);
}


/* AN EDITABLE FIGURE IN A TABLE CELL. It has to look typeable without turning
   a dense table into a form: no filled well, just the hairline underneath that
   says the number can be touched. `text-align: right` because it sits in a
   numeric column and a right-aligned column with one left-aligned input in it
   reads as a misprint. */
.ofc-inline--num {
  width: 100%;
  min-width: 3.5rem;
  max-width: 6rem;
  padding: 0.15rem 0.25rem;
  text-align: right;
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--o-line-2);
  border-radius: 0;
  color: var(--o-text);
  font: 500 var(--o-t-sm)/1.3 var(--body);
}
.ofc-inline--num::placeholder { color: var(--o-faint); }
.ofc-inline--num:hover { border-bottom-color: var(--o-line-3, var(--o-line-2)); }
.ofc-inline--num:focus {
  outline: none;
  border-bottom-color: var(--o-accent-deep);
  background: var(--o-inset);
}
.ofc-inline--num:disabled { opacity: 0.5; }


/* ---- the mailbox addresses -----------------------------------------------
   A row of what mail has actually arrived for. Pills, like every other control
   on this screen, and the count carries the weight rather than a colour: an
   address with something waiting says so in words. */
.ofc-addr {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin: 0.2rem 0 0.9rem;
}
.ofc-addr__b {
  display: inline-flex;
  align-items: baseline;
  gap: 0.45rem;
  padding: 0.35rem 0.75rem;
  border: 1px solid var(--o-line-2);
  border-radius: 999px;
  background: transparent;
  color: var(--o-dim);
  font: 600 var(--o-t-xs)/1.4 var(--body);
  cursor: pointer;
}
.ofc-addr__b:hover { border-color: var(--o-accent-deep); color: var(--o-text); }
.ofc-addr__b.is-on {
  background: var(--o-press);
  border-color: var(--o-line-2);
  color: var(--o-text);
}
.ofc-addr__t { letter-spacing: 0.02em; }
.ofc-addr__n { color: var(--o-faint); font-weight: 500; }
.ofc-addr__n.is-unread { color: var(--o-warn-ink); font-weight: 650; }


/* ---- one enquiry, opened -------------------------------------------------
   The board card is now a control, so it has to say so on hover and focus. */
.ofc-board__card[role="button"] { cursor: pointer; }
.ofc-board__card[role="button"]:hover { border-color: var(--o-accent-deep); }
.ofc-board__card[role="button"]:focus-visible {
  outline: 2px solid var(--o-accent-deep);
  outline-offset: 2px;
}
.ofc-board__card.is-on { background: var(--o-press); }

.ofc-lead__h { margin: 0 0 0.2rem; font-size: var(--o-t-lg); }
.ofc-lead__scope {
  margin: 0 0 0.9rem;
  color: var(--o-dim);
  line-height: 1.5;
  text-wrap: pretty;
}
.ofc-lead__sub {
  margin: 1.1rem 0 0.35rem;
  font-size: var(--o-t-xs);
  font-weight: 750;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--o-faint);
}
.ofc-lead__reach {
  display: flex;
  flex-wrap: wrap;
  gap: 0.2rem 0.9rem;
  margin: 0.6rem 0 0;
  font-size: var(--o-t-sm);
}
.ofc-lead__reach a { color: var(--o-text); text-decoration: none; border-bottom: 1px solid var(--o-line-2); }
.ofc-lead__reach a:hover { border-bottom-color: var(--o-accent-deep); }

/* WHAT SOMEBODY WROTE, in their own words — so it is set apart from the
   screen's own sentences rather than reading as more chrome. */
.ofc-lead__notes {
  margin: 0;
  padding: 0.6rem 0.8rem;
  border-left: 2px solid var(--o-line-2);
  background: var(--o-inset);
  border-radius: 0 var(--o-r-sm) var(--o-r-sm) 0;
  color: var(--o-text);
  line-height: 1.55;
  white-space: pre-wrap;
  text-wrap: pretty;
}

.ofc-journey { margin: 0.5rem 0 0; padding: 0; list-style: none; }
.ofc-journey li {
  display: flex;
  flex-wrap: wrap;
  gap: 0.15rem 0.6rem;
  align-items: baseline;
  padding: 0.4rem 0;
  border-top: 1px solid var(--o-line);
  font-size: var(--o-t-xs);
}
.ofc-journey__meta { color: var(--o-faint); }


/* ---- "Other" as a disclosure ---------------------------------------------
   Fourteen unfinished sections sitting between the five that are worked daily
   and the two system screens turned the rail back into a scroll — the thing
   the grouping was added to fix. It collapses, and the caret is the only
   moving part. */
button.ofc-side__g--soon {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  width: 100%;
  background: none;
  cursor: pointer;
  text-align: left;
  font: inherit;
  font-size: var(--o-t-xs);
  font-weight: 750;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--o-faint);
}
button.ofc-side__g--soon:hover { color: var(--o-dim); }
button.ofc-side__g--soon:focus-visible {
  outline: 2px solid var(--o-accent-deep);
  outline-offset: 2px;
  border-radius: var(--o-r-sm);
}
.ofc-side__caret {
  width: 12px;
  height: 12px;
  flex: none;
  transition: transform 0.16s var(--ease, ease);
}
button.ofc-side__g--soon[aria-expanded="true"] .ofc-side__caret { transform: rotate(90deg); }
/* THE COUNT IS WHAT MAKES A COLLAPSED GROUP HONEST. Hiding fourteen sections
   behind a word is fine; hiding them behind a word that does not say how many
   is a menu pretending the rest of the application is not there. */
.ofc-side__gn {
  margin-left: auto;
  font-weight: 500;
  letter-spacing: normal;
  text-transform: none;
  color: var(--o-faint);
}
.ofc-side__soon { display: contents; }
.ofc-side__soon[hidden] { display: none; }

@media (max-width: 62rem) {
  /* The rail is a horizontal strip here and a collapsed group would leave the
     unfinished tabs unreachable, so it is always open — the inline divider
     already marks where the finished work stops. */
  button.ofc-side__g--soon { display: none; }
  .ofc-side__soon[hidden] { display: contents; }
}


/* AN ESTIMATE WHOSE DATE HAS PASSED. The chip printed the expiry the same way
   whether it was next month or last year, so a price the customer can no
   longer hold the company to looked identical to one still open. */
.ofc-chip--warn {
  background: var(--o-warn-bg);
  border-color: var(--o-warn);
  color: var(--o-warn-ink);
}


/* The read-only notice sits above the address row and under the sub-line, so
   it reads as part of the card's explanation rather than as a warning. */
.ofc-mail__ro { margin: 0 0 0.6rem; }

/* ==========================================================================
   THE SHELL, RAISED.  Owner, 2026-09-05: "bare bones, psychopath" — take it to
   the next level, match the new site theme, make it formal and official.

   THE DIAGNOSIS WAS NOT THE COLOUR. The palette above is now the site's, which
   fixes the "match the theme" half. What made it read as bare was that every
   surface was doing the same amount of work: the rail, the top bar, a card and
   a table cell were all one flat fill with one hairline, so nothing announced
   itself as a frame and nothing announced itself as content. A dashboard needs
   a chrome/content split before it needs anything else.

   NO SHADOW, NO GRADIENT, NO BLUR — that ruling stands and design-check
   enforces it. So the split is drawn with the four surface steps, the two
   hairline weights, and spacing. A top border one step lighter than the panel
   is a lit edge without being a light source, and it is the only "material"
   trick used here.

   WHAT ACTUALLY CHANGES:
     · THE RAIL BECOMES CHROME. It sits on --o-panel while the content sits on
       --o-bg, so the frame is a different plane from the work — the single
       change that stops the screen reading as one undivided sheet.
     · THE CURRENT SECTION GETS AN EDGE, not just a fill. A 3px accent bar on
       the leading edge survives being screenshotted at 40% and read at a
       glance, which a tinted pill does not.
     · GROUP LABELS BECOME LABELS. Tracked caps at 11px with a rule, so the rail
       has three named regions instead of one list of twenty links.
     · THE TOP BAR BECOMES A MASTHEAD. Crumb over title, sticky, one hairline
       under it, actions right.
     · FIGURES ARE TABULAR. Every number in a stat, a table or a money cell now
       sits on the same width, so columns line up and a changing figure does not
       reflow the row it is in. This is most of what separates an application
       from a web page, and it costs one property.
   ========================================================================== */

/* ---- the rail is chrome, and chrome is a different plane ---------------- */
.ofc-rail {
  background: var(--o-panel);
  border-right: 1px solid var(--o-line-2);
}
.ofc-rail__brand {
  gap: 0.7rem;
  padding: 1rem 1rem 0.95rem;
  border-bottom: 1px solid var(--o-line-2);
}
:is(#office, #owner, #portal) .ofc-logo {
  width: 38px;
  height: 38px;
  border-radius: var(--o-r-sm);
  padding: 2px;
  background: var(--o-bg);
  border: 1px solid var(--o-line-2);
}
.ofc-rail__nav { padding: 0.9rem 0.6rem 0.6rem; }

/* A GROUP LABEL, NOT A LINK THAT LOOKS DIFFERENT. It was set at the same size
   and colour family as the items under it, so the rail read as one flat list
   of twenty destinations rather than as three named regions. */
:is(#office, #owner, #portal) .ofc-side__g {
  margin: 1.35rem 0 0.45rem;
  padding: 0 0.85rem 0.5rem;
  border-bottom: 1px solid var(--o-line);
  font-family: var(--display);
  font-size: var(--o-t-xs);
  font-weight: 700;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--o-faint);
}
:is(#office, #owner, #portal) .ofc-rail__nav > .ofc-side__g:first-child { margin-top: 0; }

/* THE CURRENT SECTION GETS AN EDGE. The filled pill was doing the whole job
   with a 0.10 alpha wash, which is roughly one step of grey — at a glance, on
   a laptop, in daylight, it was not obviously on. The bar is a shape change,
   it survives greyscale, and it is on the leading edge where the eye enters
   the row. The radius drops on the leading corners so the bar reads as part of
   the row rather than as a floating tick. */
:is(#office, #owner, #portal) .ofc-side__a {
  position: relative;
  padding-inline-start: 1rem;
  border-radius: var(--o-r-sm);
}
:is(#office, #owner, #portal) .ofc-side__a.is-on {
  background: var(--o-accent-wash);
  color: var(--o-text);
}
:is(#office, #owner, #portal) .ofc-side__a.is-on::before {
  content: '';
  position: absolute;
  inset-inline-start: 0;
  inset-block: 0.28rem;
  width: 3px;
  border-radius: var(--o-pill);
  background: var(--o-accent);
}
/* The count is a figure in a list of words, so it gets the figure treatment:
   its own well, tabular, and hard against the trailing edge. */
:is(#office, #owner, #portal) .ofc-side__n {
  margin-inline-start: auto;
  font-variant-numeric: tabular-nums;
  font-size: var(--o-t-xs);
  font-weight: 700;
}

/* ---- the top bar is a masthead ----------------------------------------- */
/* STICKY, because the title is the answer to "where am I" and scrolling a long
   table used to take it off screen. One hairline under it and nothing else —
   the same treatment the rail gets, so the two read as one frame. */
:is(#office, #owner, #portal) .ofc-top {
  position: sticky;
  top: 0;
  z-index: 20;
  background: var(--o-bg);
  border-bottom: 1px solid var(--o-line-2);
  padding-block: 0.9rem;
}
:is(#office, #owner, #portal) .ofc-top__where h1 {
  font-family: var(--display);
  font-size: var(--o-t-title);
  font-weight: 800;
  letter-spacing: -0.02em;
  line-height: 1.1;
  color: var(--o-text);
}
/* The crumb goes ABOVE the title as an eyebrow. Underneath it read as a
   caption on the heading; above it, tracked and small, it reads as the
   position in the application — which is what it is. */
:is(#office, #owner, #portal) .ofc-top__where {
  display: flex;
  flex-direction: column-reverse;
  gap: 0.3rem;
}
:is(#office, #owner, #portal) .ofc-top__where p {
  margin: 0;
  font-size: var(--o-t-xs);
  font-weight: 700;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--o-accent);
}
:is(#office, #owner, #portal) .ofc-top__where p:empty { display: none; }

/* ---- panels get a lit top edge ----------------------------------------- */
/* The only "material" move in the file, and it is a border rather than a
   shadow: one hairline of the lighter weight along the top of a card reads as
   a surface catching light from above without being a light source. Every
   other edge stays the darker weight, so the card still sits IN the page. */
:is(#office, #owner, #portal) .ofc-card {
  border-top-color: var(--o-line-2);
}
:is(#office, #owner, #portal) .ofc-card__bar {
  border-bottom: 1px solid var(--o-line);
}

/* ---- figures ------------------------------------------------------------ */
/* TABULAR EVERYWHERE A NUMBER LIVES. Proportional digits make a column of
   money ragged and make a live counter reflow its own row every time it ticks.
   One property, and it is most of the difference between a dashboard and a
   page with numbers on it. */
:is(#office, #owner, #portal) :is(
  .ofc-stat__n, .ofc-stat__s, .ofc-table td, .ofc-table th,
  .ofc-side__n, .ofc-board__money, .ofc-num, time, [data-money], [data-count]
) {
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
}

/* ---- tables read as records -------------------------------------------- */
/* A HEADER THAT STAYS. On any table long enough to scroll, the column names
   were the first thing to leave. They are the labels for everything below
   them, so they stay. */
:is(#office, #owner, #portal) .ofc-table th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--o-panel);
  font-family: var(--display);
  font-size: var(--o-t-xs);
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--o-faint);
  border-bottom: 1px solid var(--o-line-2);
}
/* Money and counts right-align so the units stack. Anything marked numeric in
   the markup gets it without a per-table rule. */
:is(#office, #owner, #portal) .ofc-table :is(td, th).is-num,
:is(#office, #owner, #portal) .ofc-table :is(td, th)[data-num] {
  text-align: right;
}

/* ---- one focus ring, everywhere ---------------------------------------- */
/* It was inconsistent per control, which on a keyboard is the difference
   between knowing where you are and guessing. The accent at 2px with an offset
   clears every surface in the ladder. */
:is(#office, #owner, #portal) :is(a, button, input, select, textarea, [tabindex]):focus-visible {
  outline: 2px solid var(--o-accent);
  outline-offset: 2px;
  border-radius: var(--o-r-sm);
}


/* ==========================================================================
   §  FIGURES ARE DATA, NOT PROSE                                    2026-09-08
   A dashboard is read by scanning columns of numbers, and proportional digits
   make a column jitter — a 1 is half the width of a 0, so nothing lines up and
   the eye has to re-find the decimal on every row. Every figure in this file
   now sets tabular-nums. The hero figure also gets negative tracking, because
   at 52px the default spacing reads as loose the way a headline does.
   ========================================================================== */
.ofc-stat__v, .ofc-hero__n, .ofc-num, .ofc-money, .ofc-t td, .ofc-t th,
.ofc-side__n, .ofc-chip__n, .ofc-kpi__v, [data-num], time, .ofc-date {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1, "lnum" 1;
}
.ofc-stat__v, .ofc-hero__n, .ofc-kpi__v { letter-spacing: -0.02em; }

/* The rail's active item is a FILL, and the fill is the badge's green — the
   one place in the office where green appears, which is what makes it read as
   the selected thing rather than as decoration. */
.ofc-side__a[aria-current="page"], .ofc-side__a.is-on {
  background: var(--o-accent-deep);
  color: #FFFFFF;
}
.ofc-side__a[aria-current="page"] .ofc-i, .ofc-side__a.is-on .ofc-i { color: #FFFFFF; }


/* ==========================================================================
   §  THE MAILBOX ON A PHONE — ONE PANE AT A TIME             2026-09-08
   The split was `grid-template-columns: 1fr` under 68rem, which stacks the
   reading pane UNDER a 24rem-tall list: you tap a message and nothing appears
   to happen, because the thing you opened is below the fold. Every mail client
   on a phone does the same thing instead — the list IS the screen, and opening
   a message replaces it, with a back control. That is what `is-reading` does:
   the shell sets it when a message is open and clears it on back.
   ========================================================================== */
@media (max-width: 68rem) {
  .ofc-split { grid-template-columns: minmax(0, 1fr); height: auto; }
  .ofc-split__list { max-height: none; border-right: 0; border-bottom: 0; }
  .ofc-split__pane { padding: 0.75rem 0 0; }

  /* One pane at a time. */
  .ofc-split.is-reading .ofc-split__list { display: none; }
  .ofc-split:not(.is-reading) .ofc-split__pane { display: none; }

  /* The back control only exists on a phone, and only while reading. */
  .ofc-split__back { display: none; }
  .ofc-split.is-reading .ofc-split__back {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    margin: 0 0 0.75rem;
    padding: 0.45rem 0.7rem 0.45rem 0.5rem;
    background: var(--o-inset);
    border: 1px solid var(--o-line-2);
    border-radius: var(--o-r-sm);
    color: var(--o-text);
    font: 600 var(--o-t-sm)/1 var(--body);
    cursor: pointer;
  }
  .ofc-split__back:hover { background: var(--o-raised); }

  /* THE ROW LOSES ITS THIRD COLUMN, NOT ITS SHAPE. At 390px there is no room
     for sender AND subject side by side, so the subject drops to a second line
     under the sender — still one row per message, still scannable, and the
     avatar still holds the left edge. This is the one place the grid reflows,
     and it reflows on purpose rather than by being squeezed. */
  .ofc-split__item {
    grid-template-columns: 2rem minmax(0, 1fr) auto;
    padding: 0.5rem 0.6rem;
    min-height: 3.5rem;
    row-gap: 0.15rem;
  }
  .ofc-mail__av   { grid-row: 1 / span 2; align-self: start; }
  .ofc-mail__from { grid-column: 2; grid-row: 1; }
  .ofc-mail__when { grid-column: 3; grid-row: 1; }
  .ofc-mail__subj { grid-column: 2 / -1; grid-row: 2; white-space: normal;
                    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }

  /* The toolbar wrapped into three ragged rows at 430px. Scroll it instead —
     the controls keep their size and their order, and the row keeps one line. */
  .ofc-mail__tools, .ofc-actions {
    display: flex;
    flex-wrap: nowrap;
    gap: 0.4rem;
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }
  .ofc-mail__tools::-webkit-scrollbar, .ofc-actions::-webkit-scrollbar { display: none; }
  .ofc-mail__tools > *, .ofc-actions > * { flex: 0 0 auto; }
}


/* ==========================================================================
   §  THE MAILBOX IS FULL WIDTH, AT EVERY SIZE                      2026-09-08
   The shared `.ofc-split` puts a 22rem list beside a reading pane, which is
   right for Estimates — a short list of names against a long document. It is
   wrong for mail, and measurably so: inside 22rem the row grid had 32px of
   avatar, 208px of sender and ~40px of date, leaving TEN PIXELS for the
   subject. That is why the subject was rendering as a single clipped
   character.
   Gmail does not have a reading pane by default, and this is why: a message
   list wants the full width for sender + subject + snippet, and a message
   wants the full width to be read. So the mailbox uses ONE PANE AT A TIME on
   every screen — list, then message, with a back control — rather than two
   panes that are both too narrow. `.is-reading` is set by drawMailRead().
   ========================================================================== */
.ofc-split--mail {
  grid-template-columns: minmax(0, 1fr);
  height: auto;
}
.ofc-split--mail .ofc-split__list {
  max-height: none;
  border-right: 0;
  border-bottom: 0;
  padding: 0;
}
.ofc-split--mail .ofc-split__pane { padding: 0; }
.ofc-split--mail.is-reading .ofc-split__list { display: none; }
.ofc-split--mail:not(.is-reading) .ofc-split__pane { display: none; }

/* The back control, at every width now — it is the only way out of a message. */
.ofc-split--mail .ofc-split__back {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  margin: 0 0 1rem;
  padding: 0.45rem 0.75rem 0.45rem 0.6rem;
  background: var(--o-inset);
  border: 1px solid var(--o-line-2);
  border-radius: var(--o-r-sm);
  color: var(--o-text);
  font: 600 var(--o-t-sm)/1 var(--body);
  cursor: pointer;
}
.ofc-split--mail .ofc-split__back:hover { background: var(--o-raised); }

/* WITH THE FULL WIDTH THE ROW CAN BREATHE. The sender column grows, and the
   subject finally gets the room it was always supposed to have. */
.ofc-split--mail .ofc-split__item {
  grid-template-columns: 2rem minmax(9rem, 15rem) minmax(0, 1fr) auto;
}

/* A message, read full width, is a document: cap the measure so the body does
   not run to 1200px, which nothing is readable at. */
.ofc-split--mail .ofc-mail__body { max-width: 68ch; }
.ofc-split--mail .ofc-mail__h { max-width: 40ch; }


/* ==========================================================================
   §  THE MAILBOX HEADER                                            2026-09-08
   Was: a two-line design note on the left, a 20rem filter box floated right,
   a full-width warning banner, and then a toolbar right-aligned over a list
   that starts on the left — four elements on four different alignments, none
   of them touching the thing they act on.
   Now: one search field the width of the card, then one toolbar row aligned
   with the list beneath it. Compose leads because it is the only thing here
   that creates something; the rest are quiet.
   ========================================================================== */
.ofc-card__bar--search { display: block; padding-bottom: 0.5rem; }
.ofc-card__bar--search .ofc-filter {
  width: 100%;
  max-width: none;
  height: 2.5rem;
  padding: 0 0.9rem;
  font-size: var(--o-t-sm);
}

/* The toolbar sits on the list's left edge, not the card's right edge. */
.ofc-mail__tools {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  justify-content: flex-start;
  padding: 0.35rem 0 0.6rem;
  border-bottom: 1px solid var(--o-line-2);
  margin-bottom: 0.25rem;
}
/* The three quiet controls go to the right, away from Compose, so the
   destructive-ish ones are not next to the thumb that hits Compose. */
.ofc-mail__tools #ofc-mail-allread { margin-left: auto; }

/* THE "NOTHING HAS ARRIVED" NOTICE, SIZED LIKE A NOTICE. It was a full-width
   block in warn ink with a 4px bar — the visual weight of an outage, for a
   quiet fortnight. One line, one dot, the faint ink. */
#ofc-mail-card .ofc-warn, #ofc-mail-card .ofc-note--warn {
  padding: 0.5rem 0.75rem;
  font-size: var(--o-t-xs);
  border-left-width: 2px;
}

/* The bar was `space-between` with the address chips on the left, so with no
   address group rendered the toolbar was pushed to the right edge — over a
   list that starts on the left. Full width, and the tools own the row. */
.ofc-mail__bar { display: block; margin: 0.5rem 0 0; }
.ofc-mail__bar .ofc-addr { margin: 0 0 0.5rem; }
.ofc-mail__tools { width: 100%; }


/* ==========================================================================
   §  THE SUBTITLE IS A CAPTION, NOT A PARAGRAPH               2026-09-08
   There are 71 `.ofc-sub` blocks in this office and on the dashboard three of
   them are the loudest text on the screen: "Every job that is not finished or
   cancelled. On hold and cancelled are states, not stages, so they sit outside
   the ramp." That is a DESIGN NOTE — true, worth having written down, and read
   exactly once. Set at 13px in the same measure as the content it explains, it
   competes with the content on every subsequent visit, which is every visit.
   It stays (deleting seventy-one of them is a different day's work and some
   are genuinely load-bearing) and it recedes: 11px, one step fainter, tighter
   measure. The eye finds it when it is looking for an explanation and skips it
   when it is not, which is the whole job of a caption.
   ========================================================================== */
.ofc-sub {
  max-width: 56ch;
  font-size: var(--o-t-xs);
  font-weight: 400;
  line-height: 1.55;
  color: var(--o-faint);
  opacity: 0.86;
}

/* ==========================================================================
   §  CARD HEADINGS CARRY THE DISPLAY FACE                     2026-09-08
   "Pipeline", "Waiting on us", "Estimated against actual hours" were body
   weight at body size — the same as the sentence under them — so a dashboard
   of six cards read as six paragraphs. The display face, tighter tracking, and
   a hair more size is the whole difference between a card and a block of text.
   ========================================================================== */
.ofc-card > h2, .ofc-card > h3, .ofc-card__t, .ofc-card__bar h2, .ofc-card__bar h3 {
  font-family: var(--display);
  font-size: 1.0625rem;
  font-weight: 700;
  letter-spacing: -0.012em;
  color: var(--o-text);
  margin: 0 0 0.15rem;
}



/* ==========================================================================
   §  THE THIRTEEN SECTIONS, AUDITED                                2026-09-08

   Invoices, Buying, Subcontractors, Hours, Catalogue, Permits, Cover, Lien
   clocks, Warranty, Sites, Attribution, Jobs and Crews were looked at one at a
   time with sample content on. They do not have thirteen different problems.
   They have the same four, thirteen times, and all four are in shared
   components — which is why this is one block of CSS and not thirteen.

   1  THE STAT TILE'S LABEL WRAPS AND TAKES THE NUMBER WITH IT.
      "TIMESHEETS WHOSE CLOCKS DISAGREE" is two lines; "OVERDUE" is one. In a
      row of three tiles the numbers then sit at three different heights, and a
      row of figures that does not share a baseline is not a row — the eye has
      to find each one separately. Two lines of label are reserved on every
      tile, so one-word labels leave a gap and every number starts level.

   2  THE TILE'S SUBTEXT IS AN ESSAY.
      Under "96 d" sat: "Across 2 running. How many idle days lapse a permit is
      set by the local jurisdiction and is not recorded here, so this is not a
      countdown." Four lines. Under a number. In a tile whose whole job is to
      be read in half a second. The caveat is real and it is worth keeping —
      it is the difference between a figure and a promise — but it is a
      footnote, so it is set as one: 11px, faint, and capped at three lines
      with the rest reachable by hovering the tile.

   3  THE TABLES HAVE NO ROW.
      No hover, no rule between rows in some views, numeric columns left-
      aligned next to text columns, and an action button dropped into the last
      cell at whatever width it happened to be. A table somebody scans for one
      row needs the row to light up under the pointer and the figures to line
      up under each other. That is most of what separates a table you can use
      from a list of words.

   4  THE FLAGS BLOW THE ROW HEIGHT.
      A permit with three flags rendered three stacked amber paragraphs inside
      one cell — a 100px row in a table whose other rows are 40px. They are
      chips now, on one line, with the full text on hover.
   ========================================================================== */

/* 1 — every tile's number starts on the same line. */
.ofc-stat__k {
  min-height: 2.2em;
  align-items: flex-start;
  line-height: 1.1;
}

/* 2 — the caveat under a figure is a footnote, not a paragraph. */
.ofc-stat__s {
  font-size: var(--o-t-xs);
  line-height: 1.5;
  color: var(--o-faint);
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
/* Hovering the tile shows the whole thing — nothing is lost, it is just not
   shouting while you are reading the number. */
.ofc-stat:hover .ofc-stat__s {
  -webkit-line-clamp: unset;
  overflow: visible;
}

/* 3 — the row. Applies to every table in the office. */
.ofc-table, .ofc-lines {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--o-t-sm);
}
.ofc-table th, .ofc-lines th {
  padding: 0.5rem 0.75rem;
  border-bottom: 1px solid var(--o-line-2);
  font-size: var(--o-t-xs);
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--o-faint);
  text-align: left;
  white-space: nowrap;
}
.ofc-table td, .ofc-lines td {
  padding: 0.55rem 0.75rem;
  border-bottom: 1px solid var(--o-line);
  vertical-align: middle;
}
.ofc-table tbody tr:hover, .ofc-lines tbody tr:hover { background: var(--o-hover); }
.ofc-table tbody tr:last-child td, .ofc-lines tbody tr:last-child td { border-bottom: 0; }

/* FIGURES RIGHT, WORDS LEFT — the oldest rule in tabular setting, and the one
   that lets a column of money be compared without reading it. Any cell the
   markup marks numeric, plus the columns that are always money or counts. */
.ofc-table td.num, .ofc-lines td.num,
.ofc-table th.num, .ofc-lines th.num,
.ofc-table td[data-num], .ofc-lines td[data-num] {
  text-align: right;
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}
/* The action column is the last one and is its own width, so buttons in a
   table line up with each other instead of with the text beside them. */
.ofc-table td:last-child:has(> button),
.ofc-lines td:last-child:has(> button) { width: 1%; white-space: nowrap; text-align: right; }

/* 4 — a flag is a chip. */
.ofc-flags { display: flex; flex-wrap: wrap; gap: 0.3rem; }
.ofc-flags > * {
  display: inline-block;
  max-width: 18rem;
  padding: 0.15rem 0.45rem;
  border-radius: 2px;
  font-size: var(--o-t-xs);
  line-height: 1.35;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
tr:hover .ofc-flags > * { white-space: normal; max-width: none; }

/* THE FOOTNOTE UNDER A TABLE. Several sections end with two lines of dense
   prose at body size — "No dollar figure appears here on purpose…" — which
   reads as content because it is set like content. */
.ofc-card > .ofc-note:last-child,
.ofc-card > p:last-child:not(.ofc-sub) {
  font-size: var(--o-t-xs);
  line-height: 1.55;
  color: var(--o-faint);
}


/* ==========================================================================
   §  CRM, CLIENTS AND ESTIMATES                                    2026-09-08

   THE BOARD CARD HAD NO HIERARCHY. Name, age, description and contact all set
   within a step of each other, stacked, in a tinted block — four lines that
   read as one paragraph, on every card, in every column. A board is scanned,
   not read: the name is what you are looking for, the age is what decides
   whether it matters, and the description is why you would open it. Three
   jobs, three weights, and the age moves to the top-right where it can be
   compared down the column instead of hiding at the end of line two.

   AND THE MOST REPEATED STRING ON THE SCREEN WAS "No phone or email given".
   It was on every card in the sample, in body ink, as the last line — so the
   thing the board said most often was that it knew nothing. It still says it,
   because a lead with no way to reach them is the single most useful thing to
   know about a lead. It says it once, small, in the warn ink, as a chip.
   ========================================================================== */
.ofc-board__card {
  grid-template-columns: 1fr auto;
  gap: 0.2rem 0.5rem;
  padding: 0.6rem 0.7rem;
  transition: border-color 0.12s, background 0.12s;
}
.ofc-board__card strong {
  grid-column: 1;
  font-size: 0.875rem;
  font-weight: 650;
  color: var(--o-text);
  line-height: 1.25;
}
/* The age, top-right, tabular — so a column of them compares. */
.ofc-board__card em {
  grid-column: 2;
  grid-row: 1;
  justify-self: end;
  font-size: var(--o-t-xs);
  font-weight: 600;
  color: var(--o-faint);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* Everything after the name and the age is the detail line. */
.ofc-board__card > span, .ofc-board__card > p {
  grid-column: 1 / -1;
  font-size: var(--o-t-xs);
  line-height: 1.45;
  color: var(--o-dim);
}
/* "No phone or email given" and its kind: a chip, not a sentence. */
.ofc-board__card .ofc-nocontact,
.ofc-board__card > span:last-child:not(:first-of-type) {
  justify-self: start;
  margin-top: 0.15rem;
  padding: 0.1rem 0.4rem;
  border-radius: 2px;
  background: rgba(180, 104, 47, 0.14);
  color: var(--o-warn-ink);
  font-size: 0.6875rem;
  font-weight: 600;
}

/* THE BOARD RAN OFF THE RIGHT EDGE with no sign that it did — SPAM was cut in
   half at 1512px and nothing said to scroll. A shadow on the cut edge is the
   convention, and it costs one gradient. */
.ofc-board { scroll-snap-type: x proximity; }
.ofc-board__col { scroll-snap-align: start; }
.ofc-board.is-overflowing::after {
  content: '';
  position: sticky;
  right: 0;
  align-self: stretch;
  width: 2.5rem;
  margin-left: -2.5rem;
  flex: none;
  pointer-events: none;
  background: linear-gradient(90deg, transparent, var(--o-bg));
}

/* THE COLUMN HEAD STAYS PUT while its column scrolls, so you never lose which
   pile you are looking at. */
.ofc-board__h {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--o-panel);
}

/* AN EMPTY COLUMN SAID "none" IN GREY under a count that said 0 — two
   different vocabularies for the same fact, one of them lower case. */
.ofc-board__empty {
  padding: 0.75rem 0.15rem;
  color: var(--o-faint);
  font-size: var(--o-t-xs);
  font-style: italic;
  opacity: 0.75;
}

/* ==========================================================================
   §  AN EMPTY SECTION IS A FORM, NOT A VOID                        2026-09-08
   Estimates with no estimates was a one-line notice, then a small form card
   floating in the top-left third of the screen with two thirds of black to
   the right of it. Whatever the section is FOR is the only thing on the
   screen at that point, so it gets the middle of it and a sensible measure
   rather than being tucked into a corner.
   ========================================================================== */
/* NARROW ONLY WHEN THE CARD IS A FORM. The first cut of this said "a section
   with one card centres it at 46rem", which is right for Estimates-with-no-
   estimates — a three-field form marooned in the top-left of a black screen —
   and wrong for the mailbox, which is also one card and wants every pixel.
   The distinction is not "how many cards" but "does this card contain a list
   of things": if it does, it is a screen of content and takes the width. */
.ofc-grid:has(> .ofc-card:only-child) > .ofc-card:not(:has(table)):not(:has(.ofc-split)):not(:has(ol)):not(:has(ul)) {
  max-width: 46rem;
  margin-inline: auto;
}
.ofc-task select, .ofc-task input, .ofc-task textarea { width: 100%; }

/* The name was wrapping because the age chip took its width. The age is four
   characters; give it exactly what it needs and the name the rest. */
.ofc-board__card { grid-template-columns: minmax(0, 1fr) max-content; }
.ofc-board__card strong { min-width: 0; }
.ofc-board__card em { font-size: 0.6875rem; opacity: 0.9; }

/* A DATE IS ONE TOKEN. "2026-08-08" was breaking across two lines in a narrow
   first column, which is a date that reads as two facts. */
.ofc-table td:first-child, .ofc-lines td:first-child { white-space: nowrap; }

/* The no-contact chip, wherever it lands on a card. */
.ofc-nocontact {
  justify-self: start;
  grid-column: 1 / -1;
  margin-top: 0.15rem;
  padding: 0.1rem 0.4rem;
  border-radius: 2px;
  background: rgba(180, 104, 47, 0.14);
  color: var(--o-warn-ink);
  font-size: 0.6875rem;
  font-weight: 600;
  white-space: nowrap;
}


/* ==========================================================================
   §  "WHY" — THE REASONING, FOLDED                                 2026-09-08
   See foldReasoning() in page-office.mjs for the argument. The control is a
   word, not a button: it sits in the run of the sentence it continues, in the
   accent, because it is a link into more of the same paragraph rather than an
   action on the page.
   ========================================================================== */
.ofc-why {
  border: 0;
  background: none;
  padding: 0;
  margin-left: 0.15rem;
  color: var(--o-accent);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-color: rgba(158, 169, 179, 0.4);
}
.ofc-why:hover { color: var(--o-text); text-decoration-color: currentColor; }
/* MEASURED AT 22×56 FOR A THREE-LETTER WORD, and the cause was not this file:
   app.css sets `button { min-height: 56px }`, which is correct for the field
   app — a control pressed with a glove on a phone in a yard — and wrong for a
   text link that happens to be a <button> for the keyboard's sake. It made the
   line box of EVERY folded paragraph in the office 40px taller than its own
   text, which is the gap that has been sitting under every card subtitle.
   min-height rather than height, so the rule states the floor it is removing.
   nowrap is the second half: an inline-block that does not fit belongs on the
   next line whole, not broken across three. */
.ofc-why { min-height: 0; white-space: nowrap; }
.ofc-why__t[hidden] { display: none; }

/* ==========================================================================
   §  A TABLE CELL IS NOT A PARAGRAPH                                2026-09-08
   The Jobs table's STANDING column printed a full explanation per row —
   "No scheduled start. Nothing on any other screen will surface this — the
   field app asks for a DATE, so a job without one is invisible to every
   phone." — in amber, three rows deep, so one table was three walls of prose
   with the job titles lost down the left.
   The fact is the first clause; the rest is the reasoning, which is the same
   on every row that has it and therefore does not belong on any of them. The
   cell shows the fact as a chip and carries the whole thing as its tooltip.
   ========================================================================== */
.ofc-table td .ofc-state,
.ofc-lines td .ofc-state,
.ofc-table td .ofc-flag,
.ofc-lines td .ofc-flag {
  display: -webkit-box;
  -webkit-line-clamp: 1;
  -webkit-box-orient: vertical;
  overflow: hidden;
  max-width: 22rem;
  padding: 0.15rem 0.45rem;
  border-radius: 2px;
  font-size: var(--o-t-xs);
  font-style: normal;
  line-height: 1.5;
}
/* The row you are pointing at gives you the whole thing. */
.ofc-table tbody tr:hover td .ofc-state,
.ofc-lines tbody tr:hover td .ofc-state,
.ofc-table tbody tr:hover td .ofc-flag,
.ofc-lines tbody tr:hover td .ofc-flag {
  -webkit-line-clamp: unset;
  max-width: none;
  overflow: visible;
}

/* ==========================================================================
   §  A SCREEN OF FORMS IS NOT A SCREEN                             2026-09-08
   Crews & the day opens with six form fields and three paragraphs, and no
   answer to the question somebody opens it for: who is on what today. The
   forms are how you CHANGE that; they are not the screen. Until the markup is
   restructured they at least stop being the first and largest thing: a form
   block inside a card is set on the inset surface, indented behind a rule, and
   its heading is a small tracked label rather than a title the same size as
   the section's own.
   ========================================================================== */
.ofc-task {
  padding: 0.85rem 1rem;
  background: var(--o-inset);
  border-left: 2px solid var(--o-line-2);
  border-radius: 0 var(--o-r-sm) var(--o-r-sm) 0;
}
.ofc-task > h3, .ofc-task > h4, .ofc-task__h {
  margin: 0 0 0.6rem;
  font-family: var(--body);
  font-size: var(--o-t-xs);
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--o-faint);
}
/* Two columns where there is room: a six-field form in one column is a scroll
   for no reason on a 1500px screen. */
@media (min-width: 60rem) {
  .ofc-task { display: grid; grid-template-columns: 1fr 1fr; gap: 0.6rem 1rem; align-items: end; }
  .ofc-task > h3, .ofc-task > h4, .ofc-task__h,
  .ofc-task > p, .ofc-task > button, .ofc-task > .ofc-actions { grid-column: 1 / -1; }
}


/* THE STANDING CELL. `.ofc-note.is-warn` is a full-width notice block — right
   for a card, wrong for a table cell, where it made three rows into three
   paragraphs and pushed the job titles into a narrow left gutter. In a cell it
   is a chip: the first clause, which is the fact, on one line. The rest is the
   reasoning, which is identical on every row that has it and therefore belongs
   on none of them — it is in the title attribute and appears on hover. */
.ofc-table td .ofc-note,
.ofc-lines td .ofc-note {
  display: -webkit-box;
  -webkit-line-clamp: 1;
  -webkit-box-orient: vertical;
  overflow: hidden;
  max-width: 20rem;
  padding: 0.15rem 0.5rem;
  border-left: 0;
  border-radius: 2px;
  font-size: var(--o-t-xs);
  font-style: normal;
  line-height: 1.6;
}
.ofc-table tbody tr:hover td .ofc-note,
.ofc-lines tbody tr:hover td .ofc-note {
  -webkit-line-clamp: unset;
  max-width: none;
  overflow: visible;
}


/* ==========================================================================
   §  NOTHING THAT IS PRESSED IS A PILL                             2026-09-08

   The radii were brought to a machined 4 / 3 / 6 last week and the buttons
   did not follow, because they do not use those tokens — they use `--o-pill`,
   which is 999px. So `.ofc-ghost` and `.ofc-quiet` (Compose, Unread only,
   Mark all read, Sent, and the same pair on every other screen) stayed fully
   round while the cards around them squared up. A 999px button beside a 4px
   card is not a style, it is two styles.

   WHAT STAYS ROUND, AND WHY. A pill is right for a thing that is a COUNT or a
   STATE and wrong for a thing that is an ACTION:
     · .ofc-side__n   the rail's unread badge — a number in a dot
     · .ofc-chip      a status chip
     · .ofc-me__mark  a monogram, which is a face
     · .ofc-mail__thread  "3 in thread", a count
   Those keep 999px. Everything you press does not.
   ========================================================================== */
:is(#office, #owner, #portal) .ofc-ghost,
:is(#office, #owner, #portal) .ofc-quiet,
:is(#office, #owner, #portal) .ofc-primary,
:is(#office, #owner, #portal) button.ofc-mini,
:is(#office, #owner, #portal) .ofc-link {
  border-radius: var(--o-r);
}
/* The rail item is a target, not a tablet. */
.ofc-side__a { border-radius: 0 var(--o-r-sm) var(--o-r-sm) 0; }
:is(#office, #owner, #portal) .ofc-side__a.is-on::before { border-radius: 0; }

/* The jump box is a search field; search fields on this site are square. */
.ofc-jump { border-radius: var(--o-r); }

/* THE STRAYS. Five literals that predate the scale and each pick their own
   number: 10px on one logo and 8px on another, 8px on the compose fields
   while every other input is 3px, 5px on two kbd elements. A scale nothing
   obeys is a list of numbers. */
.ofc-logo, .ofc-head .ofc-logo { border-radius: var(--o-r); }
.ofc-mono, .ofc-skip { border-radius: var(--o-r-sm); }
.ofc-compose input, .ofc-compose textarea { border-radius: var(--o-r-sm); }
.ofc-jump kbd, .ofc-mail__help kbd { border-radius: 2px; }
#ofc-grid:focus-visible { border-radius: var(--o-r); }


/* The address filters are pressed, so they are not pills either — and they
   were the largest round things left on the screen. A filter that is ON is a
   filled tab; one that is off is an outline. */
.ofc-addr__b {
  border-radius: var(--o-r);
  padding: 0.4rem 0.7rem;
  font-size: var(--o-t-sm);
}
.ofc-addr__b[aria-pressed="true"], .ofc-addr__b.is-on {
  background: var(--o-inset);
  border-color: var(--o-line-2);
  color: var(--o-text);
}
/* The count inside a filter is a count, not an alarm — it was in the warn ink,
   which is the colour this office uses for something being wrong. */
.ofc-addr__b b, .ofc-addr__b .ofc-addr__n {
  margin-left: 0.35rem;
  color: var(--o-faint);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.ofc-addr__b[aria-pressed="true"] b, .ofc-addr__b.is-on .ofc-addr__n { color: var(--o-dim); }


/* ==========================================================================
   §  THE ACTION BAR, AND THE DIALOG IT OPENS                       2026-09-08
   See hoistForms() in page-office.mjs. What a screen can DO now sits at the
   top of it; what a screen SHOWS fills the rest.
   ========================================================================== */
.ofc-view-acts {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  margin-left: auto;
  margin-right: 0.75rem;
}
.ofc-view-acts[hidden] { display: none; }
.ofc-act { white-space: nowrap; }
@media (max-width: 60rem) {
  /* On a narrow screen the header cannot hold six buttons and a search box.
     The bar drops under the title and scrolls, keeping every action reachable
     rather than wrapping into three ragged rows. */
  .ofc-view-acts {
    order: 3;
    width: 100%;
    margin: 0.6rem 0 0;
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
  }
  .ofc-view-acts::-webkit-scrollbar { display: none; }
  .ofc-act { flex: 0 0 auto; }
}

.ofc-modal {
  width: min(42rem, calc(100vw - 2rem));
  /* CAPPED AGAINST THE SAFE AREA, NOT JUST THE VIEWPORT. A centered dialog at
     86vh on a small phone leaves only a few px above it — enough to slide the
     title bar and its close X under the clock. Subtracting both insets (plus a
     little) guarantees the centering keeps the whole dialog, X included, inside
     the usable screen. dvh so it tracks the real viewport in standalone. */
  max-height: min(86vh, 52rem, calc(100dvh - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px) - 1.5rem));
  padding: 0;
  border: 1px solid var(--o-line-2);
  border-radius: var(--o-r-lg);
  background: var(--o-panel);
  color: var(--o-text);
  overflow: hidden;
}
.ofc-modal::backdrop { background: rgba(6, 8, 10, 0.66); backdrop-filter: blur(2px); }
.ofc-modal__h {
  margin: 0;
  padding: 1.1rem 3.25rem 0.9rem 1.4rem;
  border-bottom: 1px solid var(--o-line);
  font-family: var(--display);
  font-size: 1.0625rem;
  font-weight: 700;
  letter-spacing: -0.012em;
}
.ofc-modal__b { padding: 1.1rem 1.4rem 1.4rem; overflow: auto; max-height: calc(86vh - 4.5rem); }
/* Inside the dialog the form is the whole point, so it drops the inset panel
   and the indent rule it wears when it is sitting in a card. */
.ofc-modal__b .ofc-task {
  display: block;
  padding: 0;
  background: none;
  border-left: 0;
  border-radius: 0;
}
@media (min-width: 40rem) {
  .ofc-modal__b .ofc-task,
  .ofc-modal__b .ofc-fields {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0.75rem 1rem;
    align-items: end;
  }
  .ofc-modal__b .ofc-task > h3,
  .ofc-modal__b .ofc-task > p,
  .ofc-modal__b .ofc-task > .ofc-actions,
  .ofc-modal__b .ofc-field--wide,
  .ofc-modal__b textarea,
  .ofc-modal__b label:has(textarea) { grid-column: 1 / -1; }
}
/* The heading is in the dialog's own title bar; printing it again inside is
   the same words twice. */
.ofc-modal__b .ofc-task > h3:first-child,
.ofc-modal__b .ofc-task > h4:first-child,
.ofc-modal__b .ofc-task__h:first-child { display: none; }
.ofc-modal__x { position: absolute; top: 0.6rem; right: 0.6rem; margin: 0; }
.ofc-modal__x button {
  width: 2rem; height: 2rem;
  display: grid; place-items: center;
  border: 1px solid transparent;
  border-radius: var(--o-r);
  background: none;
  color: var(--o-faint);
  font-size: 0.9rem;
  cursor: pointer;
}
.ofc-modal__x button:hover { color: var(--o-text); background: var(--o-inset); border-color: var(--o-line-2); }


/* ==========================================================================
   §  TODAY — WHO IS ON WHAT                                        2026-09-08
   The screen is called "Crews & the day" and answered neither: it opened with
   a crew list and four forms, and the day appeared nowhere. This is the day.
   One row per booking, three fields, aligned in a grid so a morning's worth
   of them is read down the columns rather than across the sentences.
   ========================================================================== */
.ofc-today { list-style: none; margin: 0.5rem 0 0; padding: 0; }
.ofc-today__r {
  display: grid;
  grid-template-columns: minmax(6rem, 11rem) minmax(0, 1fr) auto;
  align-items: center;
  gap: 0.75rem;
  padding: 0.55rem 0.15rem;
  border-bottom: 1px solid var(--o-line);
}
.ofc-today__r:last-child { border-bottom: 0; }
.ofc-today__c {
  font-size: var(--o-t-sm);
  font-weight: 650;
  color: var(--o-text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ofc-today__j {
  font-size: var(--o-t-sm);
  color: var(--o-dim);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ofc-today__p { color: var(--o-faint); }
.ofc-today__s { justify-self: end; white-space: nowrap; }
.ofc-today__pick {
  display: flex; align-items: center; gap: 0.5rem;
  font-size: var(--o-t-xs); font-weight: 700; letter-spacing: 0.09em;
  text-transform: uppercase; color: var(--o-faint);
}
.ofc-today__pick input { width: auto; }
@media (max-width: 48rem) {
  .ofc-today__r { grid-template-columns: 1fr auto; }
  .ofc-today__j { grid-column: 1 / -1; }
}


/* THE TWO CARDS UNDER TODAY ARE NOW MOSTLY EMPTY, because everything in them
   was a form and every form moved to the header. "The day" in particular has
   nothing left but a heading and a caption. That is honest — the writing side
   is in the dialogs — but two half-width cards with one sentence each is a lot
   of furniture for two sentences. They sit on one row at their natural height
   instead of being stretched to match each other. */
#ofc-crew-card, .ofc-view[data-view="crews"] .ofc-card--half { align-self: start; }


/* ==========================================================================
   §  THE OFFICE ON A PHONE — MEASURED, NOT GUESSED               2026-09-08
   Rendered in an iframe at 390px and measured from the inside, because
   nothing about a layout this size is reliably judged by eye at a distance.
   Three things were wrong and each one had a number on it.

   1  THE RAIL WAS A 3,171px HORIZONTAL SCROLLER. Twenty-two sections in one
      strip 389px wide: reaching Settings meant eight swipes past every other
      section, and nothing said how far it went or where you were in it. A
      horizontal scroller is a fine control for six things and a filing
      cabinet with the drawers taped open for twenty-two.
      On a phone the rail collapses to ONE control — the section you are in —
      which opens the jump palette that already exists and already searches
      every section by name. Typing three letters beats eight swipes.

   2  THE ACTION BAR HAD 139px FOR 253px OF BUTTONS. The rule that drops it
      under the title existed but never won, because .ofc-top is not a wrapping
      flex container — so `width: 100%` on a flex child inside a nowrap row is
      139px of squeeze. The container has to wrap before the child can.

   3  THE BOARD COLUMNS WERE 192px, which put roughly 120px under a name after
      the avatar and the age chip — so every two-word name broke, and "Nia
      Okafor" was measured clipping a 46px word into a 26px box. Columns are
      280px on a phone: fewer on screen, each one readable, which is the trade
      every mobile board makes.
   ========================================================================== */
@media (max-width: 60rem) {
  /* 2 — let the header wrap so the action bar can take its own line. */
  .ofc-top { flex-wrap: wrap; }
  .ofc-view-acts { flex-basis: 100%; }

  /* 3 — a column somebody can read. The TRACK again, not the element. */
  .ofc-board { grid-auto-columns: minmax(17.5rem, 1fr); }
  .ofc-board__card { grid-template-columns: minmax(0, 1fr) max-content; }
}

@media (max-width: 48rem) {
  /* 1 — the rail becomes one control. The nav itself is hidden rather than
     removed, so ⌘K and every in-page link that targets a section still work;
     what goes is the strip nobody could use. */
  .ofc-rail__brand { border-bottom: 0; }


  /* The rail's footer identity block is three lines of chrome above the work
     on a screen that has none to spare. */
  .ofc-rail__foot .ofc-me { display: none; }
}
/* THE FULL-WIDTH SECTION BUTTON THAT USED TO BE HERE IS GONE. It named the
   current section and took a whole row of a screen that has nine — and it only
   opened a palette you then had to type into. The hamburger and the drawer at
   the end of this file replace it.

   Worth keeping the lesson it left: its hide rule was written as a bare
   `display: none` placed AFTER the media query that showed it, which wins on
   source order at every width — including the phone it was built for, where it
   was invisible until that was caught by measuring rather than by looking. */



/* ==========================================================================
   §  THE CRM BOARD, REBUILT                                        2026-09-08
   A lead row carries value_cents, address, timeline, project_type, message,
   first_contact_at, notes, won_at and lost_reason. The card drew four of them.
   These are the rest — see paintBoard() for which fact does what.
   ========================================================================== */

/* The header carries the column's money beside its count. */
.ofc-board__h { display: flex; align-items: baseline; gap: 0.5rem; }
.ofc-board__hv {
  margin-left: auto;
  font-size: var(--o-t-xs);
  font-weight: 700;
  color: var(--o-dim);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}

/* THE FACT ROW. Money leads because it ranks; place and timing follow in the
   quiet ink, separated by a middot rather than by punctuation of their own —
   they are three fields, not a sentence. */
.ofc-board__facts {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.15rem 0.45rem;
  margin: 0.15rem 0 0;
  font-size: var(--o-t-xs);
  line-height: 1.4;
}
.ofc-board__v {
  font-weight: 700;
  color: var(--o-text);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}
.ofc-board__f { color: var(--o-faint); }
.ofc-board__f + .ofc-board__f::before { content: '\00b7'; margin-right: 0.45rem; color: var(--o-line-2); }

/* THE REPLY CLOCK. Three bands, and the colour is never the only signal — the
   chip says the hours in words either way, so it reads the same in greyscale
   and to anyone who cannot separate the two warm steps. */
.ofc-board__sla {
  grid-column: 1 / -1;
  justify-self: start;
  margin-top: 0.2rem;
  padding: 0.1rem 0.4rem;
  border-radius: 2px;
  font-size: 0.6875rem;
  font-weight: 600;
  white-space: nowrap;
  background: rgba(158, 169, 179, 0.12);
  color: var(--o-dim);
}
.ofc-board__sla.is-late { background: rgba(180, 104, 47, 0.16); color: var(--o-warn-ink); }
.ofc-board__sla.is-cold {
  background: rgba(180, 104, 47, 0.26);
  color: var(--o-warn-ink);
  box-shadow: inset 2px 0 0 var(--o-warn);
}


/* ==========================================================================
   §  A COLUMN HAS A MINIMUM WIDTH, AT EVERY SIZE                   2026-09-08
   Measured: at 1512px the board fitted all six stages on screen by giving each
   174px, which after padding left 106px for a name. So "Tom Brightwater"
   wrapped, "Priya Raghunathan" wrapped, every chip went to its own line, and
   the fact row added on this pass had nowhere to go. Six columns were visible
   and none of them was legible.

   Fitting every column on screen is not the goal — reading one is. A pipeline
   board scrolls sideways; that is the shape, and this one already had the
   scroll and the fade. 16rem is the width at which a name, a value and an age
   sit on one line, which is the row this card is built from.
   ========================================================================== */
/* THE BOARD IS A GRID WITH grid-auto-flow: column, so a column's width is the
   TRACK's, not the element's. Setting min-width on the column made the element
   256px inside a 192px track — so every card overhung its own column and sat
   on top of the next one. The track is the thing to size. */
.ofc-board { grid-auto-columns: minmax(16rem, 1fr); }

/* THE CARD HEAD IS A FLEX ROW, NOT TWO GRID CELLS. As grid columns the name
   and the age negotiated width with each other and with everything spanning
   below them; as a row the age takes what it needs and the name takes the
   rest, which is the relationship they actually have. */
.ofc-board__card { display: block; }
.ofc-board__hd { display: flex; align-items: baseline; gap: 0.5rem; }
.ofc-board__card strong {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ofc-board__card em { flex: 0 0 auto; }

/* Chips share a row instead of stacking one per line. */
.ofc-board__chips { display: flex; flex-wrap: wrap; gap: 0.25rem; margin-top: 0.3rem; }
.ofc-board__chips > * { margin: 0; }
.ofc-board__sla, .ofc-nocontact { margin-top: 0; }


/* ==========================================================================
   §  DRAFTING A FIRST REPLY                                        2026-09-08
   See replyDraft() in page-office.mjs. The action sits beside the name in the
   detail pane, because answering is what somebody opened the enquiry to do
   and every other thing on that pane is a field to read.
   ========================================================================== */
.ofc-lead__hd {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 0.35rem;
}
.ofc-lead__hd .ofc-lead__h { margin: 0; }
.ofc-lead__reply { flex: 0 0 auto; white-space: nowrap; }

/* The redesigned lead header: name + a one-line health read on the left, the
   act-now buttons on the right. */
.ofc-lead__title { display: flex; flex-direction: column; gap: 0.25rem; min-width: 0; }
.ofc-lead__meta { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin: 0; }
.ofc-lead__stage {
  font-size: var(--o-t-xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 0.1rem 0.55rem;
  border-radius: 999px;
}
.ofc-lead__stage.is-open   { background: var(--o-accent-wash); color: var(--o-accent); }
.ofc-lead__stage.is-won    { background: var(--o-accent-deep); color: var(--o-bg); }
.ofc-lead__stage.is-closed { background: var(--o-inset); color: var(--o-dim); }
.ofc-lead__health { font-size: var(--o-t-xs); font-weight: 600; }
.ofc-lead__health.is-ok   { color: var(--o-dim); }
.ofc-lead__health.is-warn { color: var(--o-warn-ink); }
.ofc-lead__act { display: flex; align-items: center; gap: 0.5rem; flex: 0 0 auto; }
.ofc-lead__facts { margin: 0.7rem 0 0; }
.ofc-lead__noreply {
  flex: 0 0 auto;
  padding: 0.2rem 0.5rem;
  border-radius: 2px;
  background: rgba(180, 104, 47, 0.14);
  color: var(--o-warn-ink);
  font-size: var(--o-t-xs);
  font-weight: 600;
  white-space: nowrap;
}


/* THE WON COLUMN'S TOTAL was a bare paragraph sitting between the header and
   the first card, in body ink, unaligned with either — so the one number on
   the board that is already banked read as a stray sentence. It is a line of
   the column now, in the same place the other columns put their figure. */
.ofc-board__sum {
  margin: 0 0 0.5rem;
  padding: 0.4rem 0.55rem;
  background: var(--o-inset);
  border-radius: var(--o-r-sm);
  font-size: var(--o-t-xs);
  line-height: 1.5;
  color: var(--o-faint);
}
.ofc-board__sum strong {
  color: var(--o-text);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}
.ofc-board__atleast { color: var(--o-faint); font-weight: 600; }

/* A card in the last column was clipping its own age line against the board's
   right edge; the column has the room, the text just could not wrap. */
.ofc-board__card em { white-space: normal; text-align: right; }


/* ==========================================================================
   §  THE PHONE'S NAVIGATION DRAWER                                 2026-09-08
   One icon in the corner, and the real rail inside it. See openDrawer() in
   page-office.mjs — the nav element is MOVED into the drawer and back, so
   there is only ever one navigation in this document.
   ========================================================================== */
.ofc-burger { display: none; }
@media (max-width: 48rem) {
  /* PADDING RESET FIRST, THEN THE SIZE. The office's generic button carries
     `padding: 13.6px 20px` for a button with a word in it. On a square icon
     control that is 40px of horizontal padding around a 20px glyph inside a
     40px box — so the box computed 42x56 instead of 40x40 and the three rules
     were crushed into what was left. Measured, not guessed: 42 wide, 56 tall,
     padding 13.6/20.
     Setting a width without clearing the padding it inherits is the whole bug,
     and it is the reason this rule states every box property rather than the
     two that look like they matter. */
  .ofc-burger {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    padding: 0;
    margin: 0;
    width: 2.5rem;
    height: 2.5rem;
    min-width: 2.5rem;
    min-height: 2.5rem;
    flex: 0 0 2.5rem;
    line-height: 0;
    border: 1px solid var(--o-line-2);
    border-radius: var(--o-r);
    background: var(--o-inset);
    color: var(--o-text);
    cursor: pointer;
  }
  .ofc-burger svg { width: 22px; height: 22px; display: block; flex: none; }
  .ofc-burger:hover { border-color: var(--o-accent); background: var(--o-raised); }
  .ofc-burger:active { background: var(--o-press); }

  /* The rail keeps only its brand block — the drawer has the rest. */
  .ofc-rail__nav { display: none; }
  .ofc-rail__foot { display: none; }
  .ofc-rail { position: static; }
}

/* A SHEET FROM THE RIGHT, because that is the side the control is on and a
   panel that opens away from the thumb that summoned it reads as a different
   object. Full height, because a section list is a list and not a card. */
.ofc-drawer {
  margin: 0 0 0 auto;
  height: 100vh;
  max-height: 100vh;
  width: min(20rem, 88vw);
  padding: 0;
  border: 0;
  border-left: 1px solid var(--o-line-2);
  border-radius: 0;
  background: var(--o-panel);
  color: var(--o-text);
  overflow: hidden;
}
.ofc-drawer::backdrop { background: rgba(6, 8, 10, 0.6); }
.ofc-drawer__hd {
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* THE DRAWER IS A FULL-HEIGHT <dialog> IN THE TOP LAYER, so it renders above
     the status-bar band and the shell padding both — neither reaches it. Its
     header sat flush at the top edge, which put the title and the close X under
     the phone's clock and battery. This is the same notch problem as the app
     header, arriving through the one element the earlier fix could not touch,
     because a top-layer dialog is positioned against the screen, not the shell.
     Padded here directly. The right inset covers landscape, where the notch
     moves to the side the X lives on. env() is 0 everywhere without a notch. */
  padding: calc(0.9rem + env(safe-area-inset-top, 0px)) max(0.75rem, env(safe-area-inset-right, 0px)) 0.9rem 1rem;
  border-bottom: 1px solid var(--o-line);
  font-size: var(--o-t-xs);
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--o-faint);
}
.ofc-drawer__hd form { margin: 0; }
.ofc-drawer__hd button {
  width: 2rem; height: 2rem;
  display: grid; place-items: center;
  border: 1px solid transparent;
  border-radius: var(--o-r);
  background: none;
  color: var(--o-faint);
  cursor: pointer;
}
.ofc-drawer__hd button:hover { color: var(--o-text); background: var(--o-inset); border-color: var(--o-line-2); }
/* The body height loses the two insets the header and the home bar now claim,
   so the last nav item is never stranded under the home indicator, and the
   bottom padding adds a little scroll slack past it. */
.ofc-drawer__b { overflow: auto; height: calc(100vh - 3.4rem - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px)); padding: 0.5rem 0.5rem calc(2rem + env(safe-area-inset-bottom, 0px)); }
/* Inside the drawer the rail's nav is shown, whatever the rail itself is doing. */
.ofc-drawer__b #ofc-side { display: block !important; }
.ofc-drawer__b .ofc-side__a { border-radius: var(--o-r-sm); }

/* THE SECTION HEADERS COME BACK IN THE DRAWER. On a phone the rail turns into a
   horizontal strip and the mobile rule hides every group label (there is no room
   for them lying on their side). But openDrawer() moves this same nav into a
   TALL drawer, where there IS room — and without this override the labels stayed
   hidden, so Today / Sales / Money / Compliance / Property / System all ran
   together as one undifferentiated list. This restores the grouping the desktop
   rail has, so the drawer reads as organised sections, not a junk drawer. */
.ofc-drawer__b .ofc-side__g {
  display: block;
  margin: 1.1rem 0 0.35rem;
  padding: 0 0.55rem 0.4rem;
  border-bottom: 1px solid var(--o-line);
  font-family: var(--display);
  font-size: var(--o-t-xs);
  font-weight: 750;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--o-faint);
}
.ofc-drawer__b .ofc-side__g:first-child { margin-top: 0.1rem; }
/* The "coming soon" divider, if present, reads as a full-width break here rather
   than the inline pill the horizontal strip uses. */
.ofc-drawer__b .ofc-side__g--soon {
  display: block;
  margin: 1.1rem 0 0.35rem;
  padding: 0.85rem 0.55rem 0.4rem;
  border-top: 1px solid var(--o-line);
  border-left: 0;
  white-space: normal;
}


/* THE BURGER LIVES IN THE HEADER BAR. On a phone the rail collapses to its
   brand block, which is the bar across the top of the screen — so that row
   becomes the app header and the control sits at its right end, which is
   where a phone puts navigation and where the thumb already is. */
@media (max-width: 48rem) {
  .ofc-rail {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding-right: 0.75rem;
    border-bottom: 1px solid var(--o-line-2);
  }
  .ofc-rail__brand { flex: 1 1 auto; min-width: 0; border-bottom: 0; }
  .ofc-burger { margin-left: auto; }
}


/* ==========================================================================
   §  THE MARK NEEDS NO BOX                                         2026-09-08
   .ofc-logo wraps the roundel in a 1px border, a dark plate and 3px of
   padding — a square outline drawn around a badge that is already a disc on
   its own ground. That treatment made sense when the mark was a flat glyph
   with no field of its own; it has carried its own plate since the redraw, so
   the box is a second frame around a finished object, and at 30px it reads as
   a mistake rather than as a container.
   ========================================================================== */
:is(#office, #owner, #portal) .ofc-logo,
:is(#office, #owner, #portal) .ofc-head .ofc-logo,
:is(#design) .dz-brand img {
  padding: 0;
  border: 0;
  background: none;
  border-radius: 50%;
}


/* SIGNING OUT IS NOT A DASHBOARD CONTROL. On a phone the header has room for
   the mark and one button, and that button is the sections — so the top bar's
   sign-out goes, and the account block (address, role, sign out) rides into
   the drawer with the nav where it belongs. */
@media (max-width: 48rem) {
  /* Specificity: the office's ghost button is declared as
     `:is(#office, #owner, #portal) .ofc-ghost { display: inline-flex }`, which
     is (1,1,0) and beats a bare class. Matched rather than fought with
     !important, so the next person can still override it the normal way. */
  :is(#office, #owner, #portal) .ofc-top__signout { display: none; }
  .ofc-rail__foot { display: none; }
  .ofc-drawer__b .ofc-rail__foot {
    display: block;
    margin-top: 1rem;
    padding-top: 0.9rem;
    border-top: 1px solid var(--o-line);
  }
  .ofc-drawer__b .ofc-rail__foot .ofc-me { display: flex; }
}

/* ==========================================================================
   §  SETTINGS
   A settings screen is not a dashboard. It is a LIST of things you can change,
   read top to bottom, and every row answers the same three questions in the
   same three places: what is this called, what does it do, and what is it set
   to right now. The version this replaced had one half-width card floating in
   a 1200px row with two thirds of the row empty beside it, two buttons stacked
   in a column at different widths, and the only real control on the page —
   sample content — indistinguishable from a paragraph of prose.

   Everything below is one column with a hard cap. The cap is not decoration:
   a settings row is label + sentence + control, and on a 27" monitor an
   uncapped row puts the control four hundred pixels from the label it belongs
   to, which is how people toggle the wrong thing.
   ========================================================================== */

.ofc-set {
  display: grid;
  gap: 1rem;
  /* 54rem ≈ 864px. Wide enough for a sentence of description beside a control,
     narrow enough that the control never leaves the label behind. */
  max-width: 54rem;
  /* Left-aligned, not centred. Centring a single column in a shell that has a
     rail on one side reads as a layout bug rather than a choice. */
  margin: 0;
}

/* The cards live in .ofc-set, not .ofc-grid, so the 12-column span rules do
   not apply — stated anyway, because a card class that carries a span into a
   non-grid parent is the kind of thing that starts working again by accident. */
.ofc-set__card { grid-column: auto; }
.ofc-set .ofc-sub { max-width: 78ch; }

/* ---- who you are: one band, not three grey slabs ------------------------ */

.ofc-set__me {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-top: 0.5rem;
  padding: 0.7rem 0.8rem;
  background: var(--o-inset);
  border: 1px solid var(--o-line);
  border-radius: var(--o-r);
}
.ofc-set__av {
  display: grid;
  place-items: center;
  box-sizing: border-box;
  width: 2.25rem; height: 2.25rem; flex: 0 0 2.25rem;
  border-radius: 50%;
  background: var(--o-accent-deep);
  color: #FFFFFF;
  font: 600 var(--o-t-sm)/1 var(--display);
  letter-spacing: 0.04em;
}
.ofc-set__id { display: grid; gap: 0.1rem; min-width: 0; }
.ofc-set__id b {
  font-size: var(--o-t-md);
  color: var(--o-text);
  overflow-wrap: anywhere;
}
.ofc-set__id > span {
  font-size: var(--o-t-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--o-dim);
}
/* Pushed to the far edge rather than given a margin of its own, so the band
   holds its shape whatever the address length. */
.ofc-set__me .ofc-ghost { margin-left: auto; flex: none; }

.ofc-card > .ofc-set__h,
.ofc-set__h {
  margin: 1.15rem 0 0.5rem;
  font-size: var(--o-t-xs);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--o-faint);
}

/* ---- what the role reaches: chips, because it is a SET not a ranking ---- */

.ofc-set__reach {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: 0.3rem;
}
.ofc-set__reach li {
  padding: 0.2rem 0.5rem;
  border: 1px solid var(--o-line-2);
  border-radius: var(--o-r-sm);
  background: var(--o-inset);
  font-size: var(--o-t-xs);
  color: var(--o-dim);
}
.ofc-set__reach li.is-owner {
  border-color: var(--o-accent-deep);
  color: var(--o-text);
}

/* ---- the row: the whole screen is this shape --------------------------- */

.ofc-set__rows { list-style: none; margin: 0.85rem 0 0; padding: 0; }

.ofc-set__r {
  display: grid;
  /* Text takes what is left; the control takes what it needs and no more.
     `auto` rather than a fixed track so a switch, a select and a value string
     can share one list without any of them being padded out to the widest. */
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 0.5rem 1rem;
  padding: 0.7rem 0;
  border-top: 1px solid var(--o-line);
}
.ofc-set__r:first-child { border-top: 0; padding-top: 0; }
.ofc-set__r:last-child { padding-bottom: 0; }

.ofc-set__t { min-width: 0; display: grid; gap: 0.15rem; }
.ofc-set__t b {
  font-family: var(--display);
  font-size: var(--o-t-md);
  font-weight: 600;
  color: var(--o-text);
}
.ofc-set__t span {
  font-size: var(--o-t-sm);
  line-height: 1.45;
  color: var(--o-dim);
  /* Wide enough for two lines of a normal sentence, capped so it can never
     run under the control on the right. */
  max-width: 58ch;
}
/* Where a constant is set. Steel, like every other pointer on this app. */
.ofc-set__t small {
  font-size: var(--o-t-xs);
  color: var(--o-faint);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}

/* The right-hand value on a read-only row. Tabular so a column of numbers
   lines up on the decimal even though each row sets its own string. */
.ofc-set__v {
  font-family: var(--display);
  justify-self: end;
  text-align: right;
  font-size: var(--o-t-md);
  font-variant-numeric: tabular-nums;
  color: var(--o-text);
  overflow-wrap: anywhere;
}
.ofc-set__v small {
  display: block;
  font-size: var(--o-t-xs);
  color: var(--o-faint);
  font-variant-numeric: normal;
}
.ofc-set__v.is-ok  { color: var(--o-accent); }
.ofc-set__v.is-off { color: var(--o-neutral); }

/* ---- the switch ---------------------------------------------------------
   A real <button role="switch">, not a checkbox painted over: the platform
   then gives it the state in the accessibility tree for free, and Space and
   Enter both work without a keydown handler of mine to get wrong.

   ON is the brand green, because on this app a filled control is green and a
   steel one is a mark. OFF is a well with a steel knob — visibly a control,
   visibly not engaged, and the two states differ in POSITION as well as
   colour so the switch still reads with no colour at all. */

.ofc-sw {
  --sw-w: 2.75rem;
  --sw-h: 1.5rem;
  /* THE EDGE, NOT THE FILL, IS WHAT MAKES A SWITCH A SWITCH. Measured: the
     brand green fill lands 2.94:1 against a card and the standard panel edge
     lands 1.56:1, so an untouched switch read as a knob floating on nothing
     and an engaged one as a soft green smudge — both under the 3:1 that WCAG
     asks of a control's boundary. The FILL is left alone: it is the badge's
     green and the whole app's filled controls are that colour. What moves is
     the border, which carries no brand meaning and every bit of the shape.
     Its own values rather than the stage ramp's, which is about job stages and
     has been retuned twice for reasons that have nothing to do with this. */
  --sw-edge-off: var(--o-neutral);   /* 3.98:1 on a card, 3.64:1 on the well */
  --sw-edge-on:  #44A55A;            /* 5.65:1 on a card; holds under the hover */
  display: inline-block;
  box-sizing: border-box;
  position: relative;
  padding: 0; margin: 0;
  width: var(--sw-w); height: var(--sw-h);
  min-width: var(--sw-w); min-height: var(--sw-h);
  flex: 0 0 var(--sw-w);
  border: 1px solid var(--sw-edge-off);
  border-radius: calc(var(--sw-h) / 2);
  background: var(--o-inset);
  cursor: pointer;
  transition: background-color 0.14s ease, border-color 0.14s ease;
}
.ofc-sw::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 0.1875rem;
  width: 1rem; height: 1rem;
  margin-top: -0.5rem;
  border-radius: 50%;
  background: var(--o-faint);
  transition: transform 0.14s ease, background-color 0.14s ease;
}
.ofc-sw[aria-checked="true"] {
  background: var(--o-accent-deep);
  border-color: var(--sw-edge-on);
}
.ofc-sw[aria-checked="true"]::after {
  background: #FFFFFF;
  transform: translateX(calc(var(--sw-w) - 1.375rem));
}
/* Hover darkens the fill — the office's standing direction, because the step
   UP fails AA under the white labels the buttons carry. A switch carries no
   label, so what has to survive the darkening is the border and the knob: the
   border is held at the same light green (5.65:1) and the white knob GAINS
   contrast on the darker fill, 5.96:1 to 8.67:1. */
.ofc-sw:hover { border-color: var(--o-accent); }
.ofc-sw[aria-checked="true"]:hover {
  background: var(--o-accent-hi);
  border-color: var(--sw-edge-on);
}
.ofc-sw:focus-visible {
  outline: 2px solid var(--o-accent);
  outline-offset: 2px;
}

/* A select on a settings row sizes to its content rather than stretching, so
   "Dashboard" and "Permits & compliance" do not force a 300px track on every
   other row in the list. */
.ofc-set__sel {
  justify-self: end;
  max-width: 14rem;
  padding: 0.35rem 0.5rem;
  font-size: var(--o-t-sm);
  color: var(--o-text);
  background: var(--o-inset);
  border: 1px solid var(--o-line-2);
  border-radius: var(--o-r);
}
.ofc-set__sel:hover { border-color: var(--o-accent); }

/* MEASURED FLUSH: the last row of a settings list has no bottom padding by
   design, so an .ofc-actions after it started on the row's own baseline and
   the button's top edge cut through the sentence above it. */
.ofc-set__card .ofc-actions {
  margin-top: 1.1rem;
  padding-top: 0.9rem;
  border-top: 1px solid var(--o-line);
  align-items: center;
  gap: 0.75rem;
}

.ofc-set .ofc-cannot dt {
  margin-top: 1.1rem;
  padding-top: 1rem;
  border-top: 1px solid var(--o-line);
  font-family: var(--display);
  font-size: var(--o-t-md);
  color: var(--o-text);
}
.ofc-set .ofc-cannot dt:first-child { margin-top: 0.4rem; padding-top: 0; border-top: 0; }
.ofc-set .ofc-cannot dd { max-width: 74ch; }
/* "Needs" is the actionable half — what somebody would have to record before
   the number could exist. Marked rather than left to the colour alone. */
.ofc-set .ofc-cannot dd.ofc-needs {
  margin-top: 0.4rem;
  padding-left: 0.65rem;
  border-left: 2px solid var(--o-accent-deep);
}

.ofc-set__ping { font-size: var(--o-t-sm); color: var(--o-dim); align-self: center; }
.ofc-set__ping.is-ok   { color: var(--o-accent); }
.ofc-set__ping.is-bad  { color: var(--o-warn-ink); }

/* ---- the preferences, honoured ------------------------------------------
   These two do the work the switches promise. Scoped to the shell so they
   cannot leak into the palette or a dialog's own animation, and written as
   attributes on #office so the switch only has to set one thing. */

/* COMPACT IS A TOKEN, NOT A LIST OF EXCEPTIONS. The first attempt was four
   rules naming four components, and it lost to `.ofc-card { padding: var(--o-pad) }`
   on nothing more interesting than which file loaded last — and it would have
   missed every panel added after it was written. Re-pointing the variable on
   the shell moves everything that reads it, in one place, including the parts
   of owner.css and portal.css that are aliased onto these names above. */
#office[data-density="compact"] {
  --o-pad: 0.85rem;
}
/* The two things that do not read --o-pad: table cells, and the settings rows
   themselves. Both are listed because both are read as a stack of lines, which
   is exactly what density is for. */
#office[data-density="compact"] :is(td, th) { padding-top: 0.35rem; padding-bottom: 0.35rem; }
#office[data-density="compact"] .ofc-set__r { padding: 0.5rem 0; }
#office[data-density="compact"] .ofc-roster li { padding-top: 0.3rem; padding-bottom: 0.3rem; }

/* Belt and braces with the OS setting: somebody who wants motion off in this
   app specifically should not have to change a system preference to get it. */
#office[data-motion="off"],
#office[data-motion="off"] *,
#office[data-motion="off"] *::before,
#office[data-motion="off"] *::after {
  transition-duration: 0.001s !important;
  animation-duration: 0.001s !important;
  animation-iteration-count: 1 !important;
  scroll-behavior: auto !important;
}

/* ---- phone ---------------------------------------------------------------
   The control drops under its own label rather than being squeezed beside it.
   Measured: at 390px the widest label + a 44px switch on one line leaves 150px
   for a sentence, which wraps to five lines and makes the row taller than the
   stacked version it was trying to avoid. */
@media (max-width: 40rem) {
  .ofc-set__r {
    grid-template-columns: minmax(0, 1fr);
    justify-items: start;
  }
  .ofc-set__v, .ofc-set__sel, .ofc-set__r .ofc-sw { justify-self: start; }
  .ofc-set__v { text-align: left; }
  .ofc-set__sel { max-width: 100%; width: 100%; }
  .ofc-set__me { flex-wrap: wrap; }
  .ofc-set__me .ofc-ghost { margin-left: 0; width: 100%; }
}

/* ==========================================================================
   §  CALLS AND TEXTS — 0049
   A thread, not a table. Mail, texts and calls in one column in the order they
   happened, because "we called, they texted, we emailed the quote" is one
   story. Ours on the right, theirs on the left — the arrangement every person
   using this screen already reads fluently from their own phone, and the one
   place in this office where copying a consumer pattern is the right call.
   ========================================================================== */

.ofc-convo { display: grid; gap: 0.4rem; margin: 0.5rem 0 0.25rem; }

/* ---- a message --------------------------------------------------------- */

.ofc-convo__b {
  justify-self: start;
  max-width: 34rem;
  padding: 0.5rem 0.7rem;
  border: 1px solid var(--o-line);
  border-radius: var(--o-r-lg);
  background: var(--o-inset);
}
/* OURS IS THE GREEN ONE, and it is the badge's green like every other filled
   thing in this office. Right-aligned as well as recoloured: a third of people
   cannot rely on the hue, and side alone still answers "who said this". */
.ofc-convo__b.is-out {
  justify-self: end;
  background: var(--o-accent-deep);
  border-color: var(--o-accent-deep);
}

.ofc-convo__kind {
  display: block;
  font-size: var(--o-t-xs);
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--o-faint);
  margin-bottom: 0.15rem;
}
/* 0.86, not 0.72. Measured: 0.72 lands 3.96:1 on the green, under AA for
   text this size, and this is an uppercase 11px label — the size where a
   near-miss is a label nobody reads. 0.86 measures 4.89:1. */
.ofc-convo__b.is-out .ofc-convo__kind { color: rgba(255, 255, 255, 0.86); }

.ofc-convo__subj {
  display: block;
  font-family: var(--display);
  font-size: var(--o-t-sm);
  color: var(--o-text);
  margin-bottom: 0.2rem;
}

.ofc-convo__t {
  margin: 0;
  font-size: var(--o-t-sm);
  line-height: 1.5;
  color: var(--o-text);
  /* A customer's text can be one 400-character word, and a bubble that will
     not break it pushes the whole thread off the side of the pane. */
  overflow-wrap: anywhere;
  white-space: pre-wrap;
}

.ofc-convo__foot {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  margin: 0.25rem 0 0;
  font-size: var(--o-t-xs);
  color: var(--o-faint);
}
/* The office's own dim step is 1.8:1 on this fill — it was never meant to sit
   on it — so the footer is white at an alpha that was measured rather than
   picked: 0.86 gives 4.89:1. */
.ofc-convo__b.is-out .ofc-convo__foot { color: rgba(255, 255, 255, 0.86); }

.ofc-convo__st { margin-left: auto; font-weight: 600; }
.ofc-convo__st.is-ok  { color: rgba(255, 255, 255, 0.9); }   /* 5.20:1 */
.ofc-convo__st.is-dim { color: rgba(255, 255, 255, 0.86); }  /* 4.89:1 */

/* THE ONE THAT HAS TO BE SEEN, and the one that nearly shipped unreadable.
   A text the carrier dropped sits beside the ones that arrived, and read as
   sent it is the office believing it answered somebody it never reached — so
   this is the highest-stakes label in the thread.

   The warn ink alone measured 2.93:1 ON THE GREEN, which is worse than the
   text around it, on the one word that has to be seen. It gets its own plate
   instead: the warn ground and the warn ink are a pair the palette already
   defines for each other, 9.20:1, and the plate itself is 3.14:1 against the
   fill so the chip has an edge without needing a border. */
.ofc-convo__st.is-bad {
  color: var(--o-warn-ink);
  background: var(--o-warn-bg);
  padding: 0.05rem 0.3rem;
  border-radius: var(--o-r-sm);
}

/* ---- a call is a line, not a bubble ------------------------------------
   It has no body. A bubble containing nothing looks broken, so the outcome IS
   the content and it sits full width like the event it is. */

.ofc-convo__call {
  display: flex;
  align-items: baseline;
  gap: 0.4rem;
  margin: 0.15rem 0;
  padding: 0.4rem 0.6rem;
  border-left: 2px solid var(--o-line-2);
  font-size: var(--o-t-sm);
  color: var(--o-dim);
}
.ofc-convo__call b { color: var(--o-text); font-weight: 600; }
.ofc-convo__icon { color: var(--o-accent); font-size: var(--o-t-md); line-height: 1; }
.ofc-convo__meta { color: var(--o-dim); }
.ofc-convo__when { margin-left: auto; color: var(--o-faint); font-size: var(--o-t-xs); }

.ofc-convo__vm {
  margin: 0 0 0.3rem 0.6rem;
  padding: 0.45rem 0.7rem;
  border-left: 2px solid var(--o-accent-deep);
  background: var(--o-inset);
  border-radius: 0 var(--o-r) var(--o-r) 0;
  font-size: var(--o-t-sm);
  line-height: 1.5;
  color: var(--o-dim);
  overflow-wrap: anywhere;
}

/* Mark-dealt-with / bring-back, under the thread. A quiet row — the reply box
   below is the primary action here, so this sits above it without competing. */
.ofc-convo__act {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin: 0.5rem 0 0.1rem;
}
.ofc-convo__act:empty { display: none; }
.ofc-convo__said { font-size: var(--o-t-xs); color: var(--o-dim); }
.ofc-convo__said.is-bad { color: var(--o-warn-ink); }

/* ---- the reply box ------------------------------------------------------ */

.ofc-convo__reply {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 0.5rem;
  align-items: end;
  margin-top: 0.6rem;
  padding-top: 0.7rem;
  border-top: 1px solid var(--o-line);
}
.ofc-convo__reply textarea {
  width: 100%;
  box-sizing: border-box;
  padding: 0.5rem 0.6rem;
  font: inherit;
  font-size: var(--o-t-sm);
  line-height: 1.45;
  color: var(--o-text);
  background: var(--o-inset);
  border: 1px solid var(--o-line-2);
  border-radius: var(--o-r);
  resize: vertical;
  min-height: 3.25rem;
}
.ofc-convo__reply textarea:focus-visible {
  outline: 2px solid var(--o-accent);
  outline-offset: 1px;
}
.ofc-convo__reply textarea:disabled { opacity: 0.6; cursor: progress; }
.ofc-convo__reply button { align-self: end; }

/* Spans both columns so a sentence about a failure is not squeezed into the
   textarea's track. */
.ofc-convo__said {
  grid-column: 1 / -1;
  margin: 0;
  min-height: 1.1em;
  font-size: var(--o-t-sm);
  color: var(--o-dim);
}
.ofc-convo__said.is-bad { color: var(--o-warn-ink); }

@media (max-width: 40rem) {
  .ofc-convo__b { max-width: 100%; }
  .ofc-convo__reply { grid-template-columns: minmax(0, 1fr); }
  /* width, not justify-self: stretch — measured at 95px in a 319px track, so
     something on .ofc-primary is fixing the size and the stretch was a rule
     that did nothing. A send button under a full-width box wants the full
     width, on a phone more than anywhere. */
  .ofc-convo__reply button { width: 100%; }
}

/* ==========================================================================
   §  THE PHONE LOG
   Every row answers the same three questions in the same three places — who,
   when, and what needs doing — because a list whose rows do not line up is a
   list nobody scans. This is the OPPOSITE choice from the conversation pane,
   where a call is drawn as a line and a text as a bubble: there the shape
   carries the meaning, here the alignment does.
   ========================================================================== */

.ofc-ph {
  padding: 0.7rem 0.8rem;
  border-top: 1px solid var(--o-line);
}
.ofc-ph:first-child { border-top: 0; }

/* WHAT IS STILL WAITING, MARKED ON THE ROW ITSELF. The card it sits in says so
   too, but a row keeps its meaning when somebody scrolls the heading off — and
   the rail badge sends people here with no memory of which card was which. */
.ofc-ph.is-todo {
  background: var(--o-inset);
  border-left: 2px solid var(--o-accent-deep);
  border-radius: 0 var(--o-r) var(--o-r) 0;
  margin-bottom: 0.3rem;
  border-top: 0;
}

.ofc-ph__hd {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.ofc-ph__who {
  font-family: var(--display);
  font-size: var(--o-t-md);
  color: var(--o-text);
}
.ofc-ph__num { font-size: var(--o-t-sm); color: var(--o-dim); font-variant-numeric: tabular-nums; }
.ofc-ph__when { margin-left: auto; font-size: var(--o-t-xs); color: var(--o-faint); }

/* THE LOG AS A MESSAGES INBOX (see phoneContact). One collapsed row per number —
   latest message and a count — that expands to the whole thread on click. */
.ofc-ph--contact { padding: 0; }               /* the row and the thread pad themselves */
.ofc-ph__contact {
  display: block;
  padding: 0.7rem 0.8rem;
  cursor: pointer;
}
.ofc-ph__contact:hover { background: var(--o-hover); }
.ofc-ph__contact:focus-visible { outline: 2px solid var(--o-accent); outline-offset: -2px; }
.ofc-ph__chev { color: var(--o-faint); font-size: 0.7rem; }
.ofc-ph__count {
  font-size: var(--o-t-xs);
  font-weight: 650;
  color: var(--o-dim);
  background: var(--o-inset);
  border-radius: 999px;
  padding: 0.05rem 0.45rem;
}
/* Unread count — the accent, so "3 new" reads at a glance the way a phone's
   badge does. Minimum width so a single digit is still a round pill. */
.ofc-ph__unread {
  font-size: var(--o-t-xs);
  font-weight: 700;
  color: var(--o-bg);
  background: var(--o-accent);
  border-radius: 999px;
  padding: 0.05rem 0.4rem;
  min-width: 1.15rem;
  text-align: center;
  font-variant-numeric: tabular-nums;
}
.ofc-ph__preview {
  display: flex;
  align-items: baseline;
  gap: 0.4rem;
  margin: 0.25rem 0 0;
  color: var(--o-dim);
  font-size: var(--o-t-sm);
}
.ofc-ph__prev {
  flex: 1 1 auto;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Unread conversations read louder, the way a phone bolds an unopened thread. */
.ofc-ph--contact.is-unread .ofc-ph__who { color: var(--o-text); }
.ofc-ph--contact.is-unread .ofc-ph__prev { color: var(--o-text); font-weight: 600; }
.ofc-ph--contact.is-unread .ofc-ph__count { background: var(--o-accent-wash); color: var(--o-accent); }
/* The opened thread, inset under the row it belongs to. */
.ofc-ph__expand { padding: 0 0.8rem 0.7rem; }
.ofc-ph__expand .ofc-ph__thread { margin: 0.2rem 0 0.5rem; gap: 0.3rem; }
.ofc-ph__audio { width: 100%; height: 34px; margin-top: 0.25rem; }

/* The call sliver under a conversation's message summary (see phoneContact) —
   a call is not a text, and this says who rang whom and offers a call back. */
.ofc-ph__calls {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0 0.8rem;
  padding: 0.4rem 0;
  border-top: 1px solid var(--o-line);
  font-size: var(--o-t-xs);
  color: var(--o-dim);
}
.ofc-ph__callico { color: var(--o-accent); flex: none; }
.ofc-ph__callsum { flex: 1 1 auto; min-width: 0; }
.ofc-ph__callmiss { color: var(--o-warn-ink); font-weight: 600; }
.ofc-ph__callbtn {
  flex: none;
  padding: 0.2rem 0.7rem;
  border: 1px solid var(--o-accent-deep);
  border-radius: var(--o-r);
  color: var(--o-accent);
  font-weight: 650;
  text-decoration: none;
  white-space: nowrap;
}
.ofc-ph__callbtn:hover { background: var(--o-accent-deep); color: var(--o-bg); }

/* Direction and missed labels on each call/text row (2026-09-13). The body
   already carries the arrow and the outcome; these are the plain words a full
   log is expected to show, and the missed chip is what makes an unreturned call
   findable in a scan. */
.ofc-ph__tags { display: flex; flex-wrap: wrap; gap: 0.35rem; margin: 0.15rem 0 0.1rem; }

/* All / Calls / Texts — the office's own button geometry, not pills: slim
   (28px), square-cornered (var(--o-r) = 4px), transparent with a hairline edge,
   and the active one marked by an accent border, exactly the way the mailbox
   marks its "Unread only" filter (see .ofc-mail__tools .ofc-ghost.is-on). Each
   carries its count, so "have any texts come in?" is answered by the number on
   the tab rather than by switching to it and reading an empty list. (2026-09-13) */
.ofc-ph-filter {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
  margin: 0.2rem 0 0.9rem;
}
.ofc-ph-filter__b {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  min-height: 28px;
  padding: 0 0.7rem;
  border: 1px solid var(--o-line-2);
  border-radius: var(--o-r);
  background: transparent;
  color: var(--o-dim);
  font: 650 var(--o-t-xs)/1 var(--body);
  letter-spacing: 0.005em;
  cursor: pointer;
}
.ofc-ph-filter__b:hover:not(.is-on) { background: var(--o-hover); color: var(--o-text); }
.ofc-ph-filter__b.is-on {
  background: var(--o-inset);
  border-color: var(--o-accent-deep);
  color: var(--o-text);
}
.ofc-ph-filter__b:focus-visible { outline: 2px solid var(--o-accent); outline-offset: 2px; }
.ofc-ph-filter__n { color: var(--o-faint); font-weight: 500; font-variant-numeric: tabular-nums; }

.ofc-ph__tag {
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  padding: 0.05em 0.45em;
  border-radius: 2px;
  border: 1px solid var(--o-line-2);
  color: var(--o-dim);
  white-space: nowrap;
}
/* Incoming is the one that usually needs a person, so it carries the accent;
   outgoing is quieter. */
.ofc-ph__tag.is-in  { color: var(--o-accent, #6fcf97); border-color: color-mix(in srgb, var(--o-accent, #6fcf97) 45%, transparent); }
.ofc-ph__tag.is-out { color: var(--o-faint); }
.ofc-ph__tag.is-missed {
  color: var(--o-warn-ink, #f0b072);
  border-color: var(--o-warn, #a2660f);
  background: var(--o-warn-bg, rgba(162,102,15,0.12));
}


.ofc-ph__body {
  display: flex;
  gap: 0.4rem;
  margin: 0.3rem 0 0;
  font-size: var(--o-t-sm);
  line-height: 1.5;
  color: var(--o-dim);
  /* A text can arrive as one 400-character word. */
  overflow-wrap: anywhere;
}
.ofc-ph__dir { color: var(--o-accent); flex: none; }

/* The failure that has to be seen: a text the carrier dropped. Its own line
   rather than a chip, because in a log there is room to say what it means, and
   "NOT delivered" without "so they never saw this" is a status nobody acts on. */
.ofc-ph__bad {
  margin: 0.3rem 0 0;
  padding: 0.25rem 0.5rem;
  background: var(--o-warn-bg);
  border-radius: var(--o-r-sm);
  font-size: var(--o-t-xs);
  color: var(--o-warn-ink);   /* 10.3:1 on its own ground — see §7 */
}

.ofc-ph__vm {
  margin: 0.35rem 0 0;
  padding: 0.4rem 0.6rem;
  border-left: 2px solid var(--o-line-2);
  font-size: var(--o-t-sm);
  line-height: 1.5;
  color: var(--o-dim);
  overflow-wrap: anywhere;
}

.ofc-ph__audio {
  display: block;
  width: 100%;
  max-width: 22rem;
  margin-top: 0.4rem;
  /* The native control paints its own light chrome unless told the surface is
     dark. One line, and it is the difference between a player that belongs to
     this app and one that looks pasted in. */
  color-scheme: dark;
}

.ofc-ph__acts {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
  margin-top: 0.5rem;
}

/* The picker is the primary action on an unfiled row, so it is sized to be
   pressed rather than squeezed to its content. */
.ofc-ph__pick {
  max-width: 22rem;
  padding: 0.3rem 0.45rem;
  font-size: var(--o-t-sm);
  color: var(--o-text);
  background: var(--o-inset);
  border: 1px solid var(--o-line-2);
  border-radius: var(--o-r);
}
.ofc-ph__pick:hover:not(:disabled) { border-color: var(--o-accent); }
.ofc-ph__pick:disabled { color: var(--o-faint); cursor: not-allowed; }

.ofc-ph__note {
  margin: 0.35rem 0 0;
  font-size: var(--o-t-xs);
  font-style: italic;
  color: var(--o-faint);
}

.ofc-ph__said:empty { display: none; }
.ofc-ph__said {
  margin: 0.35rem 0 0;
  font-size: var(--o-t-sm);
  color: var(--o-dim);
}
.ofc-ph__said.is-bad { color: var(--o-warn-ink); }

@media (max-width: 40rem) {
  .ofc-ph__when { margin-left: 0; width: 100%; }
  .ofc-ph__pick, .ofc-ph__audio { max-width: 100%; width: 100%; }
  .ofc-ph__acts > * { flex: 1 1 100%; }
  /* EXCEPT THE LINK. Stretched to the full row its border-bottom becomes a
     292px rule under a 170px phrase, which reads as a divider rather than as
     an underline — the same misreading the min-height bug caused, arrived at
     from the other direction. */
  .ofc-ph__acts > .ofc-linkish { flex: 0 0 auto; }
}

/* A conversation that cannot be continued by text. Warn ground rather than the
   green reply box it replaces, because this is not an error the office made —
   it is a decision the customer made, and the row has to read as information
   rather than as a failure. */
.ofc-convo__stopped {
  display: grid;
  gap: 0.15rem;
  margin: 0.6rem 0 0;
  padding: 0.6rem 0.75rem;
  background: var(--o-warn-bg);
  border-left: 2px solid var(--o-warn);
  border-radius: 0 var(--o-r) var(--o-r) 0;
  font-size: var(--o-t-sm);
  line-height: 1.5;
  color: var(--o-warn-ink);   /* 10.3:1 on its own ground — see §7 */
}
.ofc-convo__stopped b { color: var(--o-warn-ink); }

/* ==========================================================================
   §  THE SAME PERSON, TWICE — 0051
   A warn ground, not an error one: nothing has gone wrong, there is just
   something to check before ringing. The distinction matters because this
   appears on enquiries that are otherwise perfectly healthy.
   ========================================================================== */

.ofc-dupe {
  margin: 0.4rem 0 0.2rem;
  padding: 0.7rem 0.8rem;
  background: var(--o-warn-bg);
  border-left: 2px solid var(--o-warn);
  border-radius: 0 var(--o-r) var(--o-r) 0;
}
.ofc-dupe__lead {
  margin: 0 0 0.5rem;
  font-size: var(--o-t-sm);
  line-height: 1.5;
  color: var(--o-warn-ink);   /* 10.3:1 on its own ground — see §7 */
  max-width: 68ch;
}
.ofc-dupe__list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.3rem; }
.ofc-dupe__list li { font-size: var(--o-t-sm); color: var(--o-dim); }
.ofc-dupe__why { color: var(--o-warn-ink); }
.ofc-dupe__when, .ofc-dupe__st { color: var(--o-faint); font-size: var(--o-t-xs); }
.ofc-dupe .ofc-note { margin: 0.55rem 0 0; }

/* The chip on the board card. Warn, and FIRST in the chip row — it changes
   what you do about the reply clock beside it, so it has to be read first. */
.ofc-board__dupe {
  padding: 0.1rem 0.4rem;
  border-radius: var(--o-r-sm);
  background: var(--o-warn-bg);
  color: var(--o-warn-ink);
  font-size: var(--o-t-xs);
  font-weight: 600;
}

/* ==========================================================================
   §  REFRESH
   Pushed to the right of the action bar and separated by a rule: it is not
   one of the things you DO on a screen, it is a property of the screen. Put
   in the same row as "Take an enquiry" with no separation, it reads as a
   fourth verb of equal weight, which it is not.
   ========================================================================== */

.ofc-refresh {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-left: auto;
  padding-left: 0.75rem;
  border-left: 1px solid var(--o-line);
}

.ofc-refresh__when {
  font-size: var(--o-t-xs);
  color: var(--o-faint);
  white-space: nowrap;
  /* Tabular so "4 min ago" and "11 min ago" do not shuffle the button
     sideways every time the stamp ticks over. */
  font-variant-numeric: tabular-nums;
}

.ofc-refresh__b {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  /* Clears the field app's `button { min-height: 56px }`, which is right for
     a gloved hand in a yard and wrong for a header control. Third time this
     rule has had to be undone — .ofc-why and .ofc-linkish were the others. */
  min-height: 0;
  padding: 0.4rem 0.7rem;
}
.ofc-refresh__i {
  font-size: 1rem;
  line-height: 1;
  /* The glyph is the only part that could spin, and it does not: a spinner on
     a two-second read is a spinner nobody sees. The label changes instead. */
}
.ofc-refresh__b:disabled { opacity: 0.65; cursor: progress; }

@media (max-width: 40rem) {
  /* At phone width the action bar wraps, and a left rule on a control that
     has just become the first thing on its own line is a rule pointing at
     nothing. */
  .ofc-refresh {
    margin-left: 0;
    padding-left: 0;
    border-left: 0;
    width: 100%;
    justify-content: space-between;
  }
}

/* ==========================================================================
   §  LINKS INSIDE A MESSAGE SOMEBODY ELSE WROTE
   Steel, like every other link in this office — the green is for fills. The
   underline is always on: inside a paragraph of a stranger's prose there is
   no other cue that a run of text is clickable, and colour alone fails for a
   third of readers.
   ========================================================================== */

.ofc-autolink {
  color: var(--o-link);          /* 8.61:1 on the ground, 7.47:1 in a well */
  text-decoration: underline;
  text-underline-offset: 2px;
  /* The underline is the link's OWN colour, faded — not a grey line that
     happens to sit under it. Colour alone fails for a reader who cannot see
     it, so the underline stays; matching it means the two cues agree. */
  text-decoration-color: rgba(108, 182, 255, 0.55);
  /* A tracking URL can be 663 characters with no space in it — the Twilio
     receipt link that prompted this is exactly that. Without a break rule it
     is one unbreakable word that pushes the whole reading pane sideways. */
  overflow-wrap: anywhere;
  word-break: break-word;
}
.ofc-autolink:hover {
  /* Brighter blue, not white: hover should say "yes, this one", not stop
     looking like a link while the pointer is on it. */
  color: var(--o-link-hi);
  text-decoration-color: currentColor;
}
.ofc-autolink:focus-visible {
  outline: 2px solid var(--o-link);
  outline-offset: 2px;
  border-radius: 2px;
}

/* On the green bubble the steel is 1.8:1 — it was never meant to sit there.
   White with a visible underline instead: 5.96:1, and the underline is what
   marks it as a link once the colour can no longer do it. */
.ofc-convo__b.is-out .ofc-autolink {
  color: #FFFFFF;
  text-decoration-color: rgba(255, 255, 255, 0.7);
}
.ofc-convo__b.is-out .ofc-autolink:hover { text-decoration-color: #FFFFFF; }

/* ---------------------------------------------------------------------------
   THE 56px EVERY BUTTON INHERITS, AND THE THREE THAT NEVER CANCELLED IT

   app.css sets `button { min-height: 56px }` for the field app, where a control
   is pressed with a thumb on a phone in the sun. Every button style in this
   office file cancels it — ofc-primary, ofc-ghost, ofc-quiet, ofc-why,
   ofc-linkish, ofc-mini and the rest — because a dashboard is a dense screen
   read with a mouse and a 56px pill in a filter row is not a pill.

   THREE NEVER DID, and all three are built in JavaScript rather than written in
   office.html, which is why they were missed when the others were fixed: the
   mailbox chips, the reply button on a lead, and the back button on a split
   pane. They came out 56px tall with their text sitting at the top and the
   bottom half of the box empty — reported as "text is top half within a
   square/rectangle button and the bottom half is empty", which is exactly what
   `align-items: baseline` inside a box twice the height of its content does.

   Fixed as a group and by NAME, so the next JS-built button that forgets is
   found by looking here rather than by somebody noticing a shape.
--------------------------------------------------------------------------- */
.ofc-addr__b,
.ofc-lead__reply,
.ofc-split__back {
  min-height: 0;
}
/* Baseline alignment is right for a label beside a count when the box hugs
   them. It is wrong the moment the box is taller than the text, so the chips
   centre instead — the count and the label still share a baseline because they
   are one line of text on one flex line. */
.ofc-addr__b { align-items: center; }

/* A THUMB STILL HAS TO HIT IT. Cancelling the 56px is right for a mouse and
   leaves a 30px target on a phone, which is under every touch guideline there
   is. The padding grows on small screens rather than the min-height coming
   back, so the chip stays a chip and the row still scrolls. */
@media (max-width: 48rem) {
  .ofc-addr__b { padding: 0.55rem 0.85rem; }
  .ofc-lead__reply, .ofc-split__back { min-height: 40px; }
}

/* ---------------------------------------------------------------------------
   THE MAILBOX HEADER

   The search box had a bar to itself above everything, and Compose was the
   first of four quiet buttons in a row UNDER the address filters — so the two
   things somebody opens this screen to do were the two hardest to find, and the
   screen's own name was only in the sidebar. One line now: name, search, the
   green button.
--------------------------------------------------------------------------- */
.ofc-mail__head {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  margin-bottom: 0.85rem;
  flex-wrap: wrap;
}
/* The box takes the room left over and can shrink to nothing before it wraps —
   `min-width: 0` because a flex item's default minimum is its content, and
   without it a search input refuses to shrink and pushes the button off. */
.ofc-mail__q { flex: 1 1 10rem; min-width: 0; }
/* BIGGER THAN THE QUIET ROW IT CAME FROM. It is the one action on this screen
   that writes anything, so it does not look like "Mark all read".

   THE SELECTOR CARRIES THE ID ON PURPOSE. Every primary button in this file is
   sized by `:is(#office, #owner, #portal) .ofc-primary { min-height: 34px }`,
   which is specificity (1,1,0); a bare class here is (0,1,0) and loses however
   far down the file it sits. Measured before matching it: 34px, not the 42 the
   rule asked for. */
:is(#office, #owner, #portal) .ofc-mail__compose {
  flex: 0 0 auto;
  min-height: 42px;
  padding: 0.6rem 1.4rem;
  font-size: var(--o-t-sm);
  font-weight: 650;
}

/* ONE ROW THAT SCROLLS, NEVER A STACK. `.ofc-addr` wrapped, so the moment the
   card was narrower than the chips they broke into a column and read as three
   separate blocks rather than one filter. Reported twice. A filter row scrolls
   sideways; it does not become a list. */
.ofc-addr {
  flex-wrap: nowrap;
  overflow-x: auto;
  scrollbar-width: none;
  scroll-snap-type: x proximity;
  padding-bottom: 2px;
}
.ofc-addr::-webkit-scrollbar { display: none; }
.ofc-addr > * { flex: 0 0 auto; scroll-snap-align: start; }

@media (max-width: 48rem) {
  /* The box and the button share the line on a phone too; the button keeps a
     44px target rather than becoming a sliver beside a full-width input. */
  :is(#office, #owner, #portal) .ofc-mail__compose { min-height: 44px; }
}

/* ==========================================================================
   MAIL, RENDERED WITH THE SHAPE ITS SENDER GAVE IT

   Since 0055 the mailbox stores `body_html` as well as the text, and
   app/js/mail-html.mjs turns it into a tree of allowlisted descriptions that
   page-office.mjs builds real nodes from. The sender's own colours, weights
   and borders arrive as CSSOM declarations on those nodes — nothing here has
   to style them, and nothing here should try to.

   WHAT THIS BLOCK DOES is the opposite: it gives the message a floor to stand
   on, so a mail that carries NO styling of its own is still readable, and a
   mail that carries plenty cannot break the pane it sits in. Everything here
   is a default the sender's own declarations override, except the containment
   rules at the end, which they must not.
   ========================================================================== */
.ofc-mail__rich {
  max-width: 78ch;
  font-size: var(--o-t-sm);
  line-height: 1.6;
  color: var(--o-dim);
  /* A stranger's markup does not get to widen the pane or bleed out of it. */
  overflow-x: auto;
  overflow-wrap: anywhere;
}
/* MAIL LAYOUT SAFETY — the message reads like a message, not a pile of boxes.
   The sanitiser already turns a fixed `height` into `min-height`, so a card
   grows to fit its text instead of clipping and letting the next card paint
   over it. This stops the remaining horizontal failure: nothing is wider than
   the reading column, so a card built at width:600px is capped rather than
   forcing a scrollbar or bleeding out of the pane. A `:where()` selector so a
   sender's own inline styles still win where they are legitimate. */
/* NOT table. Capping a fixed-width email table (width:600 with fixed cells) to
   100% squishes its columns horizontally. Let tables keep their width and scroll
   inside the pane (.ofc-mail__rich has overflow-x:auto) instead of squishing. */
.ofc-mail__rich :where(div, p, span, blockquote, ul, ol, li, h1, h2, h3, h4, h5, h6, section, article, header, footer) {
  max-width: 100%;
}
.ofc-mail__rich img { max-width: 100%; height: auto; }
/* A rendered remote image. block + a neutral placeholder tint so a tall banner
   that is still loading (or a broken one before it swaps to the alt chip) does
   not read as a rendering fault. */
.ofc-mail__img-real { display: block; max-width: 100%; height: auto; background: var(--o-line, rgba(0,0,0,0.05)); }

.ofc-mail__rich p { margin: 0 0 0.85em; }
.ofc-mail__rich p:last-child { margin-bottom: 0; }

.ofc-mail__rich h1,
.ofc-mail__rich h2,
.ofc-mail__rich h3,
.ofc-mail__rich h4 {
  margin: 1.2em 0 0.5em;
  line-height: 1.25;
  color: var(--o-fg);
  font-weight: 650;
}
.ofc-mail__rich h1 { font-size: 1.4em; }
.ofc-mail__rich h2 { font-size: 1.2em; }
.ofc-mail__rich h3,
.ofc-mail__rich h4 { font-size: 1.05em; }
.ofc-mail__rich > :first-child { margin-top: 0; }

.ofc-mail__rich ul,
.ofc-mail__rich ol { margin: 0 0 0.85em; padding-left: 1.4em; }
.ofc-mail__rich li { margin: 0.2em 0; }

.ofc-mail__rich blockquote {
  margin: 0 0 0.85em;
  padding-left: 0.9em;
  border-left: 2px solid var(--o-line);
  color: var(--o-dim);
}

.ofc-mail__rich pre {
  margin: 0 0 0.85em;
  padding: 0.7em 0.9em;
  background: var(--o-sunk, rgba(0, 0, 0, .18));
  border-radius: 3px;
  overflow-x: auto;
  white-space: pre;
}
.ofc-mail__rich code { font-size: 0.92em; }

.ofc-mail__rich hr {
  margin: 1.1em 0;
  border: 0;
  border-top: 1px solid var(--o-line);
}

/* Mail tables are layout, not data, far more often than not — so no borders by
   default. A sender that wanted them said so, and those declarations win. */
.ofc-mail__rich table { border-collapse: collapse; max-width: 100%; }
.ofc-mail__rich td,
.ofc-mail__rich th { padding: 0.15em 0.4em; vertical-align: top; text-align: left; }

/* A link the sender styled as a button keeps its own background and padding.
   This is only the floor: readable, obviously clickable, and never invisible
   against the pane if the sender set a colour that happens to match it. */
.ofc-mail__link {
  color: var(--o-accent, #6fcf97);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.ofc-mail__link:hover { text-decoration-thickness: 2px; }
/* A "designer button" link wraps its own box (a Facebook "Unlock your account",
   say). Let the sender's styling show — no forced accent colour or underline —
   and render inline-block so a link wrapping a table/div lays out cleanly and
   the whole box stays clickable. */
.ofc-mail__link--box { display: inline-block; color: inherit; text-decoration: none; }
.ofc-mail__link--box:hover { text-decoration: none; }

/* Images are not fetched. A remote image in a mail is a read receipt — it tells
   the sender the address is live, when it was opened, and from which network.
   The alt text stands in so nothing silently disappears. */
.ofc-mail__img {
  display: inline-block;
  padding: 0.1em 0.45em;
  border: 1px dashed var(--o-line);
  border-radius: 3px;
  font-size: 0.9em;
  color: var(--o-dim);
  opacity: 0.8;
}

/* CONTAINMENT, and these are not defaults. The sanitiser refuses `position`,
   `float` and `z-index`, so a message cannot escape this box by declaring its
   way out; these make the box itself hold under anything that got through. */
.ofc-mail__rich * {
  max-width: 100% !important;
  position: static !important;
  float: none !important;
}

/* A sender the receiving server did not vouch for — 0057. Amber rather than
   red: most unbadged mail is old (stored before the verdict was recorded) or
   simply arrived without an Authentication-Results header, and a red alarm on
   a routine condition is an alarm people learn to ignore. */
.ofc-mail__unverified {
  margin-left: 0.5em;
  padding: 0.05em 0.45em;
  border: 1px solid var(--o-warn, #a2660f);
  border-radius: 2px;
  font-size: 0.78em;
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--o-warn, #a2660f);
  cursor: help;
  white-space: nowrap;
}

/* ==========================================================================
   THE NOTCH, THE CLOCK AND THE HOME BAR

   Installed, the app runs `display: standalone` with `viewport-fit=cover`, so
   the web view fills the whole screen INCLUDING the strip the phone draws its
   clock, signal and battery into. app.css avoids that by padding `body` with
   env(safe-area-inset-*) — and line 242 above sets `body { padding: 0 }` for
   the right reason, because this layout is full-bleed and its scroll regions
   have to reach the edges. Nothing was left avoiding the notch.

   A FIRST ATTEMPT PUT THE INSET ON .ofc-top AND THAT WAS THE WRONG ELEMENT.
   On a phone .ofc-top is not the top of the screen: the rail collapses to its
   brand block and becomes the bar across the top (see the note at the burger
   rule further down, which says so), and .ofc-top sits BELOW it. Padding
   .ofc-top there buys a gap under the rail and leaves the rail under the clock.
   Measured rather than assumed: at rest both .ofc-rail and .ofc-top report
   top: 0, and the rail comes first.

   SO THE INSET GOES ON THE SHELL. #office is the grid that holds the rail and
   the main column, so padding it moves BOTH below the status bar in both
   layouts, with one rule and no media query — env() is 0px on a desktop, on a
   phone without a notch, and in a browser tab rather than an installed app, so
   those cases are untouched.
   ========================================================================== */
:is(#office, #owner, #portal) {
  padding-top: env(safe-area-inset-top, 0px);
  padding-left: env(safe-area-inset-left, 0px);
  padding-right: env(safe-area-inset-right, 0px);
}

/* THE STRIP ITSELF HAS TO BE PAINTED, or scrolling content shows through it.
   Padding moves the content down once; it does not mask the region, and the
   page scrolls underneath. A fixed band in the same near-black `body` already
   uses keeps the status bar legible over anything that passes beneath it.
   Height 0 on everything without a notch, so it is invisible where it is not
   needed. */
body:has(#office)::before,
body:has(#owner)::before,
body:has(#portal)::before {
  content: '';
  position: fixed;
  inset: 0 0 auto 0;
  height: env(safe-area-inset-top, 0px);
  background: #0B0E0C;
  z-index: 100;
  pointer-events: none;
}

/* AND THE STICKY HEADER HAS TO STICK BELOW IT, not at the very top. .ofc-top
   is `position: sticky; top: 0` on a phone, so shell padding alone fixes the
   view at rest and the header slides under the clock the moment somebody
   scrolls — which is the same bug arriving a second later. Offsetting the
   stick point by the inset is what keeps it clear, and it pairs with the band
   above: the header stops exactly where the band ends. */
.ofc-top {
  top: env(safe-area-inset-top, 0px);
}

/* The home indicator is a bar the phone draws OVER the bottom of the screen,
   and a control underneath it is a control that gets missed — which is the
   note app.css already makes about the Clock-in button. */
.ofc-main,
.ofc-side {
  padding-bottom: env(safe-area-inset-bottom, 0px);
}

/* ==========================================================================
   AI CHAT & ANALYTICS (data-view="analytics")
   All figures are counted client-side from four reads; these are just the
   surfaces. Tones: accent = steel (neutral series), good = the stand's green
   (a lead, a win), warn = amber (a paid model call, a handoff), bad = a muted
   red (a guard/deflection, a lost lead).
   ========================================================================== */
.ofc-anl__bar {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  flex-wrap: wrap; margin-bottom: 1rem;
}
.ofc-anl__range { display: flex; gap: 2px; background: var(--o-inset); border-radius: var(--o-pill); padding: 3px; }
.ofc-anl__r {
  appearance: none; border: 0; cursor: pointer; background: transparent;
  color: var(--o-dim); font: inherit; font-size: var(--o-t-sm); font-weight: 600;
  padding: 0.4rem 0.85rem; border-radius: var(--o-pill);
}
.ofc-anl__r:hover { color: var(--o-text); }
.ofc-anl__r.is-on { background: var(--o-accent-deep); color: #fff; }
.ofc-anl__r:focus-visible { outline: 2px solid var(--o-accent); outline-offset: 2px; }

#ofc-anl-root { display: grid; gap: 1.1rem; }
.ofc-anl__card { padding: clamp(1rem, 2vw, 1.5rem); }
.ofc-anl__head { margin-bottom: 1rem; }
.ofc-anl__title { margin: 0; font-family: var(--display, inherit); font-size: var(--o-t-lg); font-weight: 700; letter-spacing: -0.01em; color: var(--o-text); }
.ofc-anl__sub { margin: 0.2rem 0 0; font-size: var(--o-t-sm); color: var(--o-faint); }
.ofc-anl__note { margin: 0.9rem 0 0; font-size: var(--o-t-sm); color: var(--o-dim); line-height: 1.55; }

.ofc-anl__kpis { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }

/* horizontal magnitude bars */
.ofc-anl__bars { display: grid; gap: 0.5rem; }
.ofc-anl__barrow { display: grid; grid-template-columns: minmax(90px, 0.4fr) 1fr auto; align-items: center; gap: 0.7rem; font-size: var(--o-t-sm); }
.ofc-anl__barlbl { color: var(--o-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ofc-anl__bartrack { height: 12px; background: var(--o-inset); border-radius: 999px; overflow: hidden; }
.ofc-anl__barfill { display: block; height: 100%; border-radius: 999px; background: var(--o-accent); min-width: 2px; transition: width 0.4s ease; }
.ofc-anl__barfill.is-accent { background: var(--o-accent); }
.ofc-anl__barfill.is-good   { background: var(--o-accent-deep); }
.ofc-anl__barfill.is-warn   { background: var(--o-warn); }
.ofc-anl__barfill.is-bad    { background: #8f3a2a; }
.ofc-anl__barval { color: var(--o-text); font-weight: 650; font-variant-numeric: tabular-nums; }

/* line/area charts */
.ofc-anl__chartwrap { display: block; }
.ofc-anl__chart { width: 100%; height: 120px; display: block; }
.ofc-anl__area { fill: var(--o-accent-wash); stroke: none; }
.ofc-anl__area.is-good { fill: rgba(51, 112, 60, 0.15); }
.ofc-anl__linepath { fill: none; stroke: var(--o-accent); stroke-width: 2; vector-effect: non-scaling-stroke; }
.ofc-anl__linepath.is-good { stroke: var(--o-accent-deep); }
.ofc-anl__linepath.is-accent { stroke: var(--o-accent); }
.ofc-anl__chartfoot { display: flex; justify-content: space-between; font-size: var(--o-t-xs); color: var(--o-faint); margin-top: 0.3rem; }
.ofc-anl__peak { color: var(--o-dim); }

.ofc-anl__trends { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 1.2rem; }
.ofc-anl__two { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1.2rem 2rem; }
.ofc-anl__trendlbl { margin: 0 0 0.5rem; font-size: var(--o-t-xs); text-transform: uppercase; letter-spacing: 0.08em; color: var(--o-faint); font-weight: 700; }

/* the conversation log */
.ofc-anl__filters { margin-bottom: 0.8rem; }
.ofc-anl__search {
  width: 100%; max-width: 340px; padding: 0.5rem 0.8rem;
  border: 1px solid var(--o-line); border-radius: var(--o-r-sm);
  background: var(--o-inset); color: var(--o-text); font: inherit; font-size: var(--o-t-sm);
}
.ofc-anl__search:focus-visible { outline: 2px solid var(--o-accent); outline-offset: 1px; }
.ofc-anl__convo { display: grid; gap: 1px; max-height: 460px; overflow-y: auto; border: 1px solid var(--o-line); border-radius: var(--o-r-sm); background: var(--o-line); }
.ofc-anl__q { display: grid; grid-template-columns: auto 1fr auto auto; align-items: center; gap: 0.7rem; padding: 0.55rem 0.8rem; background: var(--o-panel); font-size: var(--o-t-sm); }
.ofc-anl__qtext { color: var(--o-text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ofc-anl__qwhen { color: var(--o-faint); font-size: var(--o-t-xs); font-variant-numeric: tabular-nums; }
.ofc-anl__tier { font-size: var(--o-t-xs); font-weight: 700; padding: 0.1rem 0.45rem; border-radius: 999px; text-transform: uppercase; letter-spacing: 0.04em; white-space: nowrap; }
.ofc-anl__tier.is-corpus  { background: rgba(51,112,60,0.18);  color: #9fce9f; }
.ofc-anl__tier.is-model   { background: var(--o-warn-bg);      color: var(--o-warn-ink); }
.ofc-anl__tier.is-refused { background: rgba(143,58,42,0.22);  color: #e8a08c; }
.ofc-anl__tier.is-other   { background: var(--o-inset);        color: var(--o-dim); }
.ofc-anl__unans { font-size: var(--o-t-xs); color: #e8a08c; font-weight: 650; white-space: nowrap; }

/* Dismiss on a CRM lead card — a small underlined text link that asks yes/no,
   then bins a junk/test enquiry to spam (recoverable from the spam column). */
.ofc-board__dismiss-wrap {
  justify-self: end;
  margin-top: 0.35rem;
  display: flex;
  align-items: center;
  gap: 0.55rem;
}
.ofc-board__dismiss {
  padding: 0;
  border: 0;
  background: transparent;
  font: inherit;
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--o-faint, #8B959C);
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}
.ofc-board__dismiss:hover { color: var(--o-dim, #AEB8BF); }
.ofc-board__dismiss--go { color: var(--o-warn-ink, #E8A87C); }
.ofc-board__dismiss--go:hover { color: var(--o-warn, #C4772F); }
.ofc-board__dismiss:disabled { opacity: 0.55; cursor: default; }
.ofc-board__dismiss-q { font-size: 0.72rem; color: var(--o-dim, #AEB8BF); }

/* Openable inbound-email attachments. A button per file (no URL exists until
   the office clicks — inbound-attachment mints a short-lived signed one). */
.ofc-attach__open {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.1rem;
  width: 100%;
  text-align: left;
  padding: 0.5rem 0.7rem;
  background: var(--o-inset, rgba(255, 255, 255, 0.04));
  border: 1px solid var(--o-line, rgba(255, 255, 255, 0.12));
  border-radius: var(--o-r-sm, 3px);
  color: var(--o-text, #fff);
  cursor: pointer;
  transition: background 0.12s ease, border-color 0.12s ease;
}
.ofc-attach__open:hover {
  background: var(--o-hover, rgba(255, 255, 255, 0.08));
  border-color: var(--o-accent, #9DB2C0);
}
.ofc-attach__open b { font-weight: 650; }
.ofc-attach__open small { color: var(--o-dim, #AEB8BF); font-size: 0.78rem; }
.ofc-attach__open:disabled { opacity: 0.6; cursor: default; }
.ofc-attach__err {
  display: block;
  color: var(--o-warn-ink, #E8A87C);
  font-size: 0.78rem;
  margin-top: 0.2rem;
}

/* ---------------------------------------------------------------------------
 * QUICK-WIN CONTROLS — wiring for functions that always existed with no UI:
 * editComponent (recipe steps), voidInvoice, setClaimStatus / decideClaim.
 * All three reuse .ofc-mini for buttons; these rules only lay them out inline
 * (ofc-mini is display:block by default, which would stack them) and add the
 * small labelled number fields the recipe steps need.
 * ------------------------------------------------------------------------- */

/* A shared "danger" tint for the confirm button of a destructive action. */
button.ofc-mini--danger {
  color: var(--o-warn-ink, #E8A87C);
  border-color: color-mix(in srgb, var(--o-warn, #B4682F) 55%, var(--o-line-2));
}

/* Recipe steps — one editable row per component. */
.ofc-steps {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  margin: 0.5rem 0 0.15rem;
  padding-top: 0.5rem;
  border-top: 1px dashed var(--o-line-2);
}
.ofc-step {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem 0.7rem;
}
.ofc-step__name {
  flex: 1 1 9rem;
  min-width: 7rem;
  font-size: var(--o-t-xs);
  color: var(--o-dim);
}
.ofc-step__f {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
}
.ofc-step__fl {
  font-size: var(--o-t-xs);
  color: var(--o-faint);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.ofc-step__fs { font-size: var(--o-t-xs); color: var(--o-faint); }
input.ofc-step__in {
  width: 4.5rem;
  min-height: 26px;
  padding: 0.15rem 0.4rem;
  border: 1px solid var(--o-line-2);
  border-radius: var(--o-r-sm);
  background: var(--o-inset);
  color: var(--o-text);
  font: 500 var(--o-t-xs)/1.2 var(--body);
  text-align: right;
  font-variant-numeric: tabular-nums;
}
input.ofc-step__in:focus-visible {
  outline: none;
  border-color: rgba(154, 203, 178, 0.65);
  box-shadow: 0 0 0 3px rgba(154, 203, 178, 0.16);
}

/* Invoice void — an inline reason field beside the confirm/cancel buttons. */
td.ofc-inv__act { white-space: nowrap; }
.ofc-void { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 0.3rem; }
.ofc-void button.ofc-mini { display: inline-block; margin-top: 0; }
input.ofc-void__reason {
  width: 8rem;
  min-height: 26px;
  padding: 0.15rem 0.4rem;
  border: 1px solid var(--o-line-2);
  border-radius: var(--o-r-sm);
  background: var(--o-inset);
  color: var(--o-text);
  font: 500 var(--o-t-xs)/1.2 var(--body);
}

/* Warranty claim actions — a status select that commits on change, plus a
   collapsible "Decide" form. */
.ofc-claim__act {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem 0.8rem;
  margin-top: 0.45rem;
}
.ofc-claim__f { display: inline-flex; align-items: center; gap: 0.35rem; }
.ofc-claim__fl {
  font-size: var(--o-t-xs);
  color: var(--o-faint);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.ofc-claim__decide { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 0.3rem; }
.ofc-claim__decide button.ofc-mini { display: inline-block; margin-top: 0; }
select.ofc-claim__sel {
  min-height: 26px;
  padding: 0.15rem 0.4rem;
  border: 1px solid var(--o-line-2);
  border-radius: var(--o-r-sm);
  background: var(--o-inset);
  color: var(--o-text);
  font: 500 var(--o-t-xs)/1.2 var(--body);
}
input.ofc-claim__reason {
  width: 12rem;
  max-width: 100%;
  min-height: 26px;
  padding: 0.15rem 0.4rem;
  border: 1px solid var(--o-line-2);
  border-radius: var(--o-r-sm);
  background: var(--o-inset);
  color: var(--o-text);
  font: 500 var(--o-t-xs)/1.2 var(--body);
}
.ofc-claim__sel:focus-visible,
.ofc-claim__reason:focus-visible,
.ofc-void__reason:focus-visible {
  outline: none;
  border-color: rgba(154, 203, 178, 0.65);
  box-shadow: 0 0 0 3px rgba(154, 203, 178, 0.16);
}
input.ofc-claim__sel { color-scheme: dark; }

/* Jobs — the per-row editor, revealed in a detail row beneath the job. */
tr.ofc-jb__edit > td { padding: 0.5rem 0.7rem 0.9rem; background: var(--o-inset); }
.ofc-jb__form {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 0.7rem 1rem;
}
.ofc-jb__field { display: flex; flex-direction: column; gap: 0.25rem; }
.ofc-jb__flabel {
  font-size: var(--o-t-xs);
  color: var(--o-faint);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.ofc-jb__form .ofc-ghost { align-self: flex-end; }

/* Crews — the inline booking editor under a today's-booking row. */
.ofc-today__edit { flex-basis: 100%; margin-top: 0.4rem; }

/* Permits — the engineered-design opt-in inside the row editor. */
details.ofc-pm__eng { margin-top: 0.3rem; }
details.ofc-pm__eng > summary {
  cursor: pointer; font-size: var(--o-t-xs); color: var(--o-dim);
  text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 0.4rem;
}

/* Contracts — the execute / correct / void lifecycle block in the detail pane. */
.ofc-con__life {
  margin: 0.6rem 0 0.4rem;
  padding-top: 0.6rem;
  border-top: 1px solid var(--o-line-2);
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.ofc-con__life-row { margin: 0; }
.ofc-con__void,
.ofc-con__correct { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; }
.ofc-con__void button.ofc-mini,
.ofc-con__correct button.ofc-mini { display: inline-block; margin-top: 0; }
.ofc-con__correct-form { margin-top: 0.3rem; }

/* CRM — the free-text note box in the lead detail pane. */
.ofc-lead__addnote { display: flex; flex-direction: column; gap: 0.4rem; align-items: flex-start; }
textarea.ofc-lead__notebox,
textarea.ofc-claim__reason {
  width: 100%;
  min-height: 2.4rem;
  padding: 0.35rem 0.5rem;
  border: 1px solid var(--o-line-2);
  border-radius: var(--o-r-sm);
  background: var(--o-inset);
  color: var(--o-text);
  font: 400 var(--o-t-sm, 0.8125rem)/1.4 var(--body);
  resize: vertical;
}
textarea.ofc-lead__notebox:focus-visible,
textarea.ofc-claim__reason:focus-visible {
  outline: none;
  border-color: rgba(154, 203, 178, 0.65);
  box-shadow: 0 0 0 3px rgba(154, 203, 178, 0.16);
}

/* Calls & texts — new-enquiry-from-a-number and the dial links. */
.ofc-ph__newlead { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 0.3rem; }
.ofc-ph__newlead button.ofc-mini { display: inline-block; margin-top: 0; }
input.ofc-ph__newname {
  min-height: 26px;
  padding: 0.15rem 0.4rem;
  border: 1px solid var(--o-line-2);
  border-radius: var(--o-r-sm);
  background: var(--o-inset);
  color: var(--o-text);
  font: 500 var(--o-t-xs)/1.2 var(--body);
}
input.ofc-ph__newname:focus-visible {
  outline: none;
  border-color: rgba(154, 203, 178, 0.65);
  box-shadow: 0 0 0 3px rgba(154, 203, 178, 0.16);
}
a.ofc-ph__dial { text-decoration: none; }

/* Calls & texts — inbound MMS photos. */
.ofc-ph__media { display: flex; flex-wrap: wrap; gap: 0.4rem; margin: 0.4rem 0 0.2rem; }
img.ofc-ph__photo {
  max-width: 180px; max-height: 180px; width: auto; height: auto;
  border-radius: var(--o-r-sm); border: 1px solid var(--o-line-2);
  object-fit: cover; background: var(--o-inset);
}
.ofc-ph__note { display: block; font-size: var(--o-t-xs); color: var(--o-faint); margin-top: 0.3rem; }

/* Buying — PO cancel confirm, line tax select, supplier edit/retire. */
.ofc-po__cancel { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 0.3rem; }
.ofc-po__cancel button.ofc-mini { display: inline-block; margin-top: 0; }
select.ofc-po__tax {
  min-height: 30px;
  padding: 0.15rem 0.4rem;
  border: 1px solid var(--o-line-2);
  border-radius: var(--o-r-sm);
  background: var(--o-inset);
  color: var(--o-text);
  font: 500 var(--o-t-sm, 0.8125rem)/1.2 var(--body);
}
.ofc-sup__act { display: inline-flex; flex-wrap: wrap; gap: 0.3rem; margin-top: 0.3rem; }
.ofc-sup__act button.ofc-mini { display: inline-block; margin-top: 0; }
.ofc-sup__edit { margin-top: 0.4rem; }
li.is-retired > .ofc-co__meta,
li.is-retired { color: var(--o-faint); }

/* ===========================================================================
 * CUSTOMER-FACING DOCUMENT — a printable estimate / contract / invoice. Always
 * black on white, whatever the admin theme, because it is printed and handed
 * over. The overlay covers the app on screen; @media print shows only the sheet.
 * ======================================================================== */
.ofc-doc {
  position: fixed; inset: 0; z-index: 200;
  background: rgba(8, 10, 12, 0.72);
  display: flex; flex-direction: column;
}
.ofc-doc__bar {
  flex: none; display: flex; flex-wrap: wrap; gap: 0.6rem; justify-content: flex-end; align-items: center;
  padding: 0.7rem 1rem; background: var(--o-panel, #14181c);
  border-bottom: 1px solid var(--o-line-2, #333c44);
}
.ofc-doc__email { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin-right: auto; }
.ofc-doc__email input {
  min-height: 34px; min-width: 14rem; padding: 0.2rem 0.5rem;
  border: 1px solid var(--o-line-2, #333c44); border-radius: var(--o-r-sm, 3px);
  background: var(--o-inset, #1d2226); color: var(--o-text, #e7ece2);
  font: 500 0.85rem/1.2 var(--body);
}
.ofc-doc__email .ofc-note { color: var(--o-dim, #aeb8bf); font-size: 0.78rem; }
button.ofc-ph__dial { background: transparent; border: 0; padding: 0; cursor: pointer; }
.ofc-doc__scroll { flex: 1; overflow: auto; padding: 1.5rem 1rem; display: flex; justify-content: center; }
.ofc-doc__sheet {
  width: 100%; max-width: 760px; align-self: flex-start;
  background: #ffffff; color: #14181c;
  padding: 3rem 3.2rem; border-radius: 4px;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
  font: 14px/1.5 -apple-system, "Segoe UI", system-ui, sans-serif;
}
.ofc-doc__sheet h1 { font-size: 1.6rem; margin: 0 0 0.1rem; letter-spacing: -0.01em; }
.ofc-doc__hd { display: flex; justify-content: space-between; align-items: flex-start; gap: 1.5rem; border-bottom: 2px solid #14181c; padding-bottom: 1rem; margin-bottom: 1.4rem; }
.ofc-doc__co b { font-size: 1.15rem; letter-spacing: 0.02em; }
.ofc-doc__co span { display: block; color: #55606a; font-size: 0.82rem; }
.ofc-doc__meta { text-align: right; font-size: 0.85rem; color: #34404a; white-space: nowrap; }
.ofc-doc__meta b { font-size: 1.05rem; color: #14181c; }
.ofc-doc__parties { display: flex; gap: 2.5rem; margin-bottom: 1.4rem; }
.ofc-doc__party { font-size: 0.88rem; }
.ofc-doc__party .lbl { display: block; text-transform: uppercase; letter-spacing: 0.06em; font-size: 0.68rem; color: #7a838c; margin-bottom: 0.2rem; }
.ofc-doc__sheet table { width: 100%; border-collapse: collapse; margin: 0.6rem 0 1rem; font-size: 0.9rem; }
.ofc-doc__sheet th { text-align: left; border-bottom: 1.5px solid #14181c; padding: 0.4rem 0.5rem; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; color: #55606a; }
.ofc-doc__sheet th.num, .ofc-doc__sheet td.num { text-align: right; font-variant-numeric: tabular-nums; }
.ofc-doc__sheet td { padding: 0.45rem 0.5rem; border-bottom: 1px solid #e4e8ea; vertical-align: top; }
.ofc-doc__sheet tr.ofc-doc__opt td { color: #55606a; font-style: italic; }
.ofc-doc__sheet tfoot td { border-bottom: none; border-top: 2px solid #14181c; font-weight: 700; font-size: 1rem; padding-top: 0.6rem; }
.ofc-doc__sect { margin: 1.4rem 0; }
.ofc-doc__sect h2 { font-size: 0.95rem; text-transform: uppercase; letter-spacing: 0.05em; color: #34404a; border-bottom: 1px solid #cfd6da; padding-bottom: 0.3rem; margin: 0 0 0.6rem; }
.ofc-doc__terms { font-size: 0.82rem; color: #34404a; line-height: 1.6; }
.ofc-doc__terms p { margin: 0 0 0.6rem; }
.ofc-doc__sign { display: flex; gap: 3rem; margin-top: 2.5rem; }
.ofc-doc__sign > div { flex: 1; }
.ofc-doc__sign .line { border-top: 1px solid #14181c; margin-top: 2.5rem; padding-top: 0.3rem; font-size: 0.75rem; color: #55606a; }
.ofc-doc__foot { margin-top: 2rem; padding-top: 1rem; border-top: 1px solid #cfd6da; font-size: 0.72rem; color: #7a838c; text-align: center; }
.ofc-doc__note { font-size: 0.78rem; color: #7a838c; }
@media (max-width: 560px) {
  .ofc-doc__sheet { padding: 1.6rem 1.2rem; }
  .ofc-doc__hd, .ofc-doc__parties, .ofc-doc__sign { flex-direction: column; gap: 0.8rem; }
  .ofc-doc__meta { text-align: left; }
}
@media print {
  body > *:not(.ofc-doc) { display: none !important; }
  .ofc-doc { position: static; background: #fff; display: block; }
  .ofc-doc__bar { display: none !important; }
  .ofc-doc__scroll { overflow: visible; padding: 0; display: block; }
  .ofc-doc__sheet { max-width: none; box-shadow: none; border-radius: 0; padding: 0.3in 0.5in; }
}

/* Mailbox — the saved-signature editor inside the composer. */
.ofc-sig { margin: 0.2rem 0 0.4rem; font-size: var(--o-t-sm, 0.8125rem); }
.ofc-sig > summary { cursor: pointer; color: var(--o-dim); font-size: var(--o-t-xs); text-transform: uppercase; letter-spacing: 0.04em; }
textarea#ofc-compose-sig {
  width: 100%;
  margin-top: 0.3rem;
  padding: 0.4rem 0.5rem;
  border: 1px solid var(--o-line-2);
  border-radius: var(--o-r-sm);
  background: var(--o-inset);
  color: var(--o-text);
  font: 400 var(--o-t-sm, 0.8125rem)/1.4 var(--body);
  resize: vertical;
}

/* Catalogue — the bulk price-change control. */
.ofc-bulk {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 0.5rem 0.8rem;
  margin: 0.5rem 0 0.2rem;
  padding: 0.6rem 0.7rem;
  background: var(--o-inset);
  border: 1px solid var(--o-line-2);
  border-radius: var(--o-r-sm);
}
.ofc-bulk__lbl {
  font-size: var(--o-t-xs);
  color: var(--o-dim);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  align-self: center;
}
.ofc-bulk__f { display: flex; flex-direction: column; gap: 0.2rem; font-size: var(--o-t-xs); color: var(--o-faint); }
.ofc-bulk__f select, .ofc-bulk__f input {
  min-height: 30px;
  padding: 0.15rem 0.4rem;
  border: 1px solid var(--o-line-2);
  border-radius: var(--o-r-sm);
  background: var(--o-bg, #12171b);
  color: var(--o-text);
  font: 500 var(--o-t-sm, 0.8125rem)/1.2 var(--body);
}
.ofc-bulk__f input { width: 8rem; }

/* Dashboard — KPI tiles that drill through to their tab. */
.ofc-stat--goto { cursor: pointer; transition: border-color 0.12s, transform 0.12s; }
.ofc-stat--goto:hover { border-color: var(--o-accent, #9DB2C0); }
.ofc-stat--goto:active { transform: translateY(1px); }
.ofc-stat--goto:focus-visible {
  outline: none;
  border-color: rgba(154, 203, 178, 0.65);
  box-shadow: 0 0 0 3px rgba(154, 203, 178, 0.16);
}
