/* ============================================================================
   Hank — the canvas
   ----------------------------------------------------------------------------
   THE SURFACE THE APP SITS ON, and the second half of its design system.

   harris.css is the first half: tokens, type scale, spacing scale, buttons,
   inputs, tables. That layer says what a control looks like. This one says what
   the PAGE looks like — the ground, the rail, the shape of a card, how much air
   there is between things.

   WHY IT IS A SEPARATE FILE RATHER THAN MORE harris.css. Every page in the app
   loads harris.css, so a ground written there would repaint nineteen pages in
   one commit — nineteen pages nobody had looked at. This is opted into: a page
   loads it, adds `hb-canvas` to its body, and is measured. The remaining pages
   arrive one at a time, each with its own before and after.

   WHAT IT IS NOT. A second palette. Every colour here is a harris.css token or
   a wash over one, and the type and spacing scales are untouched — a second set
   of either would be a second design system to keep in step, which is how a
   codebase ends up with two of everything and agreement on neither.
   ============================================================================ */

/* ── The ground ──────────────────────────────────────────────────────────────
   Three low-saturation washes over the app's own surface colour, so the page
   reads as WARM rather than as coloured. At full strength this is a gradient
   somebody has to look past to read the words on top of it, which is the
   opposite of the point. */
.hb-canvas {
  margin: 0;
  background: var(--surface);
  color: var(--ink);
  min-height: 100vh;
}

/* ── How much room the rail takes, as one number ─────────────────────────────
   `.hb-room` and `.hb-railed` both need it, and so does anything a page
   positions AGAINST the viewport rather than in the flow — a fixed submit bar
   is placed against the window, so no amount of padding on an ancestor moves
   it. Those pages read this token instead of restating 68 and its two
   responsive values, which is what let PM EOM Review's bar keep an offset
   scoped to the wrong opt-in when the page changed from one to the other. */
.hb-canvas,
/* --hb-rail-h is the phone bar's height, for anything a page fixes to the
   bottom of the screen (PM Review's submit bar sat on top of the bar, and the
   page had to restate 64px to lift it). */
.hb-railed { --hb-rail-w: 68px; --hb-rail-h: 64px; }
@media (pointer: coarse) {
  .hb-canvas,
  .hb-railed { --hb-rail-w: 76px; }
}
@media (max-width: 768px) {
  .hb-canvas,
  .hb-railed { --hb-rail-w: 0px; }
}

/* ── The rail without the ground ─────────────────────────────────────────────
   `hb-railed` is the lighter opt-in: a page gets the one navigation and keeps
   its own colours. It is what lets the rail reach every module that has room
   for it, before every module has been restyled — the alternative was
   repainting a dozen pages nobody had reviewed in the commit that gave them
   a menu.

   THE ROOM IS THE BODY, and that is the point of doing it this way. A
   converted page needs no structural edit: no wrapper element, no re-indent,
   nothing for a merge to fight over. Padding rather than margin so the page's
   own background still reaches the full width — and `box-sizing: border-box`
   from each page's reset is what stops it widening the document. */
.hb-railed {
  padding-left: var(--hb-rail-w);
  min-height: 100vh;
}
/* The bar's height plus the 20px of clearance .hb-room leaves, read from the
   token rather than restated as 84 — which is the number two converted pages
   had already copied out of here by hand. */
@media (max-width: 768px) { .hb-railed { padding-bottom: calc(var(--hb-rail-h) + 20px); } }

/* A GROUND OF ITS OWN, only here. On a canvas page the rail floats over the
   washes and needs nothing behind it; on a page that has kept its grey, icons
   in the left margin read as icons in the left margin. A surface and a
   hairline make it a rail. */
.hb-railed .hb-rail {
  background: var(--surface);
  border-right: 1px solid var(--border);
}
@media (max-width: 768px) {
  .hb-railed .hb-rail { border-right: 0; }
}

.hb-sky {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background:
    radial-gradient(58rem 40rem at 8% 4%, rgba(255, 214, 170, .25), transparent 62%),
    radial-gradient(52rem 38rem at 96% 22%, rgba(208, 194, 248, .16), transparent 60%),
    radial-gradient(64rem 46rem at 76% 100%, rgba(186, 232, 220, .22), transparent 64%);
}

/* A whisper of grain. A pure gradient this wide BANDS on an ordinary monitor —
   visible steps across the peach, which reads as a rendering fault rather than
   as a background. */
.hb-grain {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: .02;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ── The rail ────────────────────────────────────────────────────────────────
   OUR ICONS, from shared/modules.js, so it cannot drift from what the app
   actually has: a module added there appears here, and one renamed is renamed
   here. Filtered to what the person may open — a rail offering doors that
   answer 403 turns a permission boundary into a list of things somebody is
   missing out on. shared/harris-rail.js does the building. */
.hb-rail {
  position: fixed;
  left: 0;
  top: 0;
  bottom: 0;
  width: 68px;
  z-index: 30;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-4) 0;
}

/* The brand mark, in a tile. It carries .hb-brand too, so whatever harris.js
   puts inside it — the uploaded icon, or the letter when nothing is uploaded —
   is sized by harris.css's lockup rules. `--hb-logo-h` is the one knob that
   system exposes, and 30px is what puts a ~22px mark inside a 40px tile. */
.hb-rail-mark {
  --hb-logo-h: 30px;
  width: 40px;
  height: 40px;
  border-radius: 12px;
  display: grid;
  place-items: center;
  background: rgba(255, 255, 255, .72);
  border: 1px solid rgba(255, 255, 255, .9);
  box-shadow: 0 1px 3px rgba(21, 23, 26, .06);
  margin-bottom: var(--space-3);
  font-weight: var(--w-bold);
  color: var(--ink);
  text-decoration: none;
  flex-shrink: 0;
}

.hb-rail-links {
  display: flex;
  flex-direction: column;
  gap: 2px;
  align-items: center;
  overflow-y: auto;
  overflow-x: hidden;
  flex: 1;
  width: 100%;
  scrollbar-width: none;
}
.hb-rail-links::-webkit-scrollbar { display: none; }

/* EACH MODULE IN ITS OWN COLOUR — `--rl-ink` and `--rl-soft` come off the
   registry, set per link by shared/harris-rail.js, and are the same pair the
   module's tile on the grid and the mark in its page heading already use. A
   rail of twelve identical grey glyphs makes somebody read every label to find
   the one they want; twelve colours they already know from the grid makes it a
   glance. The fallbacks are what a link with no module behind it would get. */
.hb-rail a.hb-rl {
  position: relative;
  width: 40px;
  height: 40px;
  border-radius: 11px;
  display: grid;
  place-items: center;
  color: var(--rl-ink, var(--ink-3));
  text-decoration: none;
  flex-shrink: 0;
  transition: background .12s ease, color .12s ease, box-shadow .12s ease;
}
.hb-rail a.hb-rl svg { width: 19px; height: 19px; }
.hb-rail a.hb-rl:hover { background: var(--rl-soft, rgba(255, 255, 255, .8)); }
/* THE CURRENT ONE IS THE TINT PLUS A LIFT, not a different colour. Colour
   already says which module; making the active state a fourth treatment would
   mean the rail was saying two things at once with one variable. */
.hb-rail a.hb-rl.here {
  background: var(--rl-soft, var(--surface));
  box-shadow: 0 1px 3px rgba(21, 23, 26, .12);
}

/* The name of the thing, on hover, because nineteen pixels of line art is not
   a label. Positioned rather than a `title` attribute: a tooltip that takes a
   second to appear is not there when somebody is scanning.

   FIXED, NOT ABSOLUTE, AND THAT IS THE WHOLE BUG. This rule shipped with
   `position: absolute` and had never once been seen: `.hb-rail-links` above is
   a scroll container (`overflow-y: auto`, `overflow-x: hidden`), and a scroll
   container clips its descendants at its padding box — 68px, the width of the
   rail. The label was laid out, 112px wide at the right offset, with
   `opacity: 1` on hover, and every property you could read back said it was
   there. It was drawn entirely inside the clip. Only a screenshot said
   otherwise.

   `position: fixed` escapes it, because an ancestor's `overflow` does not clip
   a fixed box — only an ancestor that establishes a containing block for one
   does (a transform, filter, perspective or `contain`), and nothing in the
   rail has any. `top` is deliberately left alone so it resolves to the static
   position, which for a fixed box is its hypothetical position in the
   viewport — and the rail is itself fixed at the top, so the label lines up
   with the icon it belongs to without JavaScript measuring anything.

   THE WHITE PILL IS THE KINSO TREATMENT: a light surface with a soft shadow
   rather than the dark chip this had, so it reads as a small card lifting off
   the ground the rest of the canvas is made of. */
.hb-rail a.hb-rl::after {
  content: attr(data-label);
  position: fixed;
  left: calc(var(--hb-rail-w, 68px) + 8px);
  background: var(--surface);
  color: var(--ink);
  font-size: var(--t-small);
  font-weight: var(--w-medium);
  line-height: 1.3;
  padding: 6px 10px;
  border-radius: 10px;
  white-space: nowrap;
  box-shadow:
    0 0 0 1px rgba(21, 23, 26, .05),
    0 4px 14px -4px rgba(21, 23, 26, .25);
  opacity: 0;
  transform: translateX(-4px);
  transition: opacity .12s ease, transform .12s ease;
  pointer-events: none;
  z-index: 40;
}
/* ON FOCUS TOO. Somebody tabbing the rail gets exactly the same problem the
   hover label solves — nineteen pixels of line art and no name — and the
   answer is already written. */
.hb-rail a.hb-rl:hover::after,
.hb-rail a.hb-rl:focus-visible::after { opacity: 1; transform: translateX(0); }

.hb-rail-foot {
  margin-top: auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
}
/* ── How far through setting up their profile ────────────────────────────────
   Above the avatar, because that is where somebody already looks for anything
   about themselves, and pressing it reopens the setup.

   A VERTICAL BAR, because the rail is a vertical strip 68px wide. A horizontal
   progress bar in a column that narrow is 40px long, which is not enough
   length to read a proportion off — the eye reads the height of a column far
   better than the width of a stub. It fills from the bottom, the way anything
   filling does.

   IT DISAPPEARS WHEN THE PROFILE IS DONE, which reverses what this used to
   say. The old argument was that a control vanishing on success leaves
   somebody wondering whether they broke it. Against it: the rail is on every
   page, so a permanent 100 is permanent chrome about a finished job — a
   prompt with nothing left to prompt. There is nothing here to style for the
   done state, because the done state is not drawn. */
.hb-setup {
  /* One 38px square holding a 30px ring with the figure centred over it.
     `position: relative` is what the figure is absolutely placed against. */
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  border: 0;
  background: none;
  cursor: pointer;
  font: inherit;
  border-radius: var(--radius-3);
}
.hb-setup:hover { background: rgba(255, 255, 255, .55); }
.hb-setup:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }

/* ── The setup ring ───────────────────────────────────────────────────────
   A DONUT, NOT A BAR, and the reason is the rail's width. The bar was 4px
   wide and 34px tall: a whole column of vertical space spent on one number,
   in a rail that is 56px across and has doors to fit. A ring says the same
   thing in a square and puts the figure in the middle of it.

   The geometry is one number. `pathLength="100"` on both circles redefines
   the circumference as 100 whatever the radius is, so the dash array IS the
   percentage and nothing has to compute 2πr — which is the line that goes
   wrong the first time somebody changes the size. */
.hb-setup-ring {
  width: 30px;
  height: 30px;
  display: block;
  /* Twelve o'clock, not three. A progress ring that starts at the right edge
     reads as an arbitrary slice; starting at the top reads as filling up. */
  transform: rotate(-90deg);
}
.hb-setup-ring circle {
  fill: none;
  stroke-width: 3.25;
}
.hb-setup-track { stroke: rgba(21, 23, 26, .12); }
.hb-setup-fill {
  stroke: var(--blue);
  /* The dash is the percentage, and the gap is the rest of the circle. */
  stroke-dasharray: var(--hb-setup-progress, 0) 100;
  stroke-linecap: round;
  transition: stroke-dasharray .5s cubic-bezier(.4, 0, .2, 1), stroke .3s ease;
}
/* The green "done" ring used to live here, and has gone with the state it
   drew: the button is hidden at 100, so a rule for it could never match and
   would read as a state somebody could still reach. */

/* THE FIGURE SITS INSIDE THE RING, which is the space the donut buys back:
   one square rather than a bar with a number stacked under it. */
.hb-setup-pct {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--t-micro);
  font-weight: var(--w-medium);
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
  line-height: 1;
  /* The "%" is dropped inside the ring — at 10.5px in a 30px circle the two
     characters of the number are the readable part and the sign is noise. The
     button's aria-label still says what it means. */
  letter-spacing: -.02em;
}

/* The dash animates, so it is one of the things that stops when motion is
   turned off — it still arrives at the right length, it just gets there
   without travelling. */
@media (prefers-reduced-motion: reduce) {
  .hb-setup-fill { transition: none; }
}

/* On a phone the rail is a bar along the bottom and its foot is a row. The
   ring needs no rearranging for that — it is a square either way — so this
   only trims it to the shorter bar. */
@media (max-width: 768px) {
  .hb-setup { width: 32px; height: 32px; padding: 0; }
  .hb-setup-ring { width: 26px; height: 26px; }
}

/* A BUTTON, not a link, because signing out is a POST — see the note in
   shared/harris-rail.js. So it carries the reset a button needs to stop
   looking like one. */
.hb-rail-me {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: var(--ink);
  color: var(--surface);
  font-size: var(--t-small);
  font-weight: var(--w-medium);
  font-family: inherit;
  text-decoration: none;
  border: 0;
  padding: 0;
  cursor: pointer;
}
.hb-rail-me:hover { opacity: .82; }
/* The person's own photo, set on their profile, in place of the initials. */
.hb-rail-me-img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; display: block; }
.hb-rail-me:focus-visible { outline: 2px solid var(--blue); outline-offset: 3px; }

/* 44px ON A FINGER, AND THE POINTER DECIDES — NOT THE WIDTH. These are 40px
   and 36px, which is right beside a cursor and short of what the app requires
   of a touch target. Scoped by width first, they passed on a phone and failed
   on a tablet: an 800px touch screen still gets the vertical rail, and the
   width sweep found it at exactly those two viewports. */
@media (pointer: coarse) {
  .hb-rail a.hb-rl,
  .hb-rail-mark,
  .hb-rail-me { width: 44px; height: 44px; }
  .hb-rail { width: 76px; }
}

/* Phones get the rail as a bottom bar: a 68px column beside a content column
   leaves neither enough room. */
@media (max-width: 768px) {
  .hb-rail {
    top: auto;
    right: 0;
    width: auto;
    height: var(--hb-rail-h);
    flex-direction: row;
    justify-content: center;
    gap: var(--space-1);
    padding: 0 var(--space-3);
    background: rgba(255, 255, 255, .86);
    border-top: 1px solid var(--border);
    backdrop-filter: blur(12px);
  }
  .hb-rail-mark { display: none; }
  /* THE FOOT STAYS, TURNED ON ITS SIDE, and it used to be hidden with the
     brand mark beside it.

     That cost more than it looked. The foot is not decoration: it holds the
     account button — the only way to reach your profile or sign out from any
     page — and the setup ring, which is the only way back into the profile
     setup once somebody has asked the welcome to stop. So on a phone the
     dialog's own small print, "The setup ring in the sidebar stays either
     way", was simply untrue, and the permanent-stop option it sits beside was
     a one-way door.

     Found by verify-mobile, which tried to reopen the setup at sixteen widths
     and got "it has no box" at the four narrowest.

     PINNED RIGHT, OUTSIDE THE SCROLLER. `margin-left: auto` where the column
     layout used `margin-top: auto`, and `flex-shrink: 0` so the links give up
     the width rather than these two. The links already scroll — that is what
     the block below is for — so they lose a little more run and nothing
     becomes unreachable, which is the trade the other way around. */
  .hb-rail-foot {
    flex-direction: row;
    margin-top: 0;
    margin-left: auto;
    flex-shrink: 0;
    gap: var(--space-1);
  }
  /* The icons scroll inside the bar: twelve 40px links is 502px in a 366px
     strip, and the last four are only reachable because this scrolls. */
  .hb-rail-links {
    flex-direction: row;
    overflow-x: auto;
    overflow-y: hidden;
    align-items: center;
    justify-content: flex-start;
    /* Without this the scroller claims its content width and pushes the foot
       off the end of the bar — the flex default is min-content, not zero. */
    min-width: 0;
  }
  .hb-rail a.hb-rl::after { display: none; }
}

/* ── The room the rail leaves ─────────────────────────────────────────────── */
.hb-room {
  position: relative;
  z-index: 1;
  margin-left: var(--hb-rail-w);
  min-height: 100vh;
  /* ROOM FOR "ASK HANK", which is `position: fixed` and so is placed against
     the viewport — the same reason the rail needs a margin on the left.

     THE RESERVE MOVED WITH THE CONTROL. It was 60px on the right alone, for a
     44px mark in the top-right corner; the control is a 136px pill in the
     BOTTOM right now, and 60px of right gutter does not clear it. The bottom
     reserve is what does: 44px of pill, its 16px offset, and a little air.

     The right gutter stays because the pill is still against that edge and a
     page whose content runs to the very margin would sit under its shoulder.
     verify-canvas measures the overlap on both sides — and measures the ink
     rather than the boxes, which is what makes the answer mean anything. */
  padding-right: 60px;
  padding-bottom: 68px;
}
@media (max-width: 768px) {
  .hb-room { padding-bottom: calc(var(--hb-rail-h) + 20px); padding-right: var(--space-3); }
}

/* ── The page's name ──────────────────────────────────────────────────────────
   ONE HEADING FOR EVERY PAGE ON THE CANVAS. Ten pages carried this block in
   their own <style>, character for character, because docs/canvas-conversion
   told them to; the three newest came from a different template and carried a
   smaller one — so the rent calculator opened with a 20px name and a 24px mark
   on the same rail as Marketing Agent's 36px and 44px, and the difference read
   as a decision. Scoped to .hb-room so it outranks a page's own `.page-h h1`
   (0,2,1 against 0,1,1): a page cannot drift by keeping a copy, and a new page
   gets the heading by writing `<div class="page-h"><h1 class="page-title">`
   with a `.lede` under it and nothing in its own <style>.

   modules.js fills the h1 with the module's mark sized INLINE for the 20px
   chip in a header bar; !important is what overrides an inline style, not
   specificity. The mark's own margin goes: the flex gap is the spacing.
   `color` and `font-weight` are restated rather than inherited because
   .header-module is --ink-2 and medium, and a page heading in that grey at
   36px reads as a caption for something above it. The lede steps to the
   section size under a display heading, at the measure that keeps two lines
   readable; below the tablet breakpoint the column is narrow enough on its
   own, and on a phone the pair steps down to page and title size — 36px over
   18px is a masthead, not a screen you work in. */
.hb-room .page-h h1 {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  font-size: var(--t-display);
  font-weight: var(--w-bold);
  color: var(--ink);
  letter-spacing: -0.02em;
  line-height: 1.15;
  margin-bottom: var(--space-2);
}
.hb-room .page-h h1 .hb-module-mark {
  width: 44px !important;
  height: 44px !important;
  border-radius: var(--radius-2);
  flex-shrink: 0;
  margin-right: 0;
}
.hb-room .page-h h1 .hb-module-mark svg { width: 22px; height: 22px; }
.hb-room .page-h .lede {
  font-size: var(--t-section);
  color: var(--ink-2);
  line-height: 1.5;
  max-width: 760px;
}
@media (max-width: 768px) {
  .hb-room .page-h .lede { max-width: none; }
}
@media (max-width: 560px) {
  .hb-room .page-h h1 { font-size: var(--t-page); }
  .hb-room .page-h .lede { font-size: var(--t-title); }
}

/* ── The other front door, and where it went ─────────────────────────────────
   Hank has two home pages — the module grid and the Good morning briefing —
   and each used to carry the OTHER page's icon as a 44px `.hb-door` at the
   right of its heading row. Both are gone: the left column carries the same
   two doors at its head, and two controls for one destination at opposite
   corners of one screen is a choice nobody needed to make. The rules are
   deleted rather than left to rot, because a class with no users is the same
   silent nothing as a `var(--line)` that does not resolve.
   Choosing a front door without walking through it lives on /profile. */
/* ── A surface ───────────────────────────────────────────────────────────────
   Opaque cards, a neutral hairline and restrained elevation keep working
   content distinct from the subtle background washes. All values come from
   the shared design tokens. Workspace layout lives in harris-workspace.css. */
.hb-surface {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-2);
  box-shadow: var(--shadow-sm);
}

/* One that responds to being pointed at, for a card that leads somewhere. A
   surface that lifts and does nothing is a promise the next click breaks. */
.hb-surface-live {
  transition:
    transform .14s cubic-bezier(.32, .72, 0, 1),
    box-shadow .14s ease,
    background .14s ease;
}
.hb-surface-live:hover {
  background: var(--surface);
  transform: translateY(-1px);
  box-shadow: var(--shadow-md);
}

/* Form and field surfaces retain their own class, with the same opaque
   treatment as the display surfaces. */
.hb-surface-solid {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-2);
  box-shadow: var(--shadow-sm);
}

/* Native modal supplies focus containment, background inertness and Escape.
   Its body scrolls independently; names always sit below the colored icons. */
.hb-apps-sheet {
  position: fixed; inset: auto 0 0; width: 100%; max-width: 768px;
  max-height: min(86dvh, calc(100dvh - env(safe-area-inset-top, 0px))); margin: 0 auto; padding: 0; overflow: hidden;
  border: 1px solid var(--border); border-bottom: 0; border-radius: 24px 24px 0 0;
  background: var(--surface); color: var(--ink);
  box-shadow: 0 -12px 48px rgba(21,23,26,.18);
}
.hb-apps-sheet[open] { display: flex; flex-direction: column; animation: hb-apps-enter .22s ease-out; }
.hb-apps-sheet::backdrop { background: rgba(21,23,26,.42); backdrop-filter: blur(3px); }
.hb-apps-head { display: flex; justify-content: space-between; align-items: center; gap: var(--space-4); padding: var(--space-5) var(--space-4) var(--space-4); border-bottom: 1px solid var(--border); flex: none; }
.hb-apps-head h2 { font-size: var(--t-page); font-weight: var(--w-bold); margin: 0 0 var(--space-1); }
.hb-apps-head p { font-size: var(--t-body); color: var(--ink-3); margin: 0; }
.hb-apps-close { display: grid; place-items: center; width: 44px; height: 44px; flex: none; border: 1px solid var(--border); border-radius: 50%; color: var(--ink-2); background: var(--surface-2); cursor: pointer; }
.hb-apps-close svg { width: 20px; height: 20px; }
.hb-apps-body { min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: var(--space-4); }
.hb-apps-grid { display: grid; grid-template-columns: repeat(3,minmax(0,1fr)); gap: var(--space-3) var(--space-2); }
.hb-apps-tile { min-width: 0; min-height: 98px; display: flex; flex-direction: column; align-items: center; gap: var(--space-2); padding: var(--space-2) 2px; border-radius: var(--radius-2); text-align: center; text-decoration: none; color: var(--ink-2); }
.hb-apps-tile:hover { background: var(--surface-2); }
.hb-apps-icon { display: grid; place-items: center; width: 48px; height: 48px; flex: none; border-radius: 14px; box-shadow: 0 2px 5px rgba(21,23,26,.06); }
.hb-apps-icon svg { width: 24px; height: 24px; }
.hb-apps-label { font-size: var(--t-small); font-weight: var(--w-medium); line-height: 1.4; overflow-wrap: anywhere; }
.hb-apps-foot { padding: var(--space-3) var(--space-4) max(var(--space-4),env(safe-area-inset-bottom)); border-top: 1px solid var(--border); flex: none; text-align: center; }
.hb-apps-foot a { display: block; padding: var(--space-2); min-height: 44px; color: var(--blue-ink); font-size: var(--t-body); font-weight: var(--w-medium); }
.hb-apps-status { color: var(--ink-3); font-size: var(--t-body); }
.hb-apps-account:not(:empty) { margin-top: var(--space-4); padding-top: var(--space-3); border-top: 1px solid var(--border); }
.hb-apps-retry { min-height: 44px; padding: var(--space-2) var(--space-4); border: 1px solid var(--border); border-radius: var(--radius-3); background: var(--surface); color: var(--ink); font: inherit; cursor: pointer; }
.hb-apps-sheet :is(a,button):focus-visible { outline: 2px solid var(--blue-ink); outline-offset: 2px; }
@media (min-width: 560px) { .hb-apps-grid { grid-template-columns: repeat(4,minmax(0,1fr)); } }
@keyframes hb-apps-enter { from { transform: translateY(100%); } to { transform: translateY(0); } }
@media (prefers-reduced-motion: reduce) { .hb-apps-sheet[open] { animation: none; } }

/* ── The account menu ────────────────────────────────────────────────────────
   The avatar at the foot of the rail opens this rather than signing out on the
   spot. Signing out is now one item among several, which is also safer: a
   single click that ends the session, sitting in the corner of every page, is
   a click somebody makes by accident.

   WHAT IS IN IT IS WHAT HANK ACTUALLY HAS. Kinso's menu offers a dark-mode
   switch and a profile; this app removed dark mode deliberately and has no
   per-user profile page, so neither is here. A menu item that opens nothing is
   the same broken promise as a rail link that answers 403. */
.hb-me-menu {
  position: fixed;
  left: calc(var(--hb-rail-w) - 8px);
  bottom: var(--space-4);
  z-index: 40;
  min-width: 232px;
  padding: var(--space-2);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 14px;
  box-shadow:
    0 1px 2px rgba(21, 23, 26, .05),
    0 18px 40px -12px rgba(21, 23, 26, .30);
  visibility: hidden;
  opacity: 0;
  transform: translateY(4px);
  transition: opacity .12s ease, transform .12s ease, visibility 0s .12s;
}
.hb-me-menu.open {
  visibility: visible;
  opacity: 1;
  transform: translateY(0);
  transition: opacity .12s ease, transform .12s ease;
}
/* Who you are, first — the whole reason to open it is to check. */
.hb-me-who {
  padding: var(--space-2) var(--space-3) var(--space-3);
  border-bottom: 1px solid var(--border);
  margin-bottom: var(--space-2);
}
.hb-me-name { font-weight: var(--w-medium); color: var(--ink); }
.hb-me-mail {
  font-size: var(--t-small);
  color: var(--ink-3);
  margin-top: 2px;
  /* An address longer than the menu wraps rather than being cut: half an
     email address is not an email address. */
  overflow-wrap: anywhere;
}
.hb-me-menu a,
.hb-me-menu button,
.hb-apps-account a,
.hb-apps-account button {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  padding: 9px var(--space-3);
  border: 0;
  border-radius: 9px;
  background: none;
  font: inherit;
  font-size: var(--t-body);
  color: var(--ink-2);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}
.hb-me-menu a:hover,
.hb-me-menu button:hover,
.hb-apps-account a:hover,
.hb-apps-account button:hover { background: var(--surface-2); color: var(--ink); }
.hb-me-menu a:focus-visible,
.hb-me-menu button:focus-visible { outline: 2px solid var(--blue); outline-offset: -2px; }
.hb-me-menu svg,
.hb-apps-account svg { width: 16px; height: 16px; flex: none; color: var(--ink-3); }
.hb-apps-account :is(a,button) { min-height: 44px; }
.hb-me-sep { height: 1px; background: var(--border); margin: var(--space-2) 0; }
/* Ending the session is the one item that cannot be undone by clicking again. */
.hb-me-menu button.hb-me-out,
.hb-apps-account button.hb-me-out { color: var(--red-ink, var(--red)); }
.hb-me-menu button.hb-me-out svg { color: currentColor; }

@media (max-width: 768px) {
  /* The rail is a bottom bar there, so the menu comes up from the bar rather
     than out of a column that no longer exists. */
  .hb-me-menu { left: var(--space-3); right: var(--space-3); bottom: 72px; min-width: 0; }
}

/* ── Ask Hank, top right ─────────────────────────────────────────────────────
   The assistant's own control, in the same place on every page that carries
   the canvas — which is the point of it being here rather than on a page: an
   assistant you have to go and find is one nobody asks.

   IT SAYS WHAT IT IS. Asked for, with a picture: "the purple ask hank icon
   needs to be the attached — instead of Voice Chat — needs to be 'Ask Hank'.
   The Hank needs to be the Hank logo so it's consistent." A mark alone cannot
   say what pressing it does; the pill can, in two words, and the second of
   them is the real wordmark rather than the word set in the UI face.

   THE PILL IS LIGHT AND THE PURPLE IS THE ORB. The wordmark is black artwork
   on transparent, so it has to sit on a light ground to read at all — and the
   colour was never in the fill anyway. It is in the nebula, the hairline and
   the shadow, which are the app's existing --ai pair and introduce nothing
   new. The motion is a slow orbit and a slower breath: enough to read as alive
   at the edge of vision, not enough to pull the eye off the work. */
.hb-ai {
  position: fixed;
  /* ── BOTTOM RIGHT, AND IT HAD TO MOVE ────────────────────────────────────
     The mark was top right, where a 44px circle fitted in the gutter beside a
     page title. A pill is 136px and it does not: the canvas sweep found it
     drawn over the H1 on twenty of the twenty-seven pages on the canvas, which
     is the whole point of that check existing.

     Down here nothing is in its way on any of them — the rail is on the left,
     the account menu comes up from the bottom of the rail, and the room's own
     content stops well short. It is also where the picture this was taken from
     puts it, and where every assistant a reader has met before sits. */
  bottom: var(--space-4);
  right: var(--space-4);
  z-index: 31;
  height: 44px;
  /* Tight to the orb on the left, open on the right, so the round end and the
     text end are optically the same distance from the edge. */
  padding: 0 15px 0 6px;
  border-radius: 999px;
  /* ── FILLED, SINCE OCT 2026 ──────────────────────────────────────────────
     Asked for against a picture of a gradient pill: "Can you update our Ask
     Hank AI button to the attached."

     IN HANK'S OWN RAMP, NOT THE PICTURE'S. The reference runs warm red into
     violet; this is the violet-to-green the whole application's AI surfaces
     were just set in, and which the same person signed off. A control that
     is the front door to Hank in a colourway nothing else in Hank wears
     would make the gradient mean "this button" instead of "this is AI".
     Swapping --ai-grad here for the warmer ramp is a one-line change if that
     trade is wanted the other way round.

     `background-color` under it, because a gradient is an image: a browser
     that drops the image still gets a filled pill rather than white text on
     white. */
  border: 1px solid transparent;
  background-color: var(--ai-strong);
  background-image: var(--ai-grad);
  box-shadow:
    0 1px 2px rgba(21, 23, 26, .10),
    0 10px 26px -14px rgba(124, 58, 237, .70);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 9px;
  transition: transform .14s cubic-bezier(.32, .72, 0, 1), box-shadow .14s ease;
}
.hb-ai:hover {
  transform: translateY(-1px);
  /* Brightness rather than a second gradient — the same recipe .btn-hank
     uses, so "the same thing, lit" and not a fourth hue. */
  filter: brightness(1.08) saturate(1.05);
  box-shadow:
    0 1px 2px rgba(21, 23, 26, .10),
    0 16px 34px -14px rgba(124, 58, 237, .85);
}
.hb-ai:focus-visible { outline: 2px solid var(--ai-strong); outline-offset: 3px; }

/* ── THE STARS, AS THE BUTTON'S ICON ──────────────────────────────────────
   "The AI stars need to be the icon before the Ask Hank."

   So the launcher carries the sparkle every other AI surface in Hank wears,
   taken from HarrisBrand rather than drawn again here. The nebula stays in
   the popup's head, where it is still the assistant's face and still the
   only moving thing in that panel; what changed is the control, which now
   says "AI" with the same mark as the pills and the signatures.

   IT STILL MOVES, AND THAT IS NOT DECORATION. The orb's animation was the
   brief when this was a nebula — verify-canvas holds it with a comment
   saying so — and swapping the artwork is no reason to quietly drop it. A
   slow twinkle rather than a rotation: a four-pointed star that spins reads
   as a loading spinner, which is a promise this button is not making.

   AND IT IS `.hb-ai-ico`, A NAME NEITHER NEIGHBOUR USES. `.hb-ai-spark` was
   the obvious one and it was already taken — by the bright dot riding the
   nebula's near ring, which has a nine-second spin of its own. The span
   inherited it, so the check that this mark animates went on passing with
   the rule below deleted, because something else was moving it. Found by
   mutation, not by reading. `.hb-ai-stars` was the second choice and
   `.hb-ai-starters` is the chip row under the panel's greeting, which a grep
   for the shorter name matches.

   WHITE, because it sits on the fill. */
.hb-ai .hb-ai-ico {
  flex: none;
  width: 22px;
  height: 22px;
  color: #fff;
  display: grid;
  place-items: center;
  /* ON THE RULE, TURNED OFF BY THE MEDIA QUERY — not the other way round.
     Declared only inside a no-preference query it behaves identically in a
     browser and is invisible to anything reading this stylesheet for the
     selector, which is how ask-hank-launcher.test.js reads it. The orb's own
     spark two hundred lines below does it this way round, and so does
     .ai-working in harris.css.

     AND NO BRACES IN THIS PARAGRAPH, which is not a style note. That test's
     helper takes a selector and everything up to the next closing brace, so
     a comment that contains one truncates the block it is explaining — which
     is what the first draft of this very comment did. */
  animation: hb-ai-twinkle 2.6s ease-in-out infinite;
}
.hb-ai .hb-ai-ico svg { width: 100%; height: 100%; display: block; }
@keyframes hb-ai-twinkle {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: .72; transform: scale(.88); }
}
@media (prefers-reduced-motion: reduce) {
  .hb-ai .hb-ai-ico { animation: none; }
}

/* The orb is its own round box now rather than the button's whole face, so the
   same markup is 30px on the pill and 34px in the popup's head. It clips the
   nebula, which is what `overflow: hidden` on the button used to do. */
.hb-ai-orb {
  position: relative;
  flex: none;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  overflow: hidden;
  display: grid;
  place-items: center;
}

/* "Ask" in the UI face, "Hank" as the mark. The gap is a little tight because
   the wordmark carries its own side bearing. */
.hb-ai-say {
  display: inline-flex;
  align-items: baseline;
  gap: 5px;
  font-size: var(--t-body);
  font-weight: var(--w-medium);
  /* WHITE, ON THE FILL. Measured in __tests__/ai-colourway.test.js against
     every point of the ramp, which is why the ramp was darkened when the
     colourway landed: the reference's mint put white text on 2.4:1. */
  color: #fff;
  letter-spacing: .01em;
  white-space: nowrap;
}
/* The name, however it resolves. As live text it is simply the next word; as
   the uploaded mark it is an image the box is cropped around — the crop rules
   live in harris.css because the wait overlay wants them too. The height is
   generous against the 13px beside it for the same reason --hb-logo-h is: the
   lockup's artwork fills about two thirds of its own box, so 20px of image
   draws about 13px of glyph. */
.hb-ai-name {
  display: inline-flex;
  align-items: center;
  align-self: center;
}
.hb-ai-name img {
  height: 20px;
  width: auto;
  display: block;
  object-fit: contain;
}
/* ── THE WORDMARK, KNOCKED OUT ────────────────────────────────────────────
   "Make sure the Hank part is our Hank logo 'Ask Hank'." It already was —
   drawn through HarrisBrand.name from the uploaded asset — but it was drawn
   on white. On a violet fill an uploaded mark in Hank's purple is a word you
   cannot read, and the asset is somebody's file rather than something this
   stylesheet may recolour.

   `brightness(0)` takes it to black whatever it was, `invert(1)` takes it to
   white: any artwork, any hue, one legible result, and the transparency
   around the glyphs is untouched. The live-text fallback needs none of this
   — it inherits the white above — so this is scoped to the image. */
.hb-ai .hb-ai-name img { filter: brightness(0) invert(1); }

/* The nebula: two offset radial washes that rotate against each other, so the
   colour drifts rather than pulsing on a single axis. */
.hb-ai-core {
  position: absolute;
  inset: 2px;
  border-radius: 50%;
  background:
    radial-gradient(60% 60% at 32% 30%, rgba(167, 139, 250, .95), transparent 70%),
    radial-gradient(65% 65% at 72% 68%, rgba(124, 58, 237, .95), transparent 72%),
    radial-gradient(circle at 50% 50%, #4C1D95, #2E1065);
  animation: hb-ai-drift 18s linear infinite;
}
/* A breath on the whole mark, on a different period from the drift so the two
   never land together and the loop does not read as a loop. */
.hb-ai-glow {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: radial-gradient(circle at 50% 50%, rgba(196, 181, 253, .55), transparent 62%);
  animation: hb-ai-breathe 5.5s ease-in-out infinite;
}
/* 100% of whatever the orb is, so the viewBox does the scaling and there is
   one copy of the drawing at two sizes. */
.hb-ai-orbits { position: relative; width: 100%; height: 100%; overflow: visible; }
.hb-ai-orbits ellipse {
  fill: none;
  stroke: rgba(233, 213, 255, .85);
  stroke-width: 1;
  transform-origin: 20px 20px;
}
.hb-ai-o1 { animation: hb-ai-spin 9s linear infinite; }
/* Counter-rotating, and slower: two rings turning the same way read as one
   thick ring. */
.hb-ai-o2 { animation: hb-ai-spin-back 15s linear infinite; opacity: .6; }
/* One spark riding the near ring, which is what makes the rotation legible —
   a plain ellipse spinning about its own centre looks still. */
.hb-ai-spark {
  fill: #F5F3FF;
  transform-origin: 20px 20px;
  animation: hb-ai-spin 9s linear infinite;
}

@keyframes hb-ai-drift { to { transform: rotate(360deg); } }
@keyframes hb-ai-spin { to { transform: rotate(360deg); } }
@keyframes hb-ai-spin-back { to { transform: rotate(-360deg); } }
@keyframes hb-ai-breathe {
  0%, 100% { opacity: .35; transform: scale(1); }
  50%      { opacity: .75; transform: scale(1.06); }
}

/* MOTION THAT NEVER STOPS IS EXACTLY WHAT THIS SETTING IS FOR. The mark keeps
   its colour and its rings — it is still the same object — and simply holds
   still. Removing it entirely would take the affordance with the animation. */
@media (prefers-reduced-motion: reduce) {
  .hb-ai-core,
  .hb-ai-glow,
  .hb-ai-o1,
  .hb-ai-o2,
  .hb-ai-spark { animation: none; }
  .hb-ai-glow { opacity: .55; }
  .hb-ai:hover { transform: none; }
}

@media (max-width: 768px) {
  /* Clear of the bottom bar the rail becomes — --hb-rail-h exists for exactly
     this, and is what .hb-railed already reserves as padding. */
  .hb-ai { bottom: calc(var(--hb-rail-h, 64px) + var(--space-3)); right: var(--space-3); }
}

/* ── The assistant's popup ───────────────────────────────────────────────────
   Asked for: "when you click it, i dont want a slide out drawer, i want the
   attached pop up window."

   IT WAS A RIGHT-HAND SHEET, on the argument that the page it is about stays
   on screen behind it. That argument is weaker than it looked: a conversation
   is the subject while it is open, and the app already has a window for one —
   voice mode's sheet, which is a centred card on a desktop and the whole
   screen on a phone. Two windows for a conversation with Hank, in two
   different shapes, is the inconsistency; this is the same shape.

   THE CARD IS INSIDE THE SCRIM RATHER THAN BESIDE IT, which is how every other
   modal in the app is built (.modal-backdrop > .modal) and what makes centring
   a flex property rather than a transform to undo. shared/modal.js finds the
   panel either way — it looks inside the overlay first and only then at the
   next sibling — so the focus trap, Escape, the click-outside close and the
   scroll lock all still come from there. */
.hb-ai-back {
  position: fixed;
  inset: 0;
  z-index: 1200;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-5);
  background: rgba(26, 21, 17, .40);
  -webkit-backdrop-filter: blur(6px) saturate(105%);
  backdrop-filter: blur(6px) saturate(105%);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity .18s ease, visibility 0s .18s;
}
.hb-ai-back.open {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transition: opacity .18s ease;
}
.hb-ai-panel {
  width: min(460px, 100%);
  /* SIZED BY WHAT IS IN IT. On a page that has given Hank nothing to answer
     with the popup is three links, and a fixed 620px window around them is
     mostly empty card. A conversation is the other way round — it has to be
     tall enough to hold one before there is anything in it, or the window
     grows under the reader as they talk — so that state takes the height and
     this one takes its content. */
  max-height: min(620px, 100%);
  background: var(--surface);
  border: 1px solid rgba(255, 255, 255, .9);
  border-radius: var(--radius-lg);
  box-shadow:
    0 0 0 1px rgba(21, 23, 26, .04),
    0 1px 2px rgba(21, 23, 26, .06),
    0 28px 64px -28px rgba(21, 23, 26, .45);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  /* It comes up out of the control rather than fading in on the spot. */
  transform: translateY(8px) scale(.98);
  opacity: 0;
  transition: transform .2s cubic-bezier(.32, .72, 0, 1), opacity .16s ease;
}
.hb-ai-panel:has(.hb-ai-chat) { height: min(620px, 100%); }
.hb-ai-back.open .hb-ai-panel {
  transform: none;
  opacity: 1;
}
.hb-ai-head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-4) var(--space-4) var(--space-5);
  border-bottom: 1px solid var(--border);
}
/* The head's own orb, a size up: it is the window's subject rather than a
   control's ornament. */
.hb-ai-head .hb-ai-orb { width: 34px; height: 34px; }
/* Takes the room, so the close button is pushed to the far edge. */
.hb-ai-head > div { flex: 1; min-width: 0; }
.hb-ai-head h2 { font-size: var(--t-section); font-weight: var(--w-bold); line-height: 1.2; }
.hb-ai-head h2 .hb-ai-name img { height: 26px; }
.hb-ai-head .sub { font-size: var(--t-small); color: var(--ink-3); margin-top: 2px; }
/* An X rather than the word, because the window now has a head that is a
   lockup and a labelled button in it reads as a second action. Named for a
   screen reader; 44px under a finger, from the touch block in harris.css. */
.hb-ai-x {
  flex: none;
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--ink-2);
  cursor: pointer;
}
.hb-ai-x:hover { background: var(--border); color: var(--ink); }
.hb-ai-x:focus-visible { outline: 2px solid var(--ai-strong); outline-offset: 2px; }
.hb-ai-x svg { width: 15px; height: 15px; }
.hb-ai-body { padding: var(--space-5); overflow-y: auto; flex: 1; line-height: 1.6; }
.hb-ai-body p { color: var(--ink-2); font-size: var(--t-body); margin-bottom: var(--space-4); }
.hb-ai-where { display: flex; flex-direction: column; gap: var(--space-2); }
.hb-ai-where a {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--border);
  border-radius: 12px;
  text-decoration: none;
  color: var(--ink);
  font-size: var(--t-body);
  transition: background .12s ease, border-color .12s ease;
}
.hb-ai-where a:hover { background: var(--ai-soft); border-color: var(--ai-strong); }
.hb-ai-where .k { font-weight: var(--w-medium); }
.hb-ai-where .d { color: var(--ink-3); font-size: var(--t-small); }

/* ── When a page gives Hank something to answer with ────────────────────────
   shared/harris-ai.js draws a conversation instead of the shell when the page
   has configured an `ask` handler: what has been said, a row of starter
   questions, and the box to type in pinned at the foot. Bubbles rather than a
   transcript, because a question and its answer are two voices. */
.hb-ai-chat { display: flex; flex-direction: column; gap: var(--space-3); }
.hb-ai-scope { font-size: var(--t-small); color: var(--ink-3); line-height: 1.5; }
.hb-ai-starters { display: flex; flex-wrap: wrap; gap: 6px; }
.hb-ai-starters button {
  font: inherit; font-size: var(--t-small); color: var(--ink-2);
  background: var(--surface-2); border: 1px solid var(--border); border-radius: 999px;
  padding: 5px 11px; cursor: pointer; text-align: left;
}
.hb-ai-starters button:hover { background: var(--ai-soft); border-color: var(--ai-strong); color: var(--ink); }
.hb-ai-msg {
  max-width: 92%; padding: 9px 12px; border-radius: 12px;
  font-size: var(--t-body); line-height: 1.55; white-space: pre-wrap; overflow-wrap: anywhere;
}
.hb-ai-msg.me { align-self: flex-end; background: var(--ink); color: #fff; border-bottom-right-radius: 4px; }
.hb-ai-msg.hank { align-self: flex-start; background: var(--surface-2); color: var(--ink); border-bottom-left-radius: 4px; }
/* ── HANK THINKING, IN HANK'S COLOURS ────────────────────────────────────
   This was grey italic, which is how a disabled control looks. It is the one
   moment in a conversation when something IS happening, and the reference the
   colourway came from is precisely this state — a sparkle and a word running
   violet to blue to green, moving.

   THE COLOURS ARE SHARED AND THE DECLARATIONS ARE NOT, and the difference is
   specificity rather than carelessness. Every value here is an --ai token, so
   there is still one recipe for Hank's purple. But the bubble beside this one
   is `.hb-ai-msg.hank`, which sets a `color` with exactly two classes — so
   adding `.ai-gradient` (one class) to the element would have lost to it and
   drawn flat ink, and `.ai-working` with it. Two classes is what it takes to
   win here, which means the properties live on this selector. The keyframes
   are the shared ones.

   THE BUBBLE GOES WHILE IT THINKS, which is a consequence and not an
   oversight: `background-clip: text` clips the background COLOUR as well as
   the image, so a tinted bubble with gradient letters is not a thing that can
   exist. A status line with no ground is also what the reference shows. */
.hb-ai-msg.thinking {
  font-style: normal;
  font-weight: var(--w-medium);
  /* The star sits beside the words rather than above them. */
  display: inline-flex;
  align-items: center;
  gap: 7px;
  align-self: flex-start;
  background-color: transparent;
  background-image: var(--ai-grad-run);
  background-size: 200% 100%;
  background-origin: content-box;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: hb-ai-sweep 3s linear infinite;
}

/* The sparkle is drawn in currentColor, which is now transparent; it takes
   the ramp's first colour, so the star is the violet end of it. */
.hb-ai-msg.thinking .ai-mark { color: var(--ai-strong); flex-shrink: 0; }

/* The fallback, first in the cascade's eyes because the clip above needs
   support to mean anything: no gradient, no transparent text. */
@supports not ((-webkit-background-clip: text) or (background-clip: text)) {
  .hb-ai-msg.thinking { color: var(--ai-strong); background-image: none; }
}

@media (prefers-reduced-motion: reduce) {
  /* The colour stays and only the drift stops. At 200% the still frame is
     already a whole violet-to-green pass across the words, so somebody who
     has asked for less movement sees the same ramp as everybody else rather
     than a frozen slice of it. */
  .hb-ai-msg.thinking { animation: none; }
}
.hb-ai-msg.bad { background: var(--red-soft); color: var(--red-ink); }
.hb-ai-compose {
  border-top: 1px solid var(--border); padding: var(--space-3) var(--space-4) var(--space-4);
  display: flex; gap: 8px; align-items: flex-end; flex-shrink: 0;
}
.hb-ai-compose textarea {
  flex: 1; min-width: 0; min-height: 44px; resize: vertical; font: inherit; font-size: var(--t-body);
  padding: 9px 11px; border: 1px solid var(--border); border-radius: var(--radius-3);
  background: var(--surface); color: var(--ink); line-height: 1.45;
}
.hb-ai-compose textarea:focus { outline: none; border-color: var(--ai); box-shadow: 0 0 0 3px var(--ai-soft); }
@media (prefers-reduced-motion: reduce) {
  .hb-ai-panel { transform: none; transition: none; }
}

/* ── The whole screen on a phone ─────────────────────────────────────────────
   The same answer voice mode's sheet gives, at the same canonical width: a
   conversation on a 390px screen competing with the page behind it is neither.
   `dvh` rather than `vh` because the address bar moves and `vh` does not, and
   the safe-area inset keeps the compose row off the home indicator — where the
   press that reaches it is the one that leaves the app. */
@media (max-width: 560px) {
  .hb-ai-back { padding: 0; }
  .hb-ai-panel {
    width: 100%;
    height: 100dvh;
    max-height: 100dvh;
    border: 0;
    border-radius: 0;
  }
  .hb-ai-head { padding-top: max(var(--space-4), env(safe-area-inset-top)); }
  .hb-ai-compose { padding-bottom: max(var(--space-4), env(safe-area-inset-bottom)); }
}

/* ── Shared components, on the canvas ────────────────────────────────────────
   THE METRIC CARD. harris.css gives .hm-card an opaque surface and a grey
   border, which is right on a grey page and reads as a box sitting ON the warm
   ground rather than belonging to it. This is the same translucent treatment
   .hb-surface applies, expressed on the shared class so a page does not have
   to add one by hand to markup that harris-metrics.js generates. Scoped to
   .hb-canvas, so the pages still on grey keep the card they were designed
   with. */
.hb-canvas .hm-card {
  background: var(--surface);
  border-color: var(--border);
  border-radius: var(--radius-2);
  box-shadow: var(--shadow-sm);
}
.hb-canvas a.hm-card:hover,
.hb-canvas button.hm-card:hover { background: var(--surface); }

/* ── A module's own views, on the canvas ─────────────────────────────────────
   harris.css styles `.viewbar` as a full-bleed white strip with a bottom
   border and an underlined current tab, which is right under a topbar and
   wrong without one: on the warm ground it reads as a leftover piece of window
   chrome. Here it becomes a segmented row that sits inside the content column,
   under the page's heading.

   SCOPED TO .hb-canvas, so the EOM views that have not been restyled yet keep
   the bar they were designed with. One module, two treatments, for exactly as
   long as it takes to convert the other two pages. */
.hb-canvas .viewbar {
  background: none;
  border-bottom: 0;
  margin-bottom: var(--space-6);
}
.hb-canvas .viewbar-inner {
  max-width: none;
  margin: 0;
  padding: 4px;
  gap: 2px;
  display: inline-flex;
  align-items: center;
  background: rgba(255, 255, 255, .6);
  border: 1px solid rgba(255, 255, 255, .9);
  border-radius: 12px;
  box-shadow: 0 1px 2px rgba(21, 23, 26, .04);
  max-width: 100%;
}
.hb-canvas .viewbar a {
  padding: 7px var(--space-3);
  border-bottom: 0;
  margin-bottom: 0;
  border-radius: 9px;
  transition: background .12s ease, color .12s ease, box-shadow .12s ease;
}
.hb-canvas .viewbar a:hover { background: rgba(255, 255, 255, .7); }
/* A THUMB'S TARGET. harris.css's coarse-pointer floor names buttons, selects
   and inputs, and these are links — measured at 390 with a coarse pointer,
   7px of padding round 13px text is a 31px row, beside buttons at 44. Said
   once here, for every module's row of views, rather than in each module's
   own sheet. */
@media (pointer: coarse) {
  .hb-canvas .viewbar a { min-height: 44px; display: inline-flex; align-items: center; }
}
/* THE CURRENT ONE IS A RAISED TILE, not an underline. An underline needs a
   baseline to sit on, and the strip that provided one is gone. */
.hb-canvas .viewbar a[aria-current="page"] {
  background: var(--surface);
  box-shadow: 0 1px 3px rgba(21, 23, 26, .10);
}
/* `margin-left: auto` pushed the personal view to the far end of a full-width
   bar. In a segmented control that is 1200px of empty pill, so the separation
   becomes a gap and a hairline instead. */
.hb-canvas .viewbar a.viewbar-own {
  margin-left: var(--space-2);
  padding-left: calc(var(--space-3) + var(--space-2));
  border-left: 1px solid var(--border);
  border-radius: 9px;
}

/* ── A section label ─────────────────────────────────────────────────────────
   Small, grey, and the thing that lets a column of cards be read as groups
   without drawing a single line. */
.hb-group {
  font-size: var(--t-small);
  color: var(--ink-3);
  font-weight: var(--w-medium);
  margin: var(--space-6) 0 var(--space-3);
}
.hb-group:first-child { margin-top: 0; }

/* ── Reduced motion ──────────────────────────────────────────────────────────
   The lift is the only motion this layer adds, and it is the only one worth
   removing: a surface that jumps under the pointer is exactly what somebody
   with vestibular sensitivity turned the setting on for. The colour change
   stays, because it is what says "this is under your pointer". */
@media (prefers-reduced-motion: reduce) {
  .hb-surface-live { transition: background .12s ease; }
  .hb-surface-live:hover { transform: none; }
}

/* ── The data grid ───────────────────────────────────────────────────────────
   Airtable's structure, drawn in Hank's materials. The reference is functional:
   what the toolbar states, how a group header reads, where the pills go. None
   of the surface is borrowed — every colour here is a token this stylesheet
   already defines, so the grid sits on the canvas rather than on top of it.

   THE GRID IS THE ONE PLACE HAIRLINES EARN THEIR KEEP. Everywhere else on the
   canvas a border is a last resort — surfaces are separated by tint and light.
   A grid is different: a cell boundary is information, because reading across a
   row of twelve fields depends on knowing where one value stops. So the rules
   are drawn, and drawn as quietly as they can be while still doing that job. */
/* ── The view bar ────────────────────────────────────────────────────────────
   ONE BAR, WORN BY EVERY VIEW. The grid grew this first — a name, a count, the
   controls that change what you are looking at — and for a while it was the only
   view that had it: the inbox had a heading and a sentence, the calendar had a
   title and three buttons, the board had a line of hint text. Four views on one
   page with four different chromes reads as four features bolted together
   rather than one thing you are looking at four ways, which is the single
   biggest gap between this page and the reference.

   So the treatment is `.hb-bar` and the grid uses it like everything else. The
   controls differ per view; the bar does not. */
.hb-bar, .hg-bar {
  display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap;
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--border);
}
.hb-bar-name, .hg-name { font-size: var(--t-title); font-weight: var(--w-bold); }
.hb-bar-count, .hg-count { font-size: var(--t-small); color: var(--ink-3); }
.hb-bar-tools, .hg-chips { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.hb-bar-right, .hg-search { margin-left: auto; }
.hg-search .inp { width: 190px; height: 30px; padding: 0 var(--space-3); font-size: var(--t-small); }
/* A note under the bar rather than in it: the reason a view is showing fewer
   rows than the board has, said where it will be read and not competing with
   the controls for the bar's one line. */
.hb-bar-note {
  font-size: var(--t-small); color: var(--ink-3);
  padding: var(--space-3) var(--space-4) 0;
}

/* A CHIP SAYS WHAT IT IS DOING, so it has to be wide enough to say it. Airtable
   reads "Filtered by Listing Status, Listed Date" rather than showing a funnel
   with a dot on it — a state you have to hover to discover is a state people
   forget is on, and a filtered view somebody believes is the whole list is how
   work goes missing. */
.hb-chip, .hg-chip {
  font: inherit; font-size: var(--t-small); font-weight: var(--w-medium);
  color: var(--ink-2); background: none; border: 1px solid transparent;
  border-radius: var(--radius-3); padding: 5px var(--space-3);
  cursor: pointer; white-space: nowrap; min-height: 30px;
}
.hb-chip:hover, .hg-chip:hover { background: var(--surface-2); }
/* ON IS A FILL, NOT A DOT. The difference between "you can filter" and "this is
   filtered" has to survive being glanced at. */
.hb-chip.on, .hg-chip.on {
  background: var(--ai-soft); color: var(--ai-strong);
  border-color: color-mix(in srgb, var(--ai-strong) 22%, transparent);
}

.hg-scroll { overflow-x: auto; }
.hg-scroll:focus-visible { outline: 2px solid var(--ai-strong); outline-offset: -2px; }
table.hg {
  border-collapse: separate; border-spacing: 0;
  width: 100%; font-size: var(--t-small);
  /* FIXED, OR THE COLUMN WIDTHS DO NOTHING. Under the default `auto` layout the
     browser sizes every column by its content and silently ignores the widths
     the page set — which is how one long address ends up three times the width
     of the field beside it, and why the first draft of this grid looked nothing
     like the reference. */
  table-layout: fixed;
}
table.hg th, table.hg td {
  border-bottom: 1px solid var(--border);
  border-right: 1px solid var(--border);
  padding: 0; text-align: left; vertical-align: top;
  background: var(--surface);
}
table.hg th:last-child, table.hg td:last-child { border-right: none; }

table.hg thead th { background: var(--surface-2); position: sticky; top: 0; z-index: 3; }
.hg-h {
  display: flex; align-items: center; gap: 6px; width: 100%;
  font: inherit; font-size: var(--t-micro); font-weight: var(--w-bold);
  letter-spacing: 0.04em; text-transform: uppercase; color: var(--ink-3);
  background: none; border: none; cursor: pointer; text-align: left;
  padding: 6px var(--space-3); min-height: 30px;
}
.hg-h:hover { color: var(--ink); }
/* The type glyph. Airtable's own headers carry one, and it is the thing that
   tells you a column holds a fixed set of values before you have read a row. */
.hg-type {
  flex-shrink: 0; width: 14px; text-align: center; color: var(--ink-3);
  font-weight: var(--w-regular);
}
.hg-hl { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ── The frozen first columns ────────────────────────────────────────────────
   The row number and the first field stay put while the rest scrolls. THEY NEED
   AN OPAQUE BACKGROUND: a sticky cell with a transparent one lets the columns
   underneath show straight through it, which is the classic way this is got
   wrong and looks like a rendering fault rather than a missing declaration.
   `var(--surface)` on every cell above already covers it; the tinted rows set
   their own inline, which is why the tint is applied to these cells too. */
table.hg .hg-frozen { position: sticky; z-index: 2; }
table.hg th.hg-frozen { z-index: 4; }
table.hg .hg-num { left: 0; width: 54px; min-width: 54px; }
table.hg th:nth-child(2), table.hg td:nth-child(2) { left: 54px; }
/* The shadow that says the frozen edge is an edge, not a border like the rest. */
table.hg .hg-num + * { box-shadow: none; }
table.hg td:nth-child(2), table.hg th:nth-child(2) {
  box-shadow: 1px 0 0 var(--border);
}

.hg-num { padding: 0 !important; }
.hg-num { position: sticky; }
.hg-num .hg-n {
  display: inline-block; padding: 7px 6px 7px var(--space-3);
  font-size: var(--t-micro); color: var(--ink-3); font-variant-numeric: tabular-nums;
}
/* THE WAY IN, AND IT IS A REAL BUTTON. Opening on any click would make selecting
   text in a cell impossible; a button is also reachable from the keyboard
   without giving the row a tabindex and a keydown handler of its own. Shown on
   hover and whenever it has focus — a control that only appears under a pointer
   is a control a keyboard never finds. */
/* ABSOLUTE, SO AN INVISIBLE BUTTON DOES NOT MAKE THE ROW TALLER. At
   `opacity: 0` it still occupied its own line, which added 26px to every row in
   the grid — the single biggest reason the first draft was twice the height of
   the reference. Taken out of flow it costs nothing until it is wanted. */
.hg-open {
  position: absolute; right: 4px; top: 50%; transform: translateY(-50%);
  font: inherit; font-size: var(--t-micro); line-height: 1;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-3); color: var(--ink-2);
  padding: 3px 5px; cursor: pointer; opacity: 0;
}
.hg-row:hover .hg-open, .hg-open:focus-visible { opacity: 1; }
.hg-open:hover { background: var(--surface-2); color: var(--ink); }

/* THE ROW IS THE DENSITY. Airtable's short row is one line at about 32px, and
   a grid whose rows are twice that shows half as much — which is the whole
   reason somebody opens a grid rather than a board. 7px and a 12px line-height
   lands at 32; the taller settings buy their height back in line clamp. */
table.hg tbody td { padding: 7px var(--space-3); color: var(--ink); line-height: 1.45; }
table.hg tbody tr { position: relative; }
/* ROW HEIGHT IS A LINE CLAMP, not a fixed height. A row is as tall as the text
   in it; clamping the lines is what actually changes the density, and a fixed
   height would cut a wrapped address in half. */
table.hg[data-height="short"] tbody td {
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* The taller settings clamp lines rather than fixing a height, so a row is as
   tall as the wrapped text actually in it and a two-line address is never cut
   through the middle of its second line. */
table.hg[data-height="medium"] tbody td > *,
table.hg[data-height="tall"] tbody td > *,
table.hg[data-height="extra"] tbody td > * {
  display: -webkit-box; -webkit-box-orient: vertical; overflow: hidden;
}
table.hg[data-height="medium"] tbody td > * { -webkit-line-clamp: 2; line-clamp: 2; }
table.hg[data-height="tall"] tbody td > * { -webkit-line-clamp: 4; line-clamp: 4; }
table.hg[data-height="extra"] tbody td > * { -webkit-line-clamp: 8; line-clamp: 8; }
.hg-row:hover td { background: var(--surface-2); }
.hg-blank::after { content: "—"; color: var(--ink-3); }

/* ── THE VACANT LINE AT THE BOTTOM ──────────────────────────────────────────
   "adding/editing a line is simply just click on the line to edit or click
   next free vacant line - not a 'add line' button". So there is no button,
   and this line is the whole affordance — which means it has to read as an
   invitation rather than as a row. It is tinted a shade back from the rows
   above it, its cells are empty rather than dashed, and the gutter carries a
   `+` instead of a number. On hover it comes forward to the ordinary row
   tint, which is the moment it stops looking like a margin and starts
   looking like somewhere to type. */
table.hg tbody tr.hg-new td { background: var(--surface-2); }
table.hg tbody tr.hg-new:hover td { background: var(--surface); }
table.hg tbody tr.hg-new td.hg-editing { background: var(--surface); }
/* The `+` is the one thing on the line that is drawn, so it is drawn to be
   found: the row number's muted grey would hide it against the tint. */
.hg-num .hg-n-new {
  color: var(--ink-2); font-weight: var(--w-bold); font-size: var(--t-small);
  cursor: default;
}
table.hg tbody tr.hg-new:hover .hg-n-new { color: var(--blue); }

/* ── AN EDITABLE CELL, WHEN A GRID OPTED IN ─────────────────────────────────
   The affordance is deliberately quiet: a cursor and a hover tint, not a
   border on every cell. A board of 320 columns outlined like a form is a wall
   of boxes, and Airtable does not do it either — the cell looks like a cell
   until you are on it. */
table.hg td.hg-can { cursor: text; }
table.hg td.hg-can:hover { background: var(--surface-hover); }
table.hg td.hg-can:focus-visible {
  outline: 2px solid var(--blue); outline-offset: -2px;
}
table.hg td.hg-editing { padding: 0; background: var(--surface); }

.hg-ed {
  width: 100%; border: 2px solid var(--blue); border-radius: 4px;
  padding: 5px var(--space-3); font: inherit; color: var(--ink);
  background: var(--surface); outline: none;
}
.hg-ed:disabled { opacity: 0.7; }
/* A LONG NOTE EDITS AS A BOX, NOT A LINE. A one-line input strips every line
   break it is given, so a multi-line note opened and left behind came back as
   one run of words. The box is taller than the row and overlays the ones
   below it, which is what Airtable does and what the kind needs — a note
   worth keeping is longer than a cell. */
textarea.hg-ed {
  display: block; min-height: 72px; resize: vertical; line-height: 1.4;
  position: relative; z-index: 2;
}

.hg-saving {
  display: block; padding: 2px var(--space-3);
  font-size: var(--t-micro); color: var(--ink-3);
}
/* THE REFUSAL SITS ON THE CELL IT IS ABOUT. The whole objection to editing in
   a grid was that a change could not be explained where it happened; a toast
   somewhere else would have proved the objection right. */
.hg-bad {
  display: block; padding: 2px var(--space-3);
  font-size: var(--t-micro); color: var(--red); line-height: 1.35;
}
/* THE ONLY FIGURE IN THE GRID THAT EARNS COLOUR. Late in the same ink as
   everything else makes the deadline decorative — the same rule the board's
   cards already follow, and the same red, so a request that is late looks late
   in both views. */
.hg-late { color: var(--red); font-weight: var(--w-bold); }

/* The pill. Same shape and the same colours as the board's, because a status
   wears one colour wherever it appears. */
.hg-pill {
  display: inline-flex; align-items: center; padding: 2px 8px;
  border-radius: 20px; font-size: var(--t-micro); font-weight: var(--w-medium);
  white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis;
  /* A HAIRLINE, SO A NEUTRAL PILL IS STILL A PILL. The coloured ones read on
     their own fill; a value with no colour of its own — a task type, say — sat
     on `--surface-2` against a white row and read as plain text that happened
     to be indented. The border is what makes it the same object. */
  border: 1px solid rgba(25,25,23,0.07);
}

/* ── Group headers ───────────────────────────────────────────────────────────
   The field name small and grey above the value, the count on the right, and a
   chevron that collapses it. Sticky under the header row, so scrolling a long
   group never leaves you wondering which one you are in. */
.hg-group td {
  background: var(--surface-2); padding: 0;
  position: sticky; top: 30px; z-index: 1;
  border-right: none;
}
.hg-gh {
  display: flex; align-items: center; gap: var(--space-3); width: 100%;
  font: inherit; background: none; border: none; cursor: pointer; text-align: left;
  padding: 6px var(--space-4); min-height: 34px;
}
.hg-gh:hover { background: var(--surface); }
.hg-chev { color: var(--ink-3); font-size: var(--t-small); width: 12px; }
.hg-gf {
  font-size: var(--t-micro); font-weight: var(--w-bold); text-transform: uppercase;
  letter-spacing: 0.05em; color: var(--ink-3);
}
.hg-gv { font-size: var(--t-body); font-weight: var(--w-bold); color: var(--ink); }
.hg-gn {
  margin-left: auto; font-size: var(--t-small); color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}

/* ── Menus ───────────────────────────────────────────────────────────────── */
/* NOT `position: relative`. The host is only where the menu is parked in the
   DOM; the menu itself is fixed to the viewport, because a card with
   `overflow: hidden` clips anything absolutely positioned inside it — which is
   what turned every dropdown into a sliver whenever the grid was short. */
.hg-menu-host { position: static; }
.hg-menu {
  position: fixed; z-index: 60; min-width: 210px; max-width: 280px;
  max-height: min(340px, 70vh); overflow-y: auto;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-2); box-shadow: 0 12px 34px rgba(25,25,23,0.14);
  padding: var(--space-2);
}
.hg-mi {
  display: flex; align-items: center; gap: var(--space-2); width: 100%;
  font: inherit; font-size: var(--t-small); color: var(--ink);
  background: none; border: none; text-align: left; cursor: pointer;
  padding: 7px var(--space-3); border-radius: var(--radius-3); min-height: 32px;
}
.hg-mi:hover { background: var(--surface-2); }
.hg-mi.on { background: var(--ai-soft); color: var(--ai-strong); font-weight: var(--w-medium); }
.hg-mi input { width: auto; min-height: 0; margin: 0; }
.hg-mrow { justify-content: space-between; }
.hg-x {
  font: inherit; background: none; border: none; cursor: pointer;
  color: var(--ink-3); padding: 2px 6px; border-radius: var(--radius-3);
}
.hg-x:hover { background: var(--surface-2); color: var(--ink); }
.hg-ml {
  font-size: var(--t-micro); font-weight: var(--w-bold); text-transform: uppercase;
  letter-spacing: 0.05em; color: var(--ink-3); padding: var(--space-2) var(--space-3) 4px;
}
.hg-sep { height: 1px; background: var(--border); margin: var(--space-2) 0; }
.hg-empty { padding: var(--space-6) var(--space-4); text-align: center; color: var(--ink-3); }

/* ── Adding a field from the header ─────────────────────────────────────────
   The `+` where the columns end, and the panel it opens. Airtable's placement,
   because it is the right one: the control that makes a column sits at the
   edge of the last one, so it reads as "and then another" rather than as a
   setting filed under a menu.

   The cell is DELIBERATELY NARROW AND NOT STRETCHED. A header cell with no
   width competes with the real columns for the table's width, and on a board
   of six fields that gave a `+` the same share as an address. */
.hg-addh, .hg-addc { width: 44px; min-width: 44px; padding: 0 !important; }
.hg-addb {
  display: flex; align-items: center; justify-content: center;
  width: 100%; min-height: 30px; font: inherit; font-size: var(--t-body);
  color: var(--ink-3); background: none; border: none; cursor: pointer;
}
.hg-addb:hover { color: var(--ink); background: var(--surface-2); }

/* The panel is wider than a list of options and it SCROLLS ITS LIST, not
   itself: a search box that scrolls away from the results it is filtering is
   a search box you lose while using it. */
.hg-menu.hg-af { min-width: 268px; max-width: 320px; max-height: none; overflow: visible; }
.hg-afbox { display: flex; flex-direction: column; gap: var(--space-2); }
.hg-afbox .inp { width: 100%; }
.hg-aflist { max-height: min(300px, 52vh); overflow-y: auto; }
.hg-afk { justify-content: flex-start; }
/* The marker the keyboard moves. Same fill as :hover so arrowing down and
   moving the mouse do not look like two different states. */
.hg-afk.on { background: var(--surface-2); }
.hg-afmore { margin-left: auto; color: var(--ink-3); }
.hg-afback { color: var(--ink-3); }
.hg-afnone { padding: var(--space-3); color: var(--ink-3); font-size: var(--t-small); }
.hg-aff { display: flex; flex-direction: column; gap: 4px; }
.hg-aff > span {
  font-size: var(--t-micro); font-weight: var(--w-bold); text-transform: uppercase;
  letter-spacing: 0.05em; color: var(--ink-3);
}
.hg-afhint { font-size: var(--t-micro); color: var(--ink-3); }
.hg-afgo { width: 100%; }
/* The same ink a refused CELL uses, not a second red. One colour for "the
   server would not have that", wherever the refusal lands. */
.hg-afbad {
  font-size: var(--t-micro); color: var(--red); line-height: 1.35;
  padding: 2px var(--space-3);
}
/* Progress, in the quiet ink the rest of the panel's hints use — the red
   above is for a refusal, and "Adding the column…" is not one. */
.hg-afwait {
  font-size: var(--t-micro); color: var(--ink-3); line-height: 1.35;
  padding: 2px var(--space-3);
}

/* ── Touch ───────────────────────────────────────────────────────────────────
   HARRIS.CSS ALREADY GUARANTEES 44px UNDER A COARSE POINTER, and every
   `min-height` above quietly beat it. A class selector outranks the bare
   `button` the global rule uses, so `.hg-chip { min-height: 30px }` took the
   grid's whole toolbar back under the line this app holds itself to — and
   `height` on the search box beat it outright, since `height` wins over
   `min-height` whatever the specificity.

   NOTHING CAUGHT IT. `verify-mobile` walks the page as loaded and the list view
   is hidden behind a tab, so every control here measured zero and was skipped:
   the sweep passed because it could not see them, which is not the same as
   passing. Restored explicitly, where the reason can be read. */
@media (pointer: coarse) {
  .hb-chip, .hg-chip, .hg-mi, .hg-h, .hg-gh, .hg-addb { min-height: 44px; }
  .hg-search .inp { height: 44px; }
  /* The expand button is out of flow, so it takes its target from its own box
     rather than from the row it sits in. */
  .hg-open { min-width: 44px; min-height: 44px; padding: 0; }
}

/* ON A PHONE the grid scrolls sideways rather than reflowing. A twelve-field
   record cannot become one column without stopping being a grid, and the
   scroller is focusable so a keyboard reaches the right-hand fields — the same
   treatment the board already gets. */
@media (max-width: 768px) {
  .hg-search { margin-left: 0; width: 100%; }
  .hg-search .inp { width: 100%; }
  table.hg { min-width: 720px; }
}


/* ── An avatar ───────────────────────────────────────────────────────────────
   Initials in a disc. A name spelled out costs a whole line beside a deadline
   and reads as prose; two letters read as a person at a glance, which is what
   a board is scanned for. The full name is on the title attribute and in the
   accessible label, so nothing is lost — it is just not shouted. */
.hb-who {
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; border-radius: 11px; flex-shrink: 0;
  font-size: var(--t-micro); font-weight: var(--w-bold); letter-spacing: 0.01em;
  background: var(--surface-2); color: var(--ink-2);
  border: 1px solid var(--border);
}
/* NOBODY IS DRAWN AS AN EMPTY DISC, not left blank. On a board the difference
   between "nobody has this" and "the column is narrow" has to be visible, and
   an absent avatar reads as the second. */
.hb-who.none { border-style: dashed; color: var(--ink-3); }

/* ── The way back to Settings, from a Settings page that is its own page ────
   Two of the Settings cards are links rather than panes, because the pages
   behind them are more than a settings pane: System Health has tabs and live
   polling, Integrations is a whole map. Both are reached from Settings and
   both need the way back, so the way back is defined once.

   NOT admin.html's .settings-back, which looks the same and is not the same
   thing: that is a BUTTON that swaps a pane for the grid inside one page, and
   it is hidden until `body.in-section`. This is a link to another address and
   is always there. The two were briefly the same six declarations written out
   twice, which is how a rule ends up defined twice and fixed once.

   44px tall, so it is a real target under a thumb. */
.hb-back-to-settings {
  display: inline-flex; align-items: center; gap: 7px;
  margin-bottom: 18px; padding: 10px 14px 10px 10px; min-height: 44px;
  font-size: var(--t-body); font-weight: var(--w-medium);
  color: var(--text-secondary); text-decoration: none;
  border: 1px solid transparent; border-radius: var(--radius-sm);
}
/* A wash rather than --surface-hover: the same trade admin.html records, a
   cool grey reads as a different panel against this warm canvas. */
.hb-back-to-settings:hover { background: rgba(255, 255, 255, 0.7); color: var(--text); }
.hb-back-to-settings svg { width: 16px; height: 16px; }

/* ── "This is going to AgentBox" ────────────────────────────────────────────
   The confirm panel shared/agentbox-confirm.js builds. It reuses
   .modal-backdrop > .modal so shared/modal.js supplies role="dialog",
   aria-modal, a label and the focus trap; everything here is what the panel
   holds rather than how it behaves.

   THE FIELD TABLE IS THE POINT OF THE SCREEN, so it gets the weight. The
   heading says what will change and the table says exactly what with — a
   person is being asked to authorise a change in somebody else's system of
   record, and they can only do that if they can read what is being sent. */
/* IT SAYS ITS OWN OPEN, and it has to. harris.css deliberately leaves `display`
   to the page — five open/close protocols are in use across the app and no
   shared rule can infer which one an element follows, so `.modal-backdrop` is
   `display: none` and each page carries its own selector (admin.html has
   `.modal-backdrop:not(.hidden) { display: flex; }`).

   That reasoning is about markup a PAGE owns. This backdrop is built by
   shared/agentbox-confirm.js, which any page can call — including one with no
   such rule, which is exactly what happened the first time this was opened in a
   browser: role="dialog" and the focus trap were both applied correctly and the
   panel was invisible. A component that owns its own markup and its own class
   must state its own protocol, scoped to that class so it cannot decide
   anything for a modal the page built. */
.abx-back:not(.hidden) { display: flex; }

.abx-modal { max-width: 34rem; padding: 0; }
.abx-head { padding: var(--space-5) var(--space-5) var(--space-4); }
.abx-title { font-size: var(--t-title); font-weight: var(--w-bold); color: var(--ink); }
.abx-effect { font-size: var(--t-small); color: var(--ink-2); margin-top: 6px; line-height: 1.55; }
.abx-body { padding: 0 var(--space-5) var(--space-4); }
/* ── Who owns it in AgentBox ──
   ABOVE THE PAYLOAD, because it changes the payload. An administrator creating
   a contact for an agent picks the agent here; the note underneath still names
   whoever pressed Save. Changing it re-proposes, so the table below always
   shows what will actually be sent. */
.abx-own { padding-bottom: var(--space-4); margin-bottom: var(--space-4);
  border-bottom: 1px solid var(--border); }
.abx-own-l { display: block; font-size: var(--t-micro); font-weight: var(--w-medium);
  color: var(--ink-3); text-transform: uppercase; letter-spacing: .05em; margin-bottom: 7px; }
.abx-own-s { width: 100%; }
.abx-own-h { font-size: var(--t-small); color: var(--ink-3); margin-top: 7px; line-height: 1.5; }

.abx-note { font-size: var(--t-micro); font-weight: var(--w-medium); color: var(--ink-3);
  text-transform: uppercase; letter-spacing: .05em; margin-bottom: 8px; }

.abx-fields { width: 100%; border-collapse: collapse; font-size: var(--t-body); }
.abx-fields th, .abx-fields td {
  text-align: left; padding: 9px 0; border-bottom: 1px solid var(--border);
  vertical-align: top; }
.abx-fields tr:last-child th, .abx-fields tr:last-child td { border-bottom: 0; }
.abx-fields th { font-weight: var(--w-regular); color: var(--ink-3); width: 40%; padding-right: 14px; }
.abx-fields td { color: var(--ink); font-weight: var(--w-medium); word-break: break-word; }

/* A FIELD NOBODY HAS NAMED. The server shows unknown keys under their raw name
   rather than dropping them, because a label map used as a filter is how a
   field reaches the CRM having never been on screen. This makes that visible
   instead of merely present — the fix is to add the label, and looking wrong
   is what makes somebody do it. */
.abx-fields tr.abx-unlabelled th { font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--t-small); color: var(--amber-ink);
  /* harris.css uppercases every `th` — the house table-header style, and right
     for the labelled rows. It is wrong here: LEADSCOREV2 reads as another
     heading, when the whole reason a raw key is shown is that it should look
     like one nobody has named yet. */
  text-transform: none; letter-spacing: 0; }

.abx-warn { margin-top: var(--space-4); padding: 11px 13px; border-radius: var(--radius-sm);
  background: var(--amber-soft); color: var(--amber-ink); font-size: var(--t-small);
  line-height: 1.5; }

.abx-foot { display: flex; justify-content: flex-end; gap: 10px;
  padding: var(--space-4) var(--space-5); border-top: 1px solid var(--border);
  background: var(--surface-2); border-radius: 0 0 var(--radius-2) var(--radius-2); }
/* harris.css guarantees 44px under `@media (pointer: coarse)` with a bare
   `button` selector, and any class here with a smaller min-height would
   silently outrank it. Nothing below sets one. */
/* 560, the canonical narrow breakpoint — not a width of my own. The app
   consolidated onto two (560 and 768) plus five named component collapse
   points, and __tests__/breakpoints.test.js refuses a sixth without a reason.
   Nothing here needs one: the footer buttons and the field table both have
   room down to 560. */
@media (max-width: 560px) {
  .abx-foot { flex-direction: column-reverse; }
  .abx-foot .btn { width: 100%; }
  .abx-fields th { width: 45%; }
}

/* ── Search dropdown ─────────────────────────────────────────────────────────
   The panel shared/harris-search.js builds under a page's own search bar.

   OWNS ITS OWN OPEN, for the reason recorded above the AgentBox backdrop: the
   markup belongs to the component, not the page, so nothing here may depend on
   a rule a page happens to carry. It uses the `hidden` attribute, which
   harris.css already forces to `display:none !important`, and the panel's own
   `display` below only ever applies when that attribute is absent. */
.hs-anchor { position: relative; }

.hs-panel {
  position: absolute; left: 0; right: 0; top: calc(100% + 6px); z-index: 60;
  max-height: min(60vh, 420px); overflow-y: auto; overscroll-behavior: contain;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-2); box-shadow: var(--shadow-lg);
  padding: 6px; text-align: left;
}

/* The screen-reader count. Off-screen rather than `display:none`, which would
   stop it being announced at all — the point of it is that somebody who cannot
   see the panel is told how much arrived. */
.hs-live {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

.hs-glabel {
  font-size: var(--t-micro); font-weight: var(--w-medium); color: var(--ink-3);
  letter-spacing: .04em; text-transform: uppercase;
  padding: 10px 10px 5px;
}
.hs-group + .hs-group { border-top: 1px solid var(--border); }

.hs-row {
  display: flex; align-items: center; gap: 11px; width: 100%;
  padding: 8px 10px; border: 0; border-radius: var(--radius-sm);
  background: none; font: inherit; color: inherit; text-align: left;
  text-decoration: none; cursor: pointer;
}
/* HOVER AND KEYBOARD LAND ON THE SAME CLASS. Two appearances for "this is the
   one you are about to open" is how somebody arrows down three rows and clicks
   a fourth. */
.hs-row:hover, .hs-row.is-active { background: var(--surface-2); }
.hs-row:focus-visible { outline: 2px solid var(--blue); outline-offset: -2px; }

.hs-text { flex: 1; min-width: 0; }
.hs-title {
  display: block; font-size: var(--t-body); color: var(--ink); line-height: 1.35;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.hs-title mark {
  background: var(--blue-soft); color: var(--blue-ink);
  border-radius: 3px; padding: 0 1px;
}
.hs-sub {
  display: block; font-size: var(--t-micro); color: var(--ink-3); margin-top: 2px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.hs-go { flex-shrink: 0; color: var(--ink-3); display: flex; }
.hs-go svg { width: 15px; height: 15px; }

/* The neutral stand-in for a module the registry does not know, sized to match
   the marks around it so one unknown source cannot shift a whole column. */
.hs-mark {
  width: 26px; height: 26px; border-radius: 8px; flex-shrink: 0;
  display: grid; place-items: center;
}
.hs-mark-none { background: var(--surface-2); border: 1px solid var(--border); }
.hs-mark-ask { background: var(--blue-soft); color: var(--blue); }
.hs-mark-ask svg { width: 15px; height: 15px; }
/* A row that came from somebody's own mailbox wears that account's logo, on a
   neutral ground for the same reason the briefing cards use one: the artwork
   carries its own colours and a tint behind it is two signals fighting. */
.hs-mark-src { background: var(--surface-2); border: 1px solid var(--border); }
.hs-mark-src svg { width: 16px; height: 16px; }

.hs-ask .hs-title,
.hs-ms .hs-title { font-weight: var(--w-medium); }
.hs-said {
  display: flex; align-items: center; gap: 8px; padding: 9px 10px;
  font-size: var(--t-small); color: var(--ink-2); line-height: 1.45;
}
.hs-said svg { width: 15px; height: 15px; color: var(--blue); flex-shrink: 0; }

.hs-none {
  padding: 14px 10px; font-size: var(--t-small); color: var(--ink-2); line-height: 1.5;
}
.hs-thinking { display: flex; align-items: center; gap: 8px; }
.hs-thinking svg { width: 15px; height: 15px; color: var(--blue); }

/* ── A search that is still going ────────────────────────────────────────────
   THE CASE THIS IS FOR: results for "smi" are on screen, "smith" has been
   typed, and the second request has not landed. Those rows are stale and look
   exactly as current as they did a moment ago. The component marks the panel
   `is-busy` once a request has been out longer than the eye can excuse, and
   this is what that looks like.

   THE ROWS ARE DIMMED, NOT REMOVED, and they stay clickable. Somebody reading
   row three should not have it taken away for a query that may well return the
   same rows — and if the row they want is already visible, making them wait to
   click it would be the component protecting its own tidiness at their
   expense.

   `> *` so the sweep below is untouched: it is a pseudo-element and not a
   child, which is the whole reason it can sit inside a dimmed panel and stay
   at full strength. */
.hs-panel.is-busy > * { opacity: .45; transition: opacity .12s ease; }

/* The sweep, along the panel's top edge. `sticky` rather than absolute because
   the panel scrolls: an absolutely positioned bar would scroll away with the
   first group and stop being a status. The negative margins cancel the panel's
   own 6px padding so it meets both edges. */
.hs-panel.is-busy::before {
  content: ""; display: block; position: sticky; top: 0; z-index: 1;
  height: 2px; margin: -6px -6px 4px; border-radius: 2px;
  background-image: linear-gradient(90deg, transparent, var(--blue), transparent);
  background-size: 40% 100%; background-repeat: no-repeat;
  animation: hs-sweep 1.1s ease-in-out infinite;
}
@keyframes hs-sweep {
  from { background-position: -40% 0; }
  to   { background-position: 140% 0; }
}
/* Motion is the signal here, so with motion turned off the bar stays and stops
   moving rather than disappearing — a still hairline is still "something is
   happening", and removing it would leave only the dimming to carry it. */
@media (prefers-reduced-motion: reduce) {
  .hs-panel.is-busy::before {
    animation: none;
    background-image: linear-gradient(90deg, var(--blue), var(--blue));
    background-size: 100% 100%;
    opacity: .5;
  }
}
.hs-note {
  padding: 9px 10px 6px; margin-top: 4px; font-size: var(--t-micro);
  color: var(--ink-3); line-height: 1.5;
  border-top: 1px solid var(--border);
}

/* 560, the canonical narrow breakpoint. On a phone the panel is the screen for
   as long as it is open, so it gets the height rather than floating in a strip
   above the fold. */
@media (max-width: 560px) {
  .hs-panel { max-height: min(70vh, 460px); }
  .hs-sub { white-space: normal; }
}

/* The Ask row and the not-searched note, pinned to the bottom of the scroller.
   `bottom: -6px` cancels the panel's own padding so the footer sits flush on
   the edge rather than floating a gap above it. */
.hs-foot {
  position: sticky; bottom: -6px; background: var(--surface);
  border-top: 1px solid var(--border); margin-top: 4px;
}
.hs-foot .hs-note { border-top: 0; padding-top: 2px; }

/* ── A plain form field ──────────────────────────────────────────────────────
   THE VOCABULARY TWO CANVAS PAGES ALREADY SPEAK, given a floor. marketing.html
   writes `.field` / `.inp` / `.hint` on its controls and styles all three in
   its own <style>. notes.html wrote the same three classes and styled none of
   them — and nothing here defined them either — which in a browser is labels
   beside their boxes, a title input the width of its placeholder, and the
   note itself a textarea at the browser's default forty columns. Found from a
   screenshot: the page's verifier read every word off the editor and every
   word was present, because a class nothing defines is not an error anywhere.

   `:where()` ON PURPOSE. It has zero specificity, so this is a floor and not
   a ceiling: a page that defines its own `.inp` keeps every line of it —
   marketing's purple AI focus ring included — and a page that defines nothing
   gets a field that looks like the rest of the app. Written as bare `.inp`,
   these would sit later in the cascade than any page's <style> and silently
   overrule it.

   WHAT IS DELIBERATELY NOT HERE. harris.css already gives every text control
   its height, its radius and its focus ring (`input:not(...), select` and
   `input:focus, select:focus, textarea:focus`), at higher specificity than
   this can reach. Restating them would be a second answer to a settled
   question. Textarea is the exception — harris.css's radius rule does not
   name it — so it gets its own below. */
:where(.field) { margin-bottom: var(--space-4); }
:where(.field) > label {
  display: block; font-size: var(--t-small); font-weight: var(--w-medium);
  color: var(--ink-2); margin-bottom: 6px;
}
:where(.inp) {
  width: 100%; padding: 8px 12px;
  border: 1px solid var(--border); background: var(--surface); color: var(--ink);
}
:where(textarea.inp) {
  min-height: 180px; resize: vertical; line-height: 1.55;
  border-radius: var(--radius-3);
}
:where(.hint) {
  font-size: var(--t-micro); color: var(--ink-3); line-height: 1.5; margin-top: 6px;
}

/* ── Quiet text ──────────────────────────────────────────────────────────────
   `.muted` is written on canvas pages — the settings panes, PM EOM operations
   — and defined on no stylesheet any of them loads, so "never", "Retired" and
   two whole explanatory paragraphs rendered in body ink, indistinguishable
   from the thing they were meant to be quieter than. Same floor as the fields
   above: zero specificity, so a page that means something else by it keeps
   its own. */
:where(.muted) { color: var(--ink-3); }

/* ── Welcome to Hank ─────────────────────────────────────────────────────────
   The onboarding dialog: a greeting, then the three fields that make a
   document come out right, beside a live preview of the sign-off they produce.
   Markup is built by shared/harris-onboarding.js; `.hob-back` and `.hob-panel`
   are registered in shared/modal.js's overlay list, so the focus trap and the
   scroll lock are the shared ones rather than a third implementation.

   THE ANIMATION IS A SEQUENCE, not a set of independent fades. The lockup
   arrives, the name follows it, then the line, then the button — each on the
   one before, so it reads as one movement rather than four things twitching.
   Every step is `backwards`-filled from an invisible start and lands at rest,
   so the finished state is the still frame somebody actually reads. */
.hob-back {
  position: fixed; inset: 0; z-index: 1000;
  display: grid; place-items: center;
  padding: var(--space-5);
  background: rgba(21, 23, 26, .38);
  backdrop-filter: blur(3px);
  animation: hob-in .18s ease both;
}
@keyframes hob-in { from { opacity: 0; } to { opacity: 1; } }

.hob-panel {
  width: 100%; max-width: 720px; max-height: 88vh; overflow-y: auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  padding: var(--space-7);
  animation: hob-rise .28s cubic-bezier(.2, .8, .3, 1) both;
}
@keyframes hob-rise {
  from { opacity: 0; transform: translateY(10px) scale(.985); }
  to   { opacity: 1; transform: none; }
}

/* ── The greeting ─────────────────────────────────────────────────────────── */
.hob-welcome { text-align: center; padding: var(--space-5) 0; }

.hob-lockup { display: flex; justify-content: center; margin-bottom: var(--space-5); }
/* --hb-logo-h is the lockup's one knob, the same way the sign-in page sizes
   it — everything else in the composed mark is a fraction of it, so there is
   one number to change and not five. */
.hob-lockup { --hb-logo-h: 46px; }
.hob-lockup .hb-brand { --hb-logo-h: 46px; }
/* The uploaded lockup, when there is one, replaces the composed mark whole. */
.hob-lock-logo { height: var(--hb-logo-h); max-width: 100%; }

/* ── The composed lockup ──────────────────────────────────────────────────
   Shown until Admin > Branding has a logo: the shipped H. glyph, the name, a
   hairline, and the shipped HARRIS mark. It is deliberately built out of real
   artwork and real type rather than a traced approximation of the lockup —
   Harris owns that file, and a hand-drawn near-miss of a company's own logo is
   worse on a welcome screen than an honest arrangement of the parts. */
.hob-lock { display: inline-flex; align-items: center; gap: calc(var(--hb-logo-h) * .26); }
.hob-lock-icon { height: var(--hb-logo-h); width: var(--hb-logo-h); }
/* THE TWO TYPE SIZES COME OFF THE SCALE, not off --hb-logo-h. Sizing them as
   a fraction of the lockup knob was the obvious way to write this and
   __tests__/type-scale rejected it: the app has eight font sizes and a mark is
   not a reason for a ninth. --t-display beside a 46px glyph and --t-hero
   beside the 36px one are within a pixel of the fractions they replace, so the
   scale cost nothing here — which is the usual answer when a guard like that
   fires. The lengths below stay proportional; only sizes are on the scale. */
.hob-lock-name {
  font-size: var(--t-display); font-weight: var(--w-bold);
  letter-spacing: -.035em; line-height: 1; color: var(--ink);
}
.hob-lock-rule {
  width: 1px; height: calc(var(--hb-logo-h) * .62);
  background: var(--border-strong);
}
/* "by" and the mark are one phrase, so they sit closer to each other than the
   rule sits to either — spacing is what says which words belong together. */
.hob-lock-by {
  font-size: var(--t-small); color: var(--ink-3);
  margin-right: calc(var(--hb-logo-h) * -.12);
}
.hob-lock-harris { height: calc(var(--hb-logo-h) * .30); }

.hob-lockup { animation: hob-step .5s cubic-bezier(.2, .8, .3, 1) both; }
.hob-hi     { animation: hob-step .5s cubic-bezier(.2, .8, .3, 1) .16s both; }
.hob-lede   { animation: hob-step .5s cubic-bezier(.2, .8, .3, 1) .28s both; }
.hob-can    { animation: hob-step .5s cubic-bezier(.2, .8, .3, 1) .38s both; }
.hob-foot   { animation: hob-step .5s cubic-bezier(.2, .8, .3, 1) .48s both; }
.hob-acts   { animation: hob-step .5s cubic-bezier(.2, .8, .3, 1) .56s both; }
@keyframes hob-step {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}

.hob-hi {
  font-size: var(--t-hero); font-weight: var(--w-bold); color: var(--ink);
  letter-spacing: -.02em; line-height: 1.15; margin-bottom: var(--space-3);
  text-wrap: balance;
}
.hob-lede {
  font-size: var(--t-section); color: var(--ink-2); line-height: 1.5;
  /* A PX MEASURE, NOT A ch COUNT. __tests__/marketing-agent-view holds the
     canvas to that for the page lede and the reasoning is the same here: a ch
     cap is a count of characters in whichever font happens to load, which is
     not the thing anybody was measuring. 420px is roughly two lines of this
     size on the panel's width. */
  max-width: 420px; margin: 0 auto var(--space-5);
}

/* ── What Hank does ───────────────────────────────────────────────────────
   LEFT-ALIGNED INSIDE A CENTRED BLOCK. The greeting above is centred and this
   is a list: centring the lines too would leave three ragged left edges and
   nothing to run the eye down, which is the one job a list has. So the block
   is centred and its contents are not. */
.hob-can {
  list-style: none; display: flex; flex-direction: column; gap: var(--space-2);
  width: fit-content; max-width: 100%; margin: 0 auto var(--space-5);
  text-align: left;
}
.hob-can-item {
  display: flex; align-items: flex-start; gap: var(--space-3);
  font-size: var(--t-body); color: var(--ink); line-height: 1.45;
}
.hob-can-spark {
  flex: none; width: 16px; height: 16px; margin-top: .14em;
  color: var(--ink-3);
}
.hob-can-spark svg { display: block; width: 100%; height: 100%; }
.hob-foot {
  font-size: var(--t-body); color: var(--ink-3); line-height: 1.5;
  max-width: 420px; margin: 0 auto var(--space-6);
}

.hob-acts { display: flex; flex-direction: column; align-items: center; gap: var(--space-3); }
.hob-acts-end {
  flex-direction: row; justify-content: flex-end; align-items: center;
  gap: var(--space-3); margin-top: var(--space-6);
}

/* BLACK, WITH WHITE WRITING, as asked for — and it is also what Hank's primary
   action already is: --primary is the near-black ink, not a blue. So this is
   the app's own primary button at a size that suits a single decision on an
   otherwise empty screen, rather than a colour invented for this dialog. */
.hob-go {
  min-height: 48px; padding: 0 var(--space-6);
  border: 1px solid var(--primary); border-radius: var(--radius-3);
  background: var(--primary); color: var(--surface);
  font: inherit; font-size: var(--t-title); font-weight: var(--w-medium);
  cursor: pointer; transition: background-color .15s ease, transform .1s ease;
}
.hob-go:hover { background: var(--primary-light); }
.hob-go:active { transform: translateY(1px); }
.hob-go:focus-visible { outline: 2px solid var(--blue); outline-offset: 3px; }

.hob-later {
  min-height: 44px; padding: 0 var(--space-4);
  border: 0; background: none; font: inherit; font-size: var(--t-body);
  color: var(--ink-2); cursor: pointer; border-radius: var(--radius-3);
}
.hob-later:hover { color: var(--ink); text-decoration: underline; }

/* QUIETER THAN "LATER", because later is the right answer for most people and
   this one cannot be taken back from here. Offered rather than hidden, though:
   the person it annoys is exactly the person whose profile it is asking
   about, and a reminder with no off switch is the same mistake as a modal on
   every page, only slower. */
.hob-stop {
  margin-top: var(--space-2);
  min-height: 32px; padding: 0 var(--space-3);
  border: 0; background: none; font: inherit;
  font-size: var(--t-small); color: var(--ink-3);
  cursor: pointer; border-radius: var(--radius-3);
}
.hob-stop:hover { color: var(--ink-2); text-decoration: underline; }
.hob-stop:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }
/* It says what stopping leaves behind, so switching it off does not read as
   losing the way back in. */
.hob-stop-note {
  font-size: var(--t-micro); color: var(--ink-3); line-height: 1.5;
}
.hob-later:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }

/* ── The form ─────────────────────────────────────────────────────────────── */
.hob-head { margin-bottom: var(--space-6); }
.hob-h {
  font-size: var(--t-page); font-weight: var(--w-bold); color: var(--ink);
  letter-spacing: -.02em; margin-bottom: var(--space-2);
}
.hob-sub { font-size: var(--t-body); color: var(--ink-2); margin-bottom: var(--space-4); }

.hob-bar { height: 4px; border-radius: 999px; background: var(--surface-2); overflow: hidden; }
.hob-bar-fill {
  display: block; height: 100%; width: 0; border-radius: 999px;
  background: var(--blue); transition: width .4s cubic-bezier(.4, 0, .2, 1);
}

/* The form and the preview side by side, because the preview is the argument
   for the form and the two have to be read together. */
.hob-cols { display: grid; grid-template-columns: 1.15fr .85fr; gap: var(--space-6); align-items: start; }
.hob-form { display: flex; flex-direction: column; gap: var(--space-5); }
.hob-field { display: flex; flex-direction: column; gap: 6px; }
.hob-label { font-size: var(--t-small); font-weight: var(--w-medium); color: var(--ink); }
/* WHY THE FIELD IS WANTED, under it. A form that asks for a phone number
   without saying whose hands it ends up in gets a number that rings at a
   barbecue. */
.hob-why { font-size: var(--t-micro); color: var(--ink-3); line-height: 1.5; }

.hob-photo-row { display: flex; align-items: center; gap: var(--space-4); }
.hob-photo {
  width: 56px; height: 56px; border-radius: 50%; flex-shrink: 0;
  display: grid; place-items: center; overflow: hidden;
  background: var(--surface-2); color: var(--ink-2);
  font-size: var(--t-title); font-weight: var(--w-bold);
}
.hob-photo img { width: 100%; height: 100%; object-fit: cover; }
.hob-photo-acts { display: flex; gap: var(--space-2); flex-wrap: wrap; }

/* ── The preview ──────────────────────────────────────────────────────────── */
.hob-preview {
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--radius-2); padding: var(--space-4);
  display: flex; flex-direction: column; gap: var(--space-3);
}
.hob-preview-cap {
  font-size: var(--t-micro); font-weight: var(--w-bold); color: var(--ink-3);
  letter-spacing: .04em; text-transform: uppercase;
}
/* White, because it is standing in for a sheet of paper. */
.hob-card {
  display: flex; gap: var(--space-3); align-items: flex-start;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-3); padding: var(--space-4);
}
.hob-card-mark {
  width: 40px; height: 40px; border-radius: 50%; flex-shrink: 0;
  display: grid; place-items: center; overflow: hidden;
  background: var(--surface-2); color: var(--ink-2);
  font-size: var(--t-small); font-weight: var(--w-bold);
}
.hob-card-mark img { width: 100%; height: 100%; object-fit: cover; }
.hob-card-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.hob-card-kicker {
  font-size: var(--t-micro); font-weight: var(--w-bold); color: var(--ink-3);
  letter-spacing: .04em; text-transform: uppercase;
}
.hob-card-name { font-size: var(--t-body); font-weight: var(--w-bold); color: var(--ink); }
.hob-card-role, .hob-card-reach { font-size: var(--t-micro); color: var(--ink-2); line-height: 1.45; }
/* THE GAP, SHOWN AS A GAP. A missing title is drawn as the placeholder it
   would leave on the letter rather than as an empty line, because an empty
   line looks like a design decision and a greyed "Your title here" looks like
   the hole it is. */
.hob-card-role.is-missing, .hob-card-reach.is-missing {
  color: var(--ink-3); font-style: italic;
  background: var(--amber-soft); border-radius: 3px; padding: 0 4px;
  align-self: flex-start;
}

.hob-msg { font-size: var(--t-small); color: var(--red-ink); min-height: 1.2em; margin-top: var(--space-4); }

/* ── Finished ─────────────────────────────────────────────────────────────── */
.hob-tick {
  width: 64px; height: 64px; border-radius: 50%; margin: 0 auto var(--space-5);
  display: grid; place-items: center;
  background: var(--green-soft); color: var(--green-ink);
  animation: hob-pop .45s cubic-bezier(.2, 1.3, .4, 1) both;
}
.hob-tick svg { width: 30px; height: 30px; }
@keyframes hob-pop {
  from { opacity: 0; transform: scale(.6); }
  to   { opacity: 1; transform: none; }
}

/* One column on a phone, and the preview goes UNDER the form rather than
   beside it — at 390 a two-column layout gives each half 170px, which is not
   enough for either. */
/* 768, the canonical tablet step, not a width chosen for this dialog:
   __tests__/breakpoints keeps the app to a fixed set so twenty pages cannot
   each invent one. The panel is 720px wide, so it starts sharing the screen a
   little before it has to — which is the correct side to err on. */
@media (max-width: 768px) {
  .hob-panel { padding: var(--space-5); max-height: 92vh; }
  .hob-cols { grid-template-columns: 1fr; gap: var(--space-5); }
  .hob-hi { font-size: var(--t-page); }
  .hob-lede { font-size: var(--t-body); }
  /* The lockup shrinks by its one knob, so the H., the rule and the HARRIS
     mark all come down together; the name takes the scale step below. */
  .hob-lockup, .hob-lockup .hb-brand { --hb-logo-h: 36px; }
  .hob-lock-name { font-size: var(--t-hero); }
  /* The three lines stay at body size. Shrinking them alongside the lede put
     them a single pixel below it, which is not a hierarchy — it is two sizes
     that look like one mistake. Only the closing line steps down. */
  .hob-foot { font-size: var(--t-small); }
  .hob-acts-end { flex-direction: column-reverse; align-items: stretch; }
  .hob-acts-end .hob-go, .hob-acts-end .hob-later { width: 100%; }
}

/* The sequence is the welcome; with motion turned off every piece is simply
   there, at rest, which is the frame the animation was arriving at anyway. */
@media (prefers-reduced-motion: reduce) {
  .hob-back, .hob-panel, .hob-lockup, .hob-hi, .hob-lede,
  .hob-can, .hob-foot, .hob-acts, .hob-tick {
    animation: none;
  }
  .hob-bar-fill, .hob-go { transition: none; }
}
