/* THE SPACING AND TYPE SCALES. Every rem margin, padding and gap in these
   stylesheets is a step of the spacing scale, and every font size a step of
   the type scale; spec/stylesheets/scales_spec.rb fails on any that is not.
   A length off its scale says beside it why its exact number matters, and
   is named in that spec's exceptions.

   Spacing: every 0.1rem to 1rem, then 1.25, 1.5, 1.75, 2, 2.5 and 3rem. A
   2px grid at the 20px root where nearly all the spacing here lives, and
   where it had drifted onto every pixel in between; coarser above, where
   2px no longer reads.

   Type: every 0.05rem from 0.6rem to 1rem, then 1.1, 1.2, 1.25, 1.3, 1.4,
   1.5 and 1.6rem. Every whole pixel from 12px to 20px, because body copy
   here uses most of them, then a few display sizes.

   Both are the values these stylesheets had already settled on, with the
   near-misses folded into a neighbour (.scratch/css-design-tokens/issues/01,
   02); a value exactly between two steps took the larger one, except where
   the rule says otherwise. Literals rather than custom properties, because
   a property named for its value says nothing a literal doesn't, and the
   spec is what keeps them on the scale. */
:root {
  --color-bg: #f8f6f2;
  --color-surface: #ffffff;
  --color-border: #e6e0d6;
  --color-text: #241f1a;
  --color-text-muted: #6e6459;
  --color-accent: #b5652b;
  --color-accent-dark: #96501f;
  --color-accent-bg: #fdead2;
  --color-on-accent: #fff;
  --color-pass: #157347;
  --color-pass-bg: #d9f2e5;
  --color-fail: #b42318;
  --color-fail-dark: #8f1c13;
  --color-fail-bg: #fde3e1;
  --color-degraded: #92600a;
  --color-degraded-bg: #fdf0d3;
  --color-unknown: #4b5563;
  --color-unknown-bg: #e8eaee;
  /* What only Operators see: the Console's Operator-only rows and, while one
     impersonates, the App's strip (operator console spec call C1). */
  --color-operator: #3b3f8f;
  --color-operator-bg: #eceefb;
  --color-on-operator: #fff;
  --radius: 12px;
  --radius-lg: 14px;
  --radius-sm: 8px;
  --control-place-size: 2.5rem;
  --control-tool-size: 2rem;
  --control-brand-size: 2.75rem;
  --app-header-height: 4.3rem;
  /* The row the impersonation strip takes above the page, which everything
     pinned to the top of the window sits below; impersonation.css gives it
     a height only while the strip is drawn and open. A length, not a bare
     0, so it can stand in a calc(). */
  --impersonation-strip-height: 0px;
  --shadow: 0 1px 3px rgba(60, 40, 20, 0.1);
  --color-warm-surface: linear-gradient(180deg, #fff8f0, #ffffff);
  --color-warm-border: #f0ddc8;
  --color-warm-border-strong: #e2c29c;
  --color-warm-shadow: 0 10px 24px -16px rgba(180, 120, 60, 0.35);
  --color-warm-heading: #4a3221;
  --color-warm-muted: #8a6f57;
  --color-warm-accent: #b5652b;
  --color-warm-accent-bg: #fdead2;
  --card-surface: var(--color-warm-surface);
  --card-border: var(--color-warm-border);
  --card-radius: 18px;
  --card-padding: 1.25rem;
  --card-shadow: 0 12px 30px -12px rgba(180, 120, 60, 0.25);
  /* The one focus ring (the global :focus-visible below), for the few rules
     that have to draw it on something other than the focused element. */
  --focus-ring: 2px solid var(--color-accent);
  --focus-ring-offset: 2px;
  /* Motion's two curves. --ease-out settles something arriving: a row, a
     drawer, a menu. --ease-spring overshoots a touch, for a mark or a count
     changing under the reader. */
  --ease-out: cubic-bezier(0.2, 0.7, 0.2, 1);
  --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
  /* A press's entry in a transition list: the spring back on release. Held,
     the press rule below swaps it for the quick dip. */
  --press: scale 0.28s var(--ease-spring);
}

* {
  box-sizing: border-box;
}

/* A device asking for reduced motion gets none, on every page
   (accessibility.md B-06). One rule for every animation and transition in
   every stylesheet, rather than one per site, so a new one is covered by
   default.

   Zero duration rather than `animation: none`, because an animation's end
   still has to happen: a `forwards` fill is what makes the Analysis's rows
   and progress bar visible at all (they start at opacity 0 and width 0), and
   sidebar_controller.js drops the sink-out class on `animationend`, which a
   zero-length animation still fires and a removed one never would. The
   delays go too, or a staggered row would arrive late in one jump.

   A spinner with nothing to turn is a frozen part-ring, which says nothing;
   icons.css closes it into a full ring. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0s !important;
    animation-delay: 0s !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0s !important;
    transition-delay: 0s !important;
  }
}

/* A press is felt (prototype/site-micro-interactions): a control dips while
   it is held and springs back on release. The small controls only, the
   shell's included: a row is pressable too, but a whole row shrinking, with
   its open drawer still under it, reads as the page moving rather than a
   button answering.

   A transition is one list per element, so this list sits in :where() and a
   control that states its own transitions adds var(--press) to them
   (header.css, sidebar.css) rather than losing them to this one. Entering
   :active takes :active's entry, the 60ms dip; leaving it takes the rest
   state's, the spring.

   Only where motion is welcome: the global reduced-motion rule zeroes the
   dip's duration, and a control that snaps smaller is still motion. */
:where(
  .button,
  .resource-tab,
  .row-edit,
  .popover-menu-trigger,
  .popover-menu-item,
  .header-language-menu-trigger,
  .header-notifications-link,
  .app-sidebar .nav .link,
  .app-sidebar .avatar-trigger,
  .app-sidebar-float-toggle
) {
  transition: var(--press);

  @media (prefers-reduced-motion: no-preference) {
    &:active {
      --press: scale 0.06s ease-out;
      scale: 0.95;
    }
  }
}

/* PENDING. From press until the server answers, the button or menu item
   that sent the form holds a spinner in place of its words, then a brief
   tick once the press has worked (prototype/site-micro-interactions). The
   spinner is Turbo's own state: for as long as the request is out it marks
   the form aria-busy and disables the control the form was sent with. The
   tick is submit_pending.js's data-submitted, since nothing else says a
   press has just worked. The words go by their fill, not their colour, so
   the marks below still draw in the control's own currentColor: white on a
   primary button, ink on the rest.

   An <input type=submit> draws no pseudo-elements, so it only dims. */
:is(.button, .popover-menu-item) {
  &[data-submitted] {
    pointer-events: none;
  }

  &:not(input) {
    /* A dialog's Send sits in its .footer, outside the form it sends. */
    :is(form[aria-busy="true"], .house-dialog:has(form[aria-busy="true"]) .footer) &:disabled {
      position: relative;
      -webkit-text-fill-color: transparent;

      &::after {
        content: "";
        position: absolute;
        inset: 0;
        width: 1em;
        height: 1em;
        margin: auto;
        border: 2px solid;
        border-right-color: transparent;
        border-radius: 50%;
        animation: button-busy 0.7s linear infinite;

        /* A spinner that cannot turn closes into a full ring, as icons.css's does. */
        @media (prefers-reduced-motion: reduce) {
          border-right-color: currentColor;
        }
      }
    }

    &[data-submitted] {
      position: relative;
      -webkit-text-fill-color: transparent;
    }

    &[data-submitted]::after {
      content: "";
      position: absolute;
      inset: 0;
      width: 0.45em;
      height: 0.85em;
      margin: auto;
      border: solid;
      border-width: 0 2px 2px 0;
      animation: button-submitted 0.3s var(--ease-spring) both;
    }
  }

  form[aria-busy="true"] &:is(input):disabled {
    opacity: 0.6;
  }
}

@keyframes button-busy {
  to {
    transform: rotate(360deg);
  }
}

@keyframes button-submitted {
  from {
    opacity: 0;
    transform: rotate(45deg) scale(0.3);
  }

  to {
    opacity: 1;
    transform: rotate(45deg) scale(1);
  }
}

html {
  /* Base font size at 125% of the 16px browser default (20px) on a phone
     and from 1537px up; the steps below set 16px and 18px between. Every
     font-size/margin/padding in this stylesheet is in rem, so this scales
     the whole UI proportionally, same as a browser zoom.

     Careful: rem means two different things in this file. In a normal
     declaration 1rem is the root's 16, 18 or 20px. Inside a media query the
     spec resolves rem against the *initial* font size -- the browser
     default, 16px -- since a query may not depend on the results of
     declarations. The same `64rem` is therefore 1280px on .app-main but
     1024px in an @media. Every @media below is annotated with the px
     width it fires at and the layout that sets it; keep those honest.
     rem rather than px in the queries is deliberate: both sides then
     scale with the reader's default font size, so a larger default
     reflows sooner instead of only setting bigger type. */
  font-size: 125%;
  /* Anything scrolled to -- a fragment link, a focused control off-screen --
     would otherwise land under the pinned .app-header. Retuned in the 40rem
     block below, where the pinned thing is the shorter mobile bar. */
  scroll-padding-top: calc(var(--app-header-height) + var(--impersonation-strip-height));
}

/* The root steps down below 1537px so screens built for 1200px of content
   still fit a 1280px window (a 1080p laptop at 150% display scale) with the
   rail expanded. Two steps rather than a fluid size, and the 100% step from
   641px rather than 1025px, so narrowing a window never makes text bigger
   (wide-viewport-layout ticket 03). The phone layout under 641px keeps 125%.
   The queries are in rem, so they do not move with the root they set.

   Range syntax, not min-width 40.0625rem against max-width 40rem: browser
   zoom and display scaling give fractional widths, and a window 640.75px wide
   (Firefox at 110%) fell between the two and took the 125% root. Each step
   here starts exactly where the one below it ends. */

/* 641-1280px */
@media (40rem < width <= 80rem) {
  html {
    font-size: 100%;
  }
}

/* 1281-1536px */
@media (80rem < width <= 96rem) {
  html {
    font-size: 112.5%;
  }
}

body {
  margin: 0;
  font-family:
    -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial,
    sans-serif;
  background: var(--color-bg);
  color: var(--color-text);
  line-height: 1.5;
}

a {
  color: var(--color-accent);
}

/* One focus ring for anything a keyboard reaches, so a text link no longer
   falls back to the platform's blue. A field adds only its accent border
   (Forms below). A rule restates the ring only where it is drawn on
   something other than the focused element (a wrapper), and then by
   --focus-ring; one that would be clipped outside its box moves
   only the offset, inward. A control that shows focus with a fill instead
   says so in its own rule. */
:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-ring-offset);
}

/* Softer round a link, whose ring sits on text already in the accent. A
   link drawn as a button keeps the controls' full ring. */
a:not(.button):focus-visible {
  outline-color: color-mix(in srgb, var(--color-accent) 80%, transparent);
}

h1 {
  font-size: 1.6rem;
  margin: 0 0 0.3rem;
}

h2 {
  font-size: 1.1rem;
  margin: 0 0 0.5rem;
}

:first-child {
  margin-top: 0 !important;
}

:last-child {
  margin-bottom: 0 !important;
}

/* Text for screen readers only -- a column header for a cell whose content
   is an unlabelled control. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* --- Card surface ---
   Every content container in the platform gets its card look from this one
   rule, not from a per-page rule -- the element carries `card` beside its
   purpose class (`card empty-state`) rather than re-declaring
   background/border/radius/shadow/padding in a component block. Components
   below still own everything that is *theirs* (a padding other than this
   one, width, a tone-coloured edge, squared-off corners
   where they abut something); those overrides win because they come later
   in the file. */

.card {
  padding: var(--card-padding);
  background: var(--color-surface);
  border: 1px solid var(--card-border);
  border-radius: var(--card-radius);
  box-shadow: var(--card-shadow);
}

/* No cards on cards. A card already sitting on a card's surface is not a
   card -- it is a section of the one it sits in -- so it drops the whole
   treatment (padding included: padding holds content off an edge, and it
   no longer has one) and lays out as plain content in its parent.
   Structural rather than a class the views opt into, because the same
   markup legitimately renders in both contexts -- a card inside a
   .technical-details disclosure, and an identical one sitting straight on
   the page. A part that is only ever a section of its card (the action
   card's `.panel`s) leaves `card` off instead. */

.card .card {
  background: none;
  border: none;
  border-radius: 0;
  box-shadow: none;
  padding: 0;
}

/* --- A note on a card ---
   An aside about the card as a whole, inset a step darker than the card's
   surface, with an optional icon that says what it is about before the words
   do. A line that belongs to one element (a field, a row) is a .note under
   that element instead, so the box stays reserved for the whole card.

   Chosen in a UI prototype of the check-email page (2026-11-07) over a
   muted line under the button, a band across the card's foot, a line
   above the button with an accent edge, and a line under the card. */
.card-note {
  display: flex;
  gap: 0.7rem;
  align-items: center;
  padding: 0.8rem 0.9rem;
  background: var(--color-bg);
  border-radius: var(--radius-sm);
  color: var(--color-text-muted);
  font-size: 0.9rem;
  line-height: 1.5;

  svg {
    flex: none;
  }

  p {
    margin: 0;
  }
}

/* A page's sections stacked, each its own card. */
section.card + section.card {
  margin-top: 1.25rem;
}

/* The small uppercase label over a field, a column or a group of things:
   one look wherever it is drawn. --large titles a section or a card, so it
   reads a step above the labels inside; --tone takes the colour of the
   status it names, from the --tone a .status-tone--* class sets on the
   label or an ancestor. A
   screen's own class on the label says only where it sits. */
.eyebrow {
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-text-muted);

  &.eyebrow--large {
    font-size: 0.8rem;
  }

  &.eyebrow--tone {
    color: var(--tone);
  }
}

/* A .section is a title and the card it names, and the title's margin spaces
   it from whatever comes before. !important because the title leads the
   section, so the global :first-child reset would zero it. */
.section h2 {
  margin: 1.75rem 0 0.6rem !important;
}

/* --- The house dialog ---
   Every modal. A white card with no border, a neutral shadow, rising in over
   a tinted, slightly blurred page. A dialog's own file sets --dialog-width and
   styles what only it draws; the parts are .header, .body and .footer, and a
   .multi-panel holds those parts in side-by-side .panel elements.

   What closes one: a button in a <form method="dialog">, Escape, and with
   data-closes-on-backdrop a click on the backdrop (house_dialog.js). */
.house-dialog {
  --dialog-width: 32rem;
  --dialog-pad: 1.5rem;
  width: min(var(--dialog-width), calc(100vw - 2rem));
  max-height: calc(100dvh - 2rem);
  padding: 0;
  overflow: hidden;
  border: none;
  border-radius: var(--card-radius);
  background: var(--color-surface);
  box-shadow: 0 24px 60px -20px rgb(0 0 0 / 0.45);
  /* A modal centres on its auto margins, which `:last-child { margin-bottom:
     0 !important }` above cancels: Share's is the last thing in its card,
     the confirm the last in the body. */
  margin: auto !important;
  color: var(--color-text);
  font-size: 1rem;

  /* 640px: a phone. */
  @media (max-width: 40rem) {
    --dialog-pad: 1.25rem;
  }

  /* A dialog with tabindex="-1" takes focus itself, so showModal() doesn't
     ring its first control before the User has done anything. */
  &:focus {
    outline: none;
  }

  /* A column, so a tall .body scrolls and the .footer stays in reach. */
  &[open] {
    display: flex;
    flex-direction: column;
    animation: house-dialog-arrive 0.3s cubic-bezier(0.2, 0.9, 0.3, 1.2) both;
  }

  &::backdrop {
    background: rgb(36 31 26 / 0.4);
    backdrop-filter: blur(2px);
  }

  .header {
    flex: none;
    padding: var(--dialog-pad) var(--dialog-pad) 0;

    /* The dialog's title. An h2 drawn as an .eyebrow is a label, not the
       title, and keeps its own size. :where() so a dialog's own title rule
       (.onboarding-finish .header h2) still outranks this one. */
    h2:not(:where(.eyebrow)) {
      margin: 0 0 0.4rem;
      font-size: 1.25rem;
    }
  }

  .subtext {
    margin: 0;
    color: var(--color-text-muted);
    line-height: 1.5;
  }

  .body {
    flex: 1 1 auto;
    min-height: 0;
    padding: 1rem var(--dialog-pad);
    overflow: auto;

    &:first-child {
      padding-top: var(--dialog-pad);
    }
  }

  /* Its buttons end on the right, the primary last, as macOS lays out a
     dialog's foot. */
  .footer {
    flex: none;
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    align-items: center;
    gap: 0.5rem;
    padding: 0.9rem var(--dialog-pad);
    background: var(--color-surface);

    &.bordered {
      position: relative;

      &::before {
        content: "";
        position: absolute;
        top: 0;
        inset-inline: calc(0.5 * var(--dialog-pad));
        border-top: 1px solid var(--color-border);
      }
    }
  }

  /* The glyph, muted until hovered, in the dialog's own corner whichever part
     holds it: a dialog's box is its containing block. */
  .dialog-close {
    position: absolute;
    top: 0.6rem;
    right: 0.8rem;
    padding: 0;
    border: none;
    background: none;
    font-size: 1.4rem;
    line-height: 1;
    color: var(--color-text-muted);
    cursor: pointer;

    &:hover {
      color: var(--color-text);
    }
  }

  /* A field's own refusal, under it, is .refusal below .form-field--error. */

  /* A notice over a dialog's form: a tinted box, whose tint says what it is.
     This one refuses the whole send; a dialog with another kind adds its own
     tint, as Feedback's draft note does. */
  :is(.refusal--form, .restored) {
    margin: 0 0 0.9rem;
    padding: 0.6rem 0.8rem;
    border-radius: var(--radius-sm);
    font-size: 0.9rem;
    line-height: 1.45;
  }

  .refusal--form {
    background: var(--color-fail-bg);
  }

  /* Side-by-side panels that each scroll on their own, the first the work and
     the second what it refers to. Never styled apart from .house-dialog. */
  &.multi-panel {
    &[open] {
      display: grid;
      grid-template-columns: minmax(0, 1fr) minmax(0, 1.3fr);
      grid-template-rows: minmax(0, 1fr);
    }

    > .panel {
      display: flex;
      flex-direction: column;
      min-height: 0;
    }

    /* The × belongs to the dialog, not to either panel. It follows them in
       the markup so the form keeps the first focus. */
    > form:has(> .dialog-close) {
      display: contents;
    }

    /* 768px. The panels stack and scroll together. The one holding the
       .footer dissolves, so its foot can go last, after the other panel, and
       stick to the bottom of the stack as it scrolls; the × comes first and
       sticks to the top. A column, not a grid: a sticky item is held to its
       containing block, and by the spec a grid item's is its own row, though
       Chrome lets it out. */
    @media (max-width: 48rem) {
      &[open] {
        display: flex;
        overflow: auto;
      }

      > .panel {
        flex: none;
      }

      > .panel:has(> .footer) {
        display: contents;
      }

      > .panel > .body {
        flex: none;
        overflow: visible;
      }

      > .panel > .footer {
        order: 1;
        position: sticky;
        bottom: 0;
      }

      > form:has(> .dialog-close) {
        display: block;
        order: -1;
        position: sticky;
        top: 0;
        z-index: 1;
        flex: none;
        height: 0;
      }
    }
  }
}

@keyframes house-dialog-arrive {
  from {
    opacity: 0;
    transform: translateY(8px) scale(0.97);
  }
}

.section-tab {
  color: var(--color-text-muted);
  text-decoration: none;
}

.section-tab:hover {
  color: var(--color-text);
}

/* --- App shell: header + collapsible sidebar, every logged-in page ---
   Warm palette from the parallel-signup-flow-map branch's signup shell
   (unmerged as of this change) -- see --color-warm-* in :root above. */

.app-shell {
  display: flex;
  min-height: calc(100vh - var(--impersonation-strip-height));
}

/* ReadyNext's name, one type wherever a shell draws it: the sidebar's
   header (sidebar.css) and the centered shell's (centered_shell.css), each
   of which adds only where it sits. Flat, as the centered shell draws the
   page shown to a browser too old for nested CSS. public/500.html copies
   it in px. */
.brand {
  color: var(--color-warm-heading);
  font-weight: 700;
  font-size: 1.2rem;
}

.app-body {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-width: 0;
}

.app-main {
  flex: 1;
  max-width: 64rem;
  width: 100%;
  padding: 1.75rem 2rem 3rem;
}

.section-tabs {
  display: flex;
  gap: 1rem;
  margin-bottom: 1.5rem;
}

/* --- Account users (account/users/index + _user) ---
   Inviting is what the page is for -- most Accounts hold ten Users or fewer
   and come here to add one -- so the composer is one always-open line at the
   top and the roster below it is quiet.

   Folded in from the /account/users UI prototype (2026-09-05,
   .claude/skills/prototype/UI.md) -- won out over three alternatives and
   today's page; the full set stays on the throwaway branch
   prototype/account-users-page, and the reasoning is in
   .scratch/user-permission-levels/issues/03-account-users-page-redesign.md. */

.user-invite-form {
  display: flex;
  gap: 0.6rem;
  align-items: center;
  flex-wrap: wrap;

  .form-field {
    flex: 1 1 16rem;
    min-width: 0;
    margin-bottom: 0;
  }
}

/* The right-hand end of every row is the same three slots in the same order
   -- resend, Role, remove -- rendered whether or not the row can fill them,
   so the columns run straight down both sections and past rows nobody may
   act on. A row where .row-right is a column, so both classes: .row-right
   comes later in this file. */
.row-right.user-row-controls {
  flex-direction: row;
  align-items: center;
  gap: 0.2rem;
}

.user-row-slot {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: var(--control-tool-size);
  height: var(--control-tool-size);
}

/* button_to wraps each control in a form; it shouldn't add a box of its own. */
.user-row-slot form {
  display: contents;
}

/* Fixed width, not content width: sizing to the label would shift the column
   the moment a Role changed.

   width, not flex-basis: .row-right is flex: none, so it sizes itself from
   its children's max-content. Chrome floors that at an unshrinkable item's
   flex-basis; Firefox takes the select's own (narrower) content instead, then
   lays the form out at the full basis, and the remove button overflowed the
   row. A width counts in both engines. */
.user-row-role {
  flex: none;
  width: 7.5rem;
}

/* Reads as text until you reach for it, so the roster stays quiet while the
   Role is still directly editable. Keeps the app-wide select's chevron and
   its spacing -- only the border and surface drop away until hover. */
.user-row-role select {
  width: 100%;
  padding: 0.3rem 2rem 0.3rem 0.5rem;
  border-color: transparent;
  border-radius: var(--radius-sm);
  background-color: transparent;
  font-size: 0.85rem;
  color: var(--color-text-muted);
  cursor: pointer;
}

.user-row-role select:hover,
.user-row-role select:focus {
  border-color: var(--color-border);
  background-color: var(--color-surface);
  color: var(--color-text);
}

/* Static Role renders the same box as the select -- same width, same padding
   -- so your own row's label sits exactly where every dropdown above and
   below it does, with the chevron the only difference. */
.user-row-role--static {
  padding: 0.3rem 2rem 0.3rem 0.5rem;
  border: 1px solid transparent;
  font-size: 0.85rem;
  color: var(--color-text-muted);
  /* .row-right's own would carry the words to the far side of this box; a
     select's stay at the left whatever it inherits. */
  text-align: left;
}

/* The shell's tool tier (.header-notifications-link et al): a transparent
   square that washes to the accent pill on hover. Copied rather than shared
   because those selectors are scoped to .app-header. */
.user-row-action {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--control-tool-size);
  height: var(--control-tool-size);
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-warm-muted);
  cursor: pointer;
  transition:
    background 0.12s ease,
    color 0.12s ease;
}

.user-row-action:hover {
  background: var(--color-warm-accent-bg);
  color: var(--color-warm-accent);
}

/* Destructive controls take the fail palette instead, so cancelling an
   invitation never reads as the same gesture as resending one. */
.user-row-action--danger:hover {
  background: var(--color-fail-bg);
  color: var(--color-fail);
}

.user-row-action svg {
  display: block;
}

/* --- Page header --- */

.page-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 1.25rem;
  flex-wrap: wrap;
}

/* --- A Discarded resource's page (discard.md B-18) ---
   A card above the page saying when it was set aside, with Restore, and the
   page beneath dimmed: it is what the resource last showed, and nothing on it
   can be pressed (`inert`, set by the layout). Folded in from the Dashboard
   restore UI prototype (2026-11-09, .claude/skills/prototype/UI.md), whose
   variant A won out over a tab, a page of its own and the Add menu; the
   reasoning is in .scratch/row-bar-controls/issues/12. Tried there and
   rejected: a row's pill and buttons on one line, which Site#show's
   Unmonitored column has no room for. */
.discarded-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  margin-bottom: 1.5rem;

  p {
    margin: 0;
  }
}

.discarded-banner-title {
  font-weight: 600;
}

.discarded-page {
  opacity: 0.6;
}

/* A line under a page's h1, in place of a .page-header's own margin. */
.page-intro {
  color: var(--color-text-muted);
  margin: 0 0 1.25rem;
}

.breadcrumb {
  margin: 0 0 0.8rem;
  font-size: 0.9rem;
}

.breadcrumb a {
  color: var(--color-text-muted);
  text-decoration: none;
}

.breadcrumb a:hover {
  color: var(--color-text);
}

/* --- Status pills --- */

.status-pill {
  display: inline-block;
  padding: 0.2rem 0.7rem;
  border-radius: 999px;
  background: var(--tone-bg);
  color: var(--tone);
  font-size: 0.85rem;
  font-weight: 600;
  white-space: nowrap;
  /* A whole-pixel line box. The body's 1.5 makes it 25.5px here, and that
     half pixel lands everything stacked below a pill -- a source row's Edit
     caret, the next row's top -- off the pixel grid, where a 1x screen draws
     a thin stroke as two half-tone rows and the caret looks cut. */
  line-height: 1.25rem;
}

/* A live update changed its word or its tone (live_update_shows_change.js):
   it pops once, ringed in its new tone. */
.status-pill[data-changed] {
  animation: status-pill-changed 0.6s var(--ease-out);
}

@keyframes status-pill-changed {
  35% {
    transform: scale(1.18);
    box-shadow: 0 0 0 4px var(--tone-bg);
  }
}

/* --- Status tone tokens: shared by status pills, the resource summary
   tiles, and resource rows --- */

/* The field drawer's tones are these (address_field.css). */
.status-pill--pass,
.status-tone--pass,
.field-drawer--ok {
  --tone: var(--color-pass);
  --tone-bg: var(--color-pass-bg);
}

.status-pill--fail,
.status-tone--fail,
.field-drawer--stop {
  --tone: var(--color-fail);
  --tone-bg: var(--color-fail-bg);
}

.status-pill--degraded,
.status-tone--degraded,
.field-drawer--ask {
  --tone: var(--color-degraded);
  --tone-bg: var(--color-degraded-bg);
}

/* The tick in a field whose value is Confirmed (shared/_confirmed_tick):
   the User Number's and the address a User logs in with. Each field places
   it (user_number.css, user_email.css). */
.confirmed-tick {
  display: inline-flex;
  color: var(--color-pass);
}

/* Reuses the neutral "unknown" palette: Paused isn't a status severity, just
   an absence of active watching, same visual weight as "not yet known". So is
   `info` -- no verdict to be had -- which is why concern_icon already hands it
   the "unknown" glyph; this is the same fold, for the tone. */
.status-pill--unknown,
.status-pill--info,
.status-pill--paused,
.status-tone--unknown,
.status-tone--info,
.status-tone--paused {
  --tone: var(--color-unknown);
  --tone-bg: var(--color-unknown-bg);
}

/* A Scanner-found candidate awaiting Confirm/discard (CONTEXT.md's
   Confirmed) -- distinct from every health-status tone above, since it's
   not reporting a Monitor's status at all. */
.status-tone--unconfirmed {
  --tone: var(--color-accent);
  --tone-bg: var(--color-accent-bg);
}

/* Seen only by Operators: no status at all, but who may read it. */
.status-tone--operator {
  --tone: var(--color-operator);
  --tone-bg: var(--color-operator-bg);
}

/* --- Dashboard: resource summary tiles --- */

.resource-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  /* The step below 0.75rem, not the one above: the uptime report's
     two-abreast tile has under a pixel to spare for "1 day 23 h 55 min",
     and the larger step wraps it. uptime_report.css's four-abreast
     container query is sized from this gap. */
  gap: 0.7rem;
  margin-bottom: 1.5rem;
}

.resource-stat {
  background: var(--tone-bg);
  border-radius: var(--radius);
  padding: 0.9rem 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}

.resource-stat-number {
  font-size: 1.6rem;
  font-weight: 700;
  color: var(--tone);
}

.resource-stat-label {
  font-size: 0.8rem;
  color: var(--tone);
}

/* --- A row that opens a page: the Dashboard's, and a Phone's on Site#show ---
   The whole row opens the resource, as another row opens its drawer, so it
   carries .row--opens and takes that row's cursor, hover and press.
   What opens it is the name's own link, stretched over .row-top: a
   real link, so a new tab or a middle click works anywhere on the row, and
   the row's controls sit above it, which an <a> around them could not hold.
   No underline on hover, since the tint already says it. Not a row with a
   drawer: a Scan's row has a link in its name, and the row opens the drawer. */

.row--opens:not(:has(> .row-drawer)) > .row-top:has(.row-name a) {
  position: relative;

  .row-name a {
    color: var(--color-text);
    text-decoration: none;

    &::after {
      content: "";
      position: absolute;
      inset: 0;
      border-radius: inherit;
    }
  }

  /* An open menu hangs over the rows below, whose controls sit at the same
     level and come later. */
  .row-acts {
    position: relative;
    z-index: 1;

    &:has([open]) {
      z-index: 2;
    }
  }
}

/* With no drawer there is no Details to say the row opens, so a caret does,
   right of its pill and buttons, a row still asking for Confirm included
   (consistency.md C16 — A row that opens a page ends in a caret). A report's
   row draws its own. */
.row--opens:not(:has(> .row-drawer)) > .row-top:not(a)::after {
  content: "\203A";
  align-self: center;
  color: var(--color-text-muted);
  font-size: 1.2rem;
  line-height: 1;
  flex-shrink: 0;
}

/* align-self: .row--opens stretches the row's columns, and a
   stretched kebab would drop its menu from the row's bottom edge. */
.row-top > .row-acts {
  align-self: flex-start;
  flex-shrink: 0;
}

/* The page-header's "Add a resource" dropdown reuses .popover-menu's
   floating-panel behavior, but its trigger is a normal primary button
   rather than the kebab icon -- just needs its own marker hidden too. */
.page-header-add-menu summary {
  list-style: none;
  cursor: pointer;
}

.page-header-add-menu summary::-webkit-details-marker {
  display: none;
}

.page-header-add-menu .popover-menu-items {
  min-width: 12rem;
}

/* --- Empty state --- */

.empty-state {
  text-align: center;

  h2 {
    margin-bottom: 0.4rem;
  }

  p {
    color: var(--color-text-muted);
    max-width: 32rem;
    margin: 0 auto 1.25rem;
  }
}

/* --- Detail page panels --- */

.panel-empty {
  color: var(--color-text-muted);
  margin: 0;
}

/* --- The area strip (histories/show/_filters) ---
   The History page's own underlined strip. It wears .resource-tabs and
   .resource-tab, the classes of a Monitored Resource page's tabs (under
   "Resource page" below), so every rule here is keyed to the History page's
   classes as well, and puts back what those rules would otherwise bring in:
   the tab card's layout (it wears no `card`), and the pill a tab and its
   selected state are drawn as. */

.resource-tabs.history-areas {
  display: block;
  margin: 1rem 0;
  padding: 0;
}

.resource-tabs-list.history-area-list {
  display: flex;
  gap: 0.3rem;
  /* Card border, not the plain one: this rule *is* the top edge of the
     panel card butted up underneath it. */
  border-bottom: 1px solid var(--card-border);
  /* Six areas plus Everything already fill the strip; scroll rather than
     clip if the vocabulary grows. */
  overflow-x: auto;
  scrollbar-width: thin;
}

/* Links, each with its count beside it. */
.resource-tab.history-area-tab {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  text-decoration: none;
  white-space: nowrap;
  appearance: none;
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  border-radius: 0;
  padding: 0.6rem 0.9rem;
  font: inherit;
  font-weight: 600;
  color: var(--color-text-muted);
  cursor: pointer;

  &:hover {
    color: var(--color-text);
  }

  &[aria-current="page"] {
    color: var(--color-accent);
    border-bottom-color: var(--color-accent);
    background: none;
    box-shadow: none;
  }
}



/* --- Snooze (shared/_snooze_menu) ---
   Both levels of the control -- a resource's header and a row's drawer --
   reuse .popover-menu's floating panel; only the trigger differs, so
   only the trigger is restated here. */

.snooze-menu summary {
  list-style: none;
  cursor: pointer;

  &::-webkit-details-marker {
    display: none;
  }
}

/* The header's trigger is a normal .button. */
.snooze-menu-trigger {
  display: inline-flex;
  align-items: center;
}

/* The sentence that keeps a row's Snooze from reading as the whole
   resource's. Under the presets and ruled off from them: it is not a sixth
   thing to press. */
.snooze-menu-note {
  margin: 0.3rem 0.6rem;
  padding-top: 0.4rem;
  border-top: 1px solid var(--color-border);
  color: var(--color-text-muted);
  font-size: 0.75rem;
  line-height: 1.4;
}

/* --- Share (shares/_notice, shares/_menu) ---
   What was just shared, over the control it was shared from (shares/
   _notice): over a card's band, or over a Scan's header. */
.share-notice {
  margin: 0 0 0.8rem;
  padding: 0.6rem 0.8rem;
  border-radius: var(--radius-sm);
  background: var(--color-pass-bg);
  color: var(--color-pass);
  font-size: 0.9rem;
}

/* Share's trigger (shares/_menu), as the Action Card band's link or as a
   page-header .button: its mark leads the word either way, and each place
   adds only its own look (share.css draws the band's). Keyed on the
   element, which outranks the .button that button.css draws after this
   file, so the flex here wins over its inline-block. */
summary.share-trigger {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  list-style: none;
  cursor: pointer;

  &::-webkit-details-marker {
    display: none;
  }
}

/* --- Resource scans (shared/_resource_scans) ---
   A resource's chronological Scan history. */

.resource-scans-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

.resource-scans-row {
  padding: 0.5rem 0;
  border-bottom: 1px solid var(--color-border);
  font-size: 0.9rem;
}

.resource-scans-row:last-child {
  border-bottom: none;
}

.resource-scans-row-when {
  color: var(--color-text-muted);
  font-size: 0.8rem;
  margin-bottom: 0.3rem;
}

.resource-scans-row-summary {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}

/* The field table's own label, carrying the count and the date it was read.
   The browser default is centered and sits above the table's top border; this
   puts it where a caption on a data table belongs -- left, muted, on the
   table's top edge as its label rather than as a heading. */
.checks-table-caption {
  caption-side: top;
  padding: 0 0 0.5rem;
  color: var(--color-text-muted);
  font-size: 0.8rem;
  text-align: left;
}

/* The Submission tab's three sections: can we send it, when did we last,
   what do we expect. Separated by the same hairline the panels' own rows
   use, because they are three answers about one subject rather than three
   panels. */
.form-submission-section + .form-submission-section {
  margin-top: 1.5rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--color-border);
}

.form-submission-section > .resource-section-heading {
  margin: 0 0 0.6rem;
}

/* The answer sits where its empty state (.panel-empty) does. */
.form-submission-section > .resource-section-heading + p {
  margin: 0;
}

/* --- Technical details --- */

.technical-details summary {
  cursor: pointer;
  font-weight: 600;
  color: var(--color-text-muted);
}

.technical-details[open] summary {
  margin-bottom: 0.8rem;
}

.technical-details {
  overflow-x: auto;
}

.checks-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.85rem;
}

.checks-table th {
  text-align: left;
  color: var(--color-text-muted);
  font-weight: 600;
  padding: 0.4rem 0.8rem 0.4rem 0;
  border-bottom: 1px solid var(--color-border);
}

.checks-table td {
  padding: 0.4rem 0.8rem 0.4rem 0;
  border-bottom: 1px solid var(--color-border);
  vertical-align: top;
}

.checks-table tr:last-child td {
  border-bottom: none;
}

.checks-table-exact {
  display: block;
  color: var(--color-text-muted);
  font-size: 0.8rem;
}

.checks-table-error {
  display: block;
  color: var(--color-fail);
}

.dns-record-list {
  list-style: none;
  margin: 0.3rem 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}

.dns-record-list li {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.4rem;
}

.dns-record-name {
  font-weight: 600;
  min-width: 3.25rem;
}

.dns-record-status {
  color: var(--color-text-muted);
  font-size: 0.8rem;
}

.dns-record-status--problem {
  color: var(--color-fail);
  font-size: 0.8rem;
}

/* --- Detail actions --- */

/* --- Forms --- */

/* A form's hidden inputs come first, so the paragraph that opens what the
   reader sees is not the :first-child the global reset zeroes. */
.card > input[type="hidden"] + p {
  margin-top: 0;
}

/* A card's headed sections spaced apart, so one's closing line never reads as
   the caption of the next one's heading. Onboarding's sections head with
   .resource-section-heading and space themselves (onboarding_email.css). */
.card > section + section:has(> h3) {
  margin-top: 2rem;
}

.billing-plan-name,
.billing-plan-price {
  font-size: 1.1rem;
  font-weight: 600;
}

/* First-time variation's plan picker: each plan is a radio wrapped in a
   selectable card, highlighted once picked. */
.billing-plan-options {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: 0.8rem;
  margin-top: 1rem;
}

.billing-plan-option {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  padding: 1rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-surface);
  cursor: pointer;

  input {
    accent-color: var(--color-accent);
    margin: 0 0 0.3rem;
  }

  &:has(input:checked) {
    border-color: var(--color-accent);
    background: var(--color-accent-bg);
  }

  &:has(input:focus-visible) {
    outline: var(--focus-ring);
    outline-offset: var(--focus-ring-offset);
  }
}

.billing-card-form {
  margin-top: 1rem;
  max-width: 32rem;
}

.billing-card-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
  gap: 0 1rem;
}

/* The dashboard's inline add-resource form sits in its own full-width slot
   below the resource stats (not the header, so opening it doesn't push the
   header/stats around). Zero margin while empty (closed) so it takes no
   visual space on the page. */
turbo-frame#add-resource-form {
  display: block;
}

turbo-frame#add-resource-form:not(:empty) {
  margin-bottom: 1.75rem;
}

.form-field {
  margin-bottom: 1rem;
}

.form-field label {
  display: block;
  margin-bottom: 0.3rem;

  /* An .eyebrow label takes its weight from .eyebrow. :where() keeps this
     at the specificity a screen's own label weight is written against. */
  &:not(:where(.eyebrow)) {
    font-weight: 600;
  }
}

.form-field input[type="text"],
.form-field input[type="email"],
.form-field input[type="url"],
.form-field input[type="tel"],
.form-field input[type="number"],
.form-field textarea,
/* The history filter bar's search field sits in a row of its own controls
   rather than a stacked .form-field, so it takes this treatment on the
   element -- the same reason `select` below is styled on the element. */
input[type="search"] {
  width: 100%;
  padding: 0.5rem 0.7rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  font-size: 0.95rem;
  font-family: inherit;
}

/* A field in a tight panel, a row's settings or the More card, where the
   usual field would outweigh what it sits beside. Flush, with the gap as
   the only space between its parts, so no label brings its usual margin. */
.form-field--dense {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  margin-bottom: 0;

  label {
    margin-bottom: 0;
  }

  input[type="text"] {
    padding: 0.4rem 0.5rem;
    border-radius: var(--radius-sm);
    font-size: 0.9rem;
    line-height: inherit;
  }
}

/* Not above a control, so nothing else holds its fact off it. */
p.form-field-label {
  margin-bottom: 0.5rem;
}

/* Every <select> in the app, styled on the element rather than per form,
   so a dropdown looks the same wherever one is dropped in. The native
   caret sits hard against the field's right edge with no room to breathe
   and no way to move it, so the control drops the platform widget
   (appearance: none) and draws its own chevron with real space either
   side of it. That's why the field treatment lives here rather than in
   the .form-field input list above: appearance: none also strips the
   border and background the platform was drawing.

   The chevron is an inline SVG -- no asset request, no icon dependency --
   and has to hardcode its stroke colour, since a data: URI can't read
   --color-text-muted. Keep #6e6459 in step with that token. */
select {
  padding: 0.5rem 2rem 0.5rem 0.7rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  font-size: 0.95rem;
  font-family: inherit;
  appearance: none;
  -webkit-appearance: none;
  background-color: var(--color-surface);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1.25 1.75 6 6.25l4.75-4.5' fill='none' stroke='%236e6459' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.7rem center;
  background-size: 0.6rem;
}

/* appearance: none also drops the platform's greyed-out disabled state
   (the profile page's working-hours selects), so restate it. */
select:disabled {
  background-color: var(--color-bg);
  color: var(--color-text-muted);
}

/* The ring is the global :focus-visible's; a field adds its border. Not a
   .checkbox in a .form-field (All day), which takes the ring alone, nor a
   readonly one, which takes neither (input[readonly]:focus below). */
.form-field input:not(.checkbox, [readonly]):focus,
.form-field textarea:focus,
select:focus {
  border-color: var(--color-accent);
}

.form-field textarea {
  display: block;
  resize: vertical;
}

/* A field's refusal takes the focus ring's outline in red, focused or not,
   and its border turns red while focused, as the ring turns it accent. Its
   words sit tight under it, and the gap after them is wider, so they read
   as that field's and not the next one's. */
.form-field--error {
  margin-bottom: 1.25rem;

  /* A phone number's box is its shell, so the ring goes round that and not
     the borderless input inside it. A radio group is one answer, so the ring
     goes round the group and not each radio. */
  :is(input:not(.phone-field-number, [type="radio"]), select, textarea),
  .phone-field-shell,
  &[role="radiogroup"] {
    outline: var(--focus-ring);
    outline-color: var(--color-fail);
    outline-offset: var(--focus-ring-offset);
  }

  /* The group has no box of its own, so its ring takes its options' corners. */
  &[role="radiogroup"] {
    border-radius: var(--radius);
  }

  /* The :not() matches the accent border's selector, whose specificity this
     has to meet. */
  :is(input:not(.checkbox, [readonly]), select, textarea):focus {
    border-color: var(--color-fail);
  }
}

/* The words of a refusal, under its field: in a dialog's form, or on a page
   where the server has said why a field is still needed. The outline takes
   4px of the gap under a field. */
.refusal {
  margin: 0.4rem 0 0;
  color: var(--color-fail);
  font-size: 0.9rem;
  line-height: 1.45;
}

/* Account -> General's Business Details facts are readonly controls -- they
   can't be edited yet -- and nothing in here covered an inactive input, so
   this is select:disabled above restated for input[readonly], with the caret
   and focus ring dropped too. KEEP THE BORDER: a borderless washed-out box
   on this warm page reads as a missing field, not an inert one. */
input[readonly] {
  background-color: var(--color-bg);
  color: var(--color-text-muted);
  cursor: default;
  caret-color: transparent;
}

input[readonly]:focus {
  outline: none;
  border-color: var(--color-border);
}

/* A select in a stacked .form-field fills the row like the inputs beside
   it; one in a flex row (the users list's inline Role select) is left to
   take its content's width instead. */
.form-field select {
  width: 100%;
}

/* The country/number pair (shared/_phone_field). One control, and it looks
   like one: both halves sit inside a single bordered shell that carries the
   focus ring, rather than two boxes that happen to be adjacent.

   This replaced a native <select> capped at 11rem, which truncated -- a
   native select is as wide as its longest option, so "Bosnia and Herzegovina
   +387" was cut off, and shortening the label would have shortened the open
   list with it. The picker escapes that: a flag and a dialling code closed, a
   searchable list of full names open. See
   .scratch/phone-field-ui-prototype/README.md. */
.phone-field-shell {
  display: flex;
  align-items: stretch;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-surface);
}

.phone-field-shell:focus-within {
  outline: var(--focus-ring);
  outline-offset: var(--focus-ring-offset);
  border-color: var(--color-accent);
}

/* Beats .form-field input[type="tel"] on specificity, so the number field can
   drop the border it would otherwise draw inside the shell's own. */
.form-field .phone-field-shell .phone-field-number {
  flex: 1 1 auto;
  min-width: 0;
  border: 0;
  border-radius: 0 var(--radius) var(--radius) 0;
  background: transparent;
}

.form-field .phone-field-shell .phone-field-number:focus {
  outline: none;
}

.phone-field-divider {
  flex: 0 0 auto;
  width: 1px;
  margin: 0.5rem 0;
  background: var(--color-border);
}

.phone-field-picker {
  position: relative;
  flex: 0 0 auto;
}

/* The <select> is the real form field and the no-JS fallback, so it only goes
   away once phone_picker_controller has actually connected. Until then it is
   the control, and it has to lose its own border inside the shell the same
   way the number field does. */
.phone-field-picker:not(.is-enhanced) {
  flex: 0 1 11rem;
  min-width: 0;
}

.phone-field-picker:not(.is-enhanced) .phone-field-native {
  border: 0;
  border-radius: var(--radius) 0 0 var(--radius);
  background-color: transparent;
}

.phone-field-picker:not(.is-enhanced) .phone-field-native:focus {
  outline: none;
}

.phone-field-picker.is-enhanced .phone-field-native {
  display: none;
}

/* KEEP `line-height: normal`. `font: inherit` drags the body's 1.5 in with
   it, and this is a flex container, so its height is the tallest child's line
   box -- 28.5px against the 22px an <input> gets. Without this line the whole
   control stands 6.5px taller than every other field on the page and breaks
   the rows it sits in. The flag and code carry `line-height: 1` for the same
   reason: neither may become what sets the height. */
.phone-field-chip {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.5rem 0.6rem;
  border: 0;
  border-radius: var(--radius) 0 0 var(--radius);
  background: transparent;
  font: inherit;
  font-size: 0.95rem;
  line-height: normal;
  color: var(--color-text);
  white-space: nowrap;
  cursor: pointer;
}

.phone-field-chip:focus-visible {
  outline: none;
  background: var(--color-accent-bg);
}

/* `display: flex` above beats the UA stylesheet's `[hidden] { display: none }`
   -- same specificity, author sheet wins -- so without this the chip renders
   whatever its `hidden` attribute says. That attribute is how the chip stays
   out of the way until phone_picker_controller has connected, so losing it
   means a flash of a dead button at every first paint, and a permanent one
   beside the <select> wherever the JavaScript never runs at all. */
.phone-field-chip[hidden] {
  display: none;
}

/* The webfont first, so the flag is the same artwork on every platform rather
   than whatever the OS happens to ship. It covers only the regional-indicator
   range, so everything else falls straight through to the stack behind it. */
.phone-field-flag,
.phone-field-option-flag {
  flex: 0 0 auto;
  font-family:
    "Twemoji Country Flags", "Apple Color Emoji", "Segoe UI Emoji",
    "Noto Color Emoji", sans-serif;
  font-size: 1.1rem;
  line-height: 1;
}

.phone-field-code {
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

.phone-field-caret {
  font-size: 0.7rem;
  line-height: 1;
  color: var(--color-text-muted);
}

.phone-field-pop {
  position: absolute;
  z-index: 60;
  top: calc(100% + 0.4rem);
  left: 0;
  width: 20rem;
  max-width: min(20rem, calc(100vw - 3rem));
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  box-shadow: 0 12px 28px -12px rgba(60, 40, 20, 0.35);
  overflow: hidden;
}

.phone-field-search {
  padding: 0.5rem;
  border-bottom: 1px solid var(--color-border);
}

.phone-field-search input[type="search"] {
  width: 100%;
}

.phone-field-list {
  max-height: 16rem;
  overflow-y: auto;
}

.phone-field-group-label {
  margin: 0;
  padding: 0.5rem 0.7rem 0.2rem;
}

.phone-field-option {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  width: 100%;
  padding: 0.4rem 0.7rem;
  border: 0;
  background: none;
  font: inherit;
  font-size: 0.9rem;
  color: var(--color-text);
  text-align: left;
  cursor: pointer;
}

/* Hover and keyboard highlight share one treatment: the list is driven by
   both and they are the same state to a User. */
.phone-field-option:hover,
.phone-field-option.is-active {
  background: var(--color-accent-bg);
}

.phone-field-option[aria-selected="true"] .phone-field-option-name {
  font-weight: 600;
}

.phone-field-option-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.phone-field-option-code {
  color: var(--color-text-muted);
  font-variant-numeric: tabular-nums;
}

.phone-field-empty {
  margin: 0;
  padding: 0.9rem 0.7rem;
  font-size: 0.9rem;
  color: var(--color-text-muted);
}

.form-hint {
  margin: 0.3rem 0;
  font-size: 0.8rem;
  color: var(--color-text-muted);
}

/* A checkbox drawn as a switch, which says by its shape that it turns something
   on. It works beside a visible <label> or as one wrapped around the box, so
   onboarding's tick with no word beside it is this same control, and so is
   every switch on the notification grid. */
.form-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;

  input[type="checkbox"] {
    flex: none;
    appearance: none;
    -webkit-appearance: none;
    width: 2rem;
    height: 1.15rem;
    margin: 0;
    border-radius: 999px;
    background: var(--color-border);
    position: relative;
    cursor: pointer;
    transition: background 0.15s ease;

    &::before {
      content: "";
      position: absolute;
      top: 0.125rem;
      left: 0.125rem;
      width: 0.9rem;
      height: 0.9rem;
      border-radius: 50%;
      background: var(--color-surface);
      box-shadow: var(--shadow);
      transition:
        transform 0.22s var(--ease-spring),
        width 0.12s ease;
    }

    /* Held, the knob stretches toward where it is going. */
    &:active::before {
      width: 1.15rem;
    }

    /* Pressed and waiting on the server: Turbo marks the form busy while
       its request is out. */
    form[aria-busy="true"] &::before {
      animation: switch-busy 0.8s ease-in-out infinite;
    }

    &:checked {
      background: var(--color-accent);

      &::before {
        transform: translateX(0.85rem);
      }

      &:active::before {
        transform: translateX(0.6rem);
      }
    }

    &:disabled {
      opacity: 0.4;
      cursor: default;
    }
  }

  label {
    font-size: 0.9rem;
    cursor: pointer;
  }
}

@keyframes switch-busy {
  50% {
    opacity: 0.45;
  }
}

/* Every checkbox that is a box rather than a switch, wherever it sits: the
   consents, the Terms box, the weekly summary, All day. Left to itself it
   draws in the platform's blue, the one colour on a warm page that belongs
   to nothing on it, and accent-color only recolours it -- its size and shape
   stay the platform's. So it is drawn: --color-border at --radius-sm, filled
   with the accent when checked, with the accent ring of the global
   :focus-visible. A class the box carries rather than input[type=checkbox],
   because the switch (.form-toggle) is a checkbox too.

   It sits centered on the first line of the text beside it at any size: it
   takes that text's font and line-height, so 1lh is that line. The margin is
   !important to outrank the global :first-child reset, which would zero it
   where the box leads its label (check_box_tag on signup).

   The tick is an inline SVG for the same reason the select's chevron is (no
   asset request), and hardcodes its stroke colour for the same reason: a
   data: URI can't read --color-on-accent. Keep it in step with that token. */
.checkbox {
  appearance: none;
  -webkit-appearance: none;
  flex: none;
  width: 1.25rem;
  height: 1.25rem;
  font: inherit;
  margin: calc((1lh - 1.25rem) / 2) 0 !important;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  cursor: pointer;

  &:checked {
    border-color: var(--color-accent);
    background-color: var(--color-accent);
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.75 6.25 5 8.5l4.25-5' fill='none' stroke='%23fff' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: center;
  }
}

/* A checkbox with its sentence beside it, the box held to the first line so
   a long sentence (the signup's consent wording) wraps under itself rather
   than under the box. Every box's sentence is drawn alike, whether optional
   or required; only a consent's carrier fine print (-fine) is set apart,
   smaller and muted on its own line. */
.form-check {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  margin: 0.3rem 0 1rem;
  font-size: 0.9rem;
  color: var(--color-text);
  cursor: pointer;
}

.form-check-fine {
  display: block;
  margin-top: 0.2rem;
  font-size: 0.8rem;
  font-style: italic;
  color: var(--color-text-muted);
}

/* Separates fields about different things on one form (the signup's website
   from the person signing up), where a heading would be more than they need. */
.form-divider {
  border: 0;
  border-top: 1px solid var(--color-border);
  margin: 1.25rem 0;
}

.form-errors {
  background: var(--color-fail-bg);
  color: var(--color-fail);
  border-radius: var(--radius);
  padding: 0.6rem 0.9rem;
  margin-bottom: 1rem;
}

.form-errors p {
  margin: 0 0 0.3rem;
  font-weight: 600;
}

.form-errors ul {
  margin: 0;
  padding-left: 1.25rem;
}

.form-actions {
  margin-top: 1.25rem;
}

/* --- Popup windows (Mailbox Connection OAuth flow, layouts/popup) --- */

.popup-container {
  min-height: calc(100vh - var(--impersonation-strip-height));
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
}

.popup-card {
  max-width: 26rem;
  text-align: center;
}

.popup-card-icon {
  display: inline-block;
  color: var(--color-degraded);
  margin-bottom: 0.8rem;
}

.popup-card-heading {
  font-size: 1.25rem;
  margin-bottom: 0.5rem;
}

.popup-card-body {
  color: var(--color-text-muted);
  margin-bottom: 1.25rem;
}

.popup-card-actions {
  display: flex;
  gap: 0.5rem;
  justify-content: center;
  flex-wrap: wrap;
}

/* --- Screen 2 (signup/analyses#show): live analysis report ---
   Folded in from the "report generating" prototype (2026-09-03): a
   real-time progress bar over the list. The rows are the main app's
   (urls/_source_row, resource-evaluator spec § 8.8), so they take its
   .row rules and keep no shape of their own here. */

/* An address has no break opportunities of its own, and on a phone the card
   leaves it under 200px: unbroken, a long one ran out of the card and pushed
   the page sideways (mobile.md B-05, product-docs 169). */
.signup-analyses-address {
  color: var(--color-text-muted);
  margin: 0 0 1.25rem;
  overflow-wrap: anywhere;
}

.signup-analyses-progress {
  height: 6px;
  background: var(--color-border);
  border-radius: 999px;
  overflow: hidden;
  margin-bottom: 0.4rem;
}
.signup-analyses-progress-bar {
  height: 100%;
  width: 0%;
  background: var(--color-accent);
  animation: signup-analyses-progress-in 1.8s ease-out forwards;
}
@keyframes signup-analyses-progress-in {
  to {
    width: var(--target);
  }
}

.signup-analyses-count {
  margin: 0 0 1.5rem;
  font-size: 0.85rem;
  color: var(--color-text-muted);
}

.signup-analyses-footer {
  margin-top: 0.5rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--color-border);
}

/* --- Screens 1 & 3 (signup/analyses/new, signup/users/new): warm shell ---
   Folded in from the Screen 1/3 UI prototype (2026-09-03): centered card,
   warm palette, step badge. Same --color-warm-* tokens as the app shell's
   sidebar/resource rows (both independently converged on this palette) --
   shadow/radius stay their own literals since this is a bigger, standalone
   hero card rather than a compact row, not because the palette differs. */

.signup-shell {
  display: flex;
  justify-content: center;
  padding: 2.5rem 1rem;
}

/* This card's surface is now the platform's card surface -- the
   background/border/radius/shadow it used to declare here live in the
   shared card rule near the top of this file. */
.signup-shell-card {
  width: 100%;
  max-width: 28rem;
  text-align: center;
}

/* Screen 2's report is the page's whole point, so its card takes the room
   rather than sharing it with empty background. Widening here alone does
   nothing -- the logged-out shell's .app-main clamps a logged-out page to a
   36rem column -- so that clamp lifts for this card too
   (logged_out.css). */
.signup-shell--wide .signup-shell-card {
  max-width: 52rem;
}

.signup-analyses {
  text-align: left;
}

.signup-shell-step {
  display: inline-block;
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--color-warm-accent);
  background: var(--color-warm-accent-bg);
  border-radius: 999px;
  padding: 0.3rem 0.8rem;
  margin-bottom: 0.8rem;
}

.signup-shell-heading {
  font-size: 1.6rem;
  margin: 0 0 0.5rem;
  color: var(--color-warm-heading);
}

.signup-shell-subheading {
  color: var(--color-warm-muted);
  margin: 0 0 1.5rem;
  font-size: 0.95rem;
}

.signup-shell .form-field {
  text-align: left;
}

/* --- History (histories/show) ---
   A dense log of everything that has happened on the Account, with an
   inspector that slides over the right edge rather than taking a column of
   its own, so the table keeps its full width whenever nothing is selected.

   Chosen from a seven-variant UI prototype (2026-09-05,
   .claude/skills/prototype/UI.md): a day-grouped feed, a query console, a
   grouping by subject, a split inspector, and three placements of this
   drawer. The full set stays on the throwaway branch
   worktree-history-page-prototype, and the reasoning is in
   .scratch/account-history/issues/01-history-page-ui-prototype.md.

   Every control on the page is one the app already had -- the shared input
   treatment, the app-wide <select>, .button/.button--primary/.button--subtle,
   and the .resource-tabs strip from the Url detail page. The only new
   drawing here is the table, the drawer and the row avatar. */

.history-lede {
  margin: 0 0 1rem;
  color: var(--color-text-muted);
  max-width: 40rem;
}

.history-filters {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}

/* A <select> is as wide as its longest option, and this one lists every
   resource by name, so one long page name must not widen the bar past a
   phone's screen. */
.history-resource {
  max-width: 100%;
}

/* Takes the shared input treatment (see the .form-field list above, which
   this type is part of) but grows to fill the bar. */
.history-search {
  flex: 1;
  min-width: 12rem;
}

/* The stretch a link narrowed History to (history.md B-26). Not muted like
   the result line under it: it is a narrowing in force, and the only one
   with no control on the bar to show it. */
.history-stretch,
.history-kind {
  margin: 0.6rem 0;
  font-size: 0.85rem;
}

.history-result-count {
  margin: 0.6rem 0;
  font-size: 0.85rem;
  color: var(--color-text-muted);
}

/* The area strip itself is under "The area strip" above. */
.history-area-count {
  color: var(--color-text-muted);
  font-weight: 400;
  font-variant-numeric: tabular-nums;
}

.history-area-tab[aria-current="page"] .history-area-count {
  color: inherit;
}

/* 641-1536px, where the root steps down: the English strip does not fit with
   its counts, so they go and the tabs tighten. The total is still in the
   result count above the strip. The Spanish strip still scrolls, by design
   (wide-viewport-layout ticket 09). Range syntax, as the root's steps. */
@media (40rem < width <= 96rem) {
  .resource-tab.history-area-tab {
    padding: 0.6rem;
  }

  .history-area-count {
    display: none;
  }
}

/* The panel is the tab strip's card, butted under it -- the same join
   .resource-panel makes, minus the padding, since a table holds itself off its
   own edges. */
.history-panel {
  padding: 0;
  border-top: none;
  border-radius: 0 0 var(--card-radius) var(--card-radius);
  /* Clips the table's corners to the card's radius; scrolls sideways rather
     than cutting a row off if the viewport is narrower than the columns. */
  overflow-x: auto;
  overflow-y: hidden;
  /* The containing block for the details column's .visually-hidden header,
     which is absolutely positioned: without it, that span is placed against
     the page, past this scroller, and widens the page. */
  position: relative;
}

.history-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.85rem;
}

.history-table th {
  text-align: left;
  padding: 0.5rem 0.7rem;
  border-bottom: 1px solid var(--color-warm-border);
  white-space: nowrap;
}

.history-table td {
  padding: 0.5rem 0.7rem;
  border-bottom: 1px solid var(--color-warm-border);
  vertical-align: middle;
}

.history-table tbody tr:last-child td {
  border-bottom: none;
}

.history-row {
  cursor: pointer;
}

.history-row:hover {
  background: var(--color-surface);
}

.history-row--active {
  background: var(--color-accent-bg);
}

/* A row only Operators read (the Console's trail) is tinted in that tone.
   The row carries the fact, so the rule keys on it, not on the Console. */
.history-table .history-row.status-tone--operator {
  background: var(--tone-bg);
}

/* A row that opens a page (the Console's Accounts): its name's link is
   stretched over the row, as .row--opens stretches a row's name, so a new
   tab or a middle click works anywhere on it. */
.history-row:has(.history-row-link) {
  position: relative;
}

.history-row-link::after {
  content: "";
  position: absolute;
  inset: 0;
}

/* Cells lay out through this, never on the <td> itself: a td with
   `display: flex` is no longer a table-cell, and the column widths and row
   rules go with it. */
.history-cell {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;
}

/* The three narrow columns hold their width so the sentence beside them
   takes the slack and wraps last; without the minimums the greedy column
   squeezes them until email addresses break mid-address. */
.history-when {
  width: 7.6rem;
  min-width: 7.6rem;
  white-space: nowrap;
  color: var(--color-text-muted);
  font-size: 0.8rem;
  font-variant-numeric: tabular-nums;
}

/* 12rem, not 14: at 1025px, the narrowest width with the rail expanded, the
   columns' minimums must sum within the panel's 687px.
   `overflow-wrap` here and on `.history-subject` is the last resort for an
   address part wider than its line: the address itself wraps at its own
   punctuation (ApplicationHelper#address_breaks). Without a wrap rule the
   table's min-content is its longest address, and the table outgrows its
   panel. */
.history-what {
  width: 100%;
  min-width: 12rem;
  overflow-wrap: anywhere;
}

.history-who {
  width: 7rem;
  min-width: 7rem;
}

.history-who-name {
  font-size: 0.8rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.history-subject {
  width: 13rem;
  min-width: 13rem;
  overflow-wrap: anywhere;
}

.history-subject-text {
  display: flex;
  flex-direction: column;
  min-width: 0;
  line-height: 1.25;
}

.history-subject-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.6rem;
  height: 1.6rem;
  border-radius: 999px;
  background: var(--color-warm-accent-bg);
  color: var(--color-warm-accent);
  flex-shrink: 0;
}

.history-avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: var(--radius-sm);
  background: var(--color-warm-accent-bg);
  color: var(--color-warm-accent);
  font-size: 0.7rem;
  font-weight: 700;
  flex-shrink: 0;
}

.history-open {
  width: 2rem;
}

/* The row's real control: the row itself is a convenience surface, this is
   what the keyboard reaches and what carries the expanded state. */
.history-open-button {
  display: block;
  appearance: none;
  border: none;
  background: none;
  padding: 0.2rem 0.3rem;
  border-radius: var(--radius-sm);
  color: var(--color-text-muted);
  font: inherit;
  cursor: pointer;
}

.history-open-button:hover {
  color: var(--color-text);
}

.history-chevron {
  display: block;
  transition: transform 0.12s ease;
}

.history-open-button[aria-expanded="true"] .history-chevron {
  transform: rotate(90deg);
}

.history-empty {
  margin: 0;
  padding: var(--card-padding);
  color: var(--color-text-muted);
}

/* --- History: the inspector drawer --- */

.history-drawer {
  position: fixed;
  top: calc(var(--app-header-height) + var(--impersonation-strip-height));
  right: 0;
  bottom: 0;
  width: min(24rem, 92vw);
  z-index: 5;
  display: flex;
  flex-direction: column;
  background: var(--color-surface);
  border-left: 1px solid var(--color-warm-border-strong);
  box-shadow: -14px 0 30px -18px rgba(180, 120, 60, 0.5);
}

/* `display: flex` above outranks the hidden attribute's UA rule, so the
   closed drawer has to be told to stay shut. */
.history-drawer[hidden] {
  display: none;
}

.history-drawer-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.5rem 0.9rem;
  border-bottom: 1px solid var(--color-warm-border);
  flex-shrink: 0;
}

.history-drawer-title {
  margin: 0;
}

.history-drawer-body {
  overflow-y: auto;
  padding: 0.9rem 1rem;
}

.history-detail-head {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
}

.history-detail-title {
  margin: 0;
  font-size: 0.95rem;
  line-height: 1.3;
}

.history-detail-when {
  margin: 0.2rem 0 0;
  font-size: 0.8rem;
  color: var(--color-text-muted);
}

.history-detail-ago {
  opacity: 0.8;
}

.history-detail-block {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  margin-top: 0.9rem;
  padding-top: 0.7rem;
  border-top: 1px solid var(--color-warm-border);
  min-width: 0;
}

.history-detail-party {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;
}

.history-detail-party-name {
  display: block;
  font-size: 0.85rem;
  overflow-wrap: anywhere;
}

.history-detail-party-kind {
  display: block;
  font-size: 0.7rem;
  color: var(--color-text-muted);
}

.history-facts {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 0.2rem 0.8rem;
  margin: 0;
  font-size: 0.8rem;
}

.history-facts dt {
  color: var(--color-text-muted);
}

.history-facts dd {
  margin: 0;
  overflow-wrap: anywhere;
}

.history-related {
  list-style: none;
  margin: 0;
  padding: 0;
}

.history-related li {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.3rem 0;
  font-size: 0.8rem;
}

.history-detail-technical {
  margin: 0.9rem 0 0;
  padding-top: 0.6rem;
  border-top: 1px solid var(--color-warm-border);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.7rem;
  color: var(--color-text-muted);
  overflow-wrap: anywhere;
}

/* --- Notification Preferences (profile/notifications) ---
   The Message Type x Notification Channel grid, grouped by Monitored
   Resource. Settled over five rounds with the human on
   .scratch/notification-preferences-ui/issues/01-preferences-control-surface-prototype.md,
   which records what was tried first and why each of these won. --- */

/* The channel masters and the individual cells are .form-toggle (Forms
   above); what follows is only where they sit and the states the grid gives
   them. */

/* No busy pulse here. The grid posts as one form, on Save or on onboarding's
   Finish, so every switch would pulse at once rather than the one pressed. */
form[aria-busy="true"] .notif-prefs-table .form-toggle input[type="checkbox"]::before {
  animation: none;
}

/* A cell whose channel master is off. The value is kept (the master is a
   gate, not a bulk-set) but it is out of reach, so it reads as inactive
   rather than as off. A switch awaiting its Channel Consent is recoverable the
   same way (tick the box and it returns), so it wears the same treatment; it
   is drawn off as well, and still takes the press that says why. */
.notif-prefs-table .form-toggle.is-gated,
.notif-prefs-table .form-toggle.is-awaiting-consent {
  opacity: 0.45;
}

/* The label and its timing share a line, and the timing wraps under the label
   when the column cannot fit both -- a narrow viewport, a longer language. */
.notif-prefs-type-name {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  column-gap: 0.8rem;
  row-gap: 0.1rem;
  font-weight: 600;
  color: var(--color-text);
}

/* When the row can reach the User. Hint-sized and muted, with a stroke icon
   from the chrome's set, so it reads as part of the label rather than as a
   badge: a filled pill would look like the contact chips above the grid, and
   a red one like a failing check. */
.notif-prefs-timing {
  display: inline-flex;
  align-items: center;
  align-self: center;
  gap: 0.4rem;
  font-size: 0.8rem;
  font-weight: 400;
  color: var(--color-warm-muted);
}

.notif-prefs-timing svg {
  flex: none;
}

.notif-prefs-description {
  display: block;
  margin-top: 0.2rem;
  font-size: 0.85rem;
  color: var(--color-text-muted);
}

.notif-prefs-grid-hint {
  margin: 0 0 0.6rem;
}

/* The scroll wrapper is the card, and the table sits bare on it -- the same
   join .history-panel makes, and for the same reason: a table holds itself
   off its own edges, so the card wants no padding. */
.notif-prefs-wrap {
  padding: 0;
  /* Clips the table's corners to the card's radius; scrolls sideways rather
     than cutting a column off on a narrow viewport. */
  overflow-x: auto;
  overflow-y: hidden;
}

/* One table with a <tbody> per section, so the channel columns stay aligned
   all the way down -- a table per section would let them drift apart and
   lose the grid's whole advantage. */
.notif-prefs-table {
  width: 100%;
  border-collapse: collapse;
}

.notif-prefs-table th,
.notif-prefs-table td {
  padding: 0.8rem 0.9rem;
  text-align: left;
  vertical-align: top;
  border-bottom: 1px solid var(--color-warm-border);
}

.notif-prefs-table tbody tr:last-child th,
.notif-prefs-table tbody tr:last-child td {
  border-bottom: 0;
}

.notif-prefs-table thead th {
  border-bottom: 2px solid var(--color-warm-border);
  color: var(--color-warm-heading);
}

.notif-prefs-corner {
  width: 100%;
}

/* Channel name, then master toggle. */
.notif-prefs-channel {
  min-width: 5.5rem;
}

.notif-prefs-channel-name {
  display: block;
  font-size: 0.95rem;
}

.notif-prefs-channel .form-toggle {
  display: flex;
  margin-top: 0.4rem;
}

.notif-prefs-unavailable-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.05rem;
  height: 1.05rem;
  padding: 0;
  border: 1px solid var(--color-border);
  border-radius: 50%;
  background: var(--color-surface);
  color: var(--color-text-muted);
  font-family: Georgia, "Times New Roman", serif;
  font-size: 0.7rem;
  font-weight: 700;
  line-height: 1;
  cursor: help;
}

.notif-prefs-unavailable-note {
  position: absolute;
  z-index: 1;
  top: calc(100% + 0.35rem);
  right: 0;
  width: max-content;
  max-width: min(14rem, 60vw);
  padding: 0.5rem 0.6rem;
  border: 1px solid var(--color-warm-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  box-shadow: var(--shadow);
  color: var(--color-text);
  font-size: 0.7rem;
  font-weight: 400;
  line-height: 1.35;
  text-align: left;
  white-space: normal;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.12s ease;
}

.notif-prefs-unavailable:hover .notif-prefs-unavailable-note,
.notif-prefs-unavailable-icon:focus-visible + .notif-prefs-unavailable-note {
  opacity: 1;
  visibility: visible;
}

.notif-prefs-row-head {
  font-weight: 400;
}

.notif-prefs-cell {
  text-align: center;
}

.notif-prefs-cell .form-toggle {
  justify-content: center;
}

/* A (Message Type, Notification Channel) pair the registry does not carry.
   Deliberately not .is-gated's treatment: a gated cell is recoverable (reopen
   the column and the User's choice comes back, which is the whole point of
   ADR 0046), this one is permanent. So the two must not look alike -- dimmed
   like the gate, but with the switch hollowed out to a dashed outline, which
   reads as "there is no switch here" rather than "the switch is off".

   The note is absolutely positioned against this wrapper, which is why the
   wrapper itself is relative. */
.notif-prefs-unavailable {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
}

.notif-prefs-table .form-toggle.is-unavailable {
  opacity: 0.55;

  input[type="checkbox"] {
    background: transparent;
    border: 1px dashed var(--color-border);

    &::before {
      display: none;
    }
  }
}

/* The section header is a *label*, not a surface. Two earlier attempts
   treated it as one: the page background read as a hole punched through the
   card, and a filled accent band read as zebra striping once there were six
   sections over seven content rows. What carries the grouping instead is a
   hairline, whitespace, and the section's own daily-summary control. */
.notif-prefs-section-head th {
  padding: 1.25rem 0.9rem 0.3rem;
  border-top: 1px solid var(--color-warm-border);
  color: var(--color-warm-muted);
}

.notif-prefs-section:first-of-type .notif-prefs-section-head th {
  border-top: 0;
  padding-top: 0.7rem;
}

.notif-prefs-section-bar {
  display: flex;
  align-items: center;
}

/* Who we reach, and when: the User's own columns, above the grid but a line
   tall until opened. Closed it is a summary of its own fields; opened they
   lie across rather than down, so the grid keeps the fold. Chosen from a
   five-variant prototype -- see the notification-preferences-ui entry in the
   issue tracker for what below-the-grid, a side rail and a full band cost. */
.notif-prefs-contact {
  margin: 0 0 1.25rem;
  padding: 0;
}

/* `list-style: none` and the -webkit rule are one job in two dialects:
   Safari still draws the disclosure triangle through the pseudo-element. */
.notif-prefs-contact-summary {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 0.6rem;
  /* The right gutter is the toggle's own width plus this padding again: the
     word is out of flow, so nothing else keeps the chips off it. */
  padding: 0.7rem 5.3rem 0.7rem 0.9rem;
  cursor: pointer;
  list-style: none;
}

.notif-prefs-contact-summary::-webkit-details-marker {
  display: none;
}

.notif-prefs-contact-summary:focus-visible {
  outline-offset: -2px;
  border-radius: var(--card-radius);
}

.notif-prefs-contact-heading {
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--color-warm-heading);
}

.notif-prefs-contact-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}

/* Hidden the moment the fields are reachable: these read *stored* state, and
   a summary of what the fields no longer say is worse than no summary. */
.notif-prefs-contact[open] .notif-prefs-contact-chips {
  display: none;
}

.notif-prefs-contact-chip {
  padding: 0.2rem 0.6rem;
  border: 1px solid var(--color-warm-border);
  border-radius: 999px;
  background: var(--color-warm-accent-bg);
  font-size: 0.8rem;
  color: var(--color-warm-heading);
}

/* No User Number on file -- the one gap that stops a Notification Channel
   reaching anybody. Said in the summary's vocabulary rather than with an
   icon: this is a resting state, not something revealed on hover. */
.notif-prefs-contact-chip.is-missing {
  border-color: var(--color-degraded);
  background: var(--color-degraded-bg);
  color: var(--color-degraded);
}

/* Pinned to the summary's top-right corner rather than carried along at the
   end of the row. In flow the word rides everything the toggle changes:
   closed, four chips wrap and push it down a line; open, they are gone and
   the summary is one line tall -- so Close would land somewhere Edit never
   was, out from under the pointer that just opened the block. Out of flow it
   is the same box in both states, and the fixed width with centered text
   holds the two words concentric despite Close being the longer of them. */
.notif-prefs-contact-toggle {
  position: absolute;
  /* The summary's own padding-top: level with the heading's first line. */
  top: 0.7rem;
  right: 0.9rem;
  min-width: 3.5rem;
  text-align: right;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--color-accent);
}

.notif-prefs-contact-toggle-close,
.notif-prefs-contact[open] .notif-prefs-contact-toggle-open {
  display: none;
}

.notif-prefs-contact[open] .notif-prefs-contact-toggle-close {
  display: inline;
}

.notif-prefs-contact-body {
  padding: 1rem 0.9rem;
  border-top: 1px solid var(--color-warm-border);
}

/* Two panels: where we reach the User, and when. The number's consent boxes
   ride under the number they are given for, and their labels are long (the
   texts one is fixed by carrier vetting), so "where" takes the wider share.
   Folded in from the contact-card UI prototype (2026-10-20,
   .claude/skills/prototype/UI.md) -- won out over a single stacked column,
   label-left channel rows and two separate cards; the reasoning is in
   .scratch/signup-form/issues/05-redesign-the-contact-card.md. */
.notif-prefs-contact-fields {
  display: grid;
  grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
  gap: 0 2rem;
  align-items: start;
}

.notif-prefs-contact-fields .form-field {
  margin-bottom: 0.9rem;
}

/* Set in from the number by a rule down its edge, so the two boxes read as
   belonging to the field above them rather than as fields of their own. */
.notif-prefs-contact-consents {
  margin: -0.3rem 0 0.9rem;
  padding: 0.6rem 0.8rem 0;
  border-left: 2px solid var(--color-warm-accent);
}

.notif-prefs-contact-consents .form-check:last-of-type {
  margin-bottom: 0.6rem;
}

/* Under its box, set in to the box's own wording, once a switch in a column
   awaiting that consent is pressed (channels.md B-26). Hint-sized, as the
   field hints are, and coloured as the missing-number chip is: it names
   the same kind of gap. */
.notif-prefs-consent-note {
  margin: -0.2rem 0 0.6rem 1.5rem;
  color: var(--color-degraded);
}

/* The hours window, inside the narrower panel: the "all day" box and the
   hint take whole rows, the two selects share one. */
.notif-prefs-contact-hours {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 0 1rem;
  align-items: end;
  /* Not a .form-field, so it does not inherit the row's own bottom margin --
     restate it, or the hint runs into the Save below. */
  margin-bottom: 0.9rem;
}

.notif-prefs-contact-hours > .notif-prefs-contact-subheading,
.notif-prefs-contact-hours > .notif-prefs-contact-check,
.notif-prefs-contact-hours > .form-hint {
  grid-column: 1 / -1;
}

/* Names a panel, and the hours group within "when". Half a step down from
   the block's own heading: a label for part of that block, not a peer of it. */
.notif-prefs-contact-subheading {
  display: block;
  margin: 0 0 0.6rem;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--color-warm-heading);
  border-bottom: 1px solid var(--color-warm-accent);
}

.notif-prefs-contact-hours > .form-hint {
  margin-top: 0;
}

.notif-prefs-contact-hours .form-field select {
  width: 100%;
}

/* An "All day" checkbox is a switch with its label beside it, not a stacked
   field: .form-field's block, bold label would put the word underneath the
   box and read as a third heading in the group. */
.notif-prefs-contact-check {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}

.notif-prefs-contact-fields .notif-prefs-contact-check {
  margin-bottom: 0.5rem;
}

.notif-prefs-contact-check label {
  margin-bottom: 0;
  font-weight: 400;
}

.notif-prefs-contact-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 0.8rem;
  margin-top: 0.8rem;
}

.notif-prefs-contact-actions .form-hint {
  margin: 0;
}

/* One column below this: the consent labels stop fitting beside "when"
   before the grid does, and the panels stack, "where" first. */
@media (max-width: 56rem) {
  .notif-prefs-contact-fields {
    grid-template-columns: minmax(0, 1fr);
    max-width: 32rem;
  }

  .notif-prefs-contact-panel + .notif-prefs-contact-panel {
    margin-top: 1.25rem;
  }
}

.notif-prefs-save {
  display: flex;
  align-items: center;
  gap: 0.8rem;
}

/* --- Resource page, band 1: the resource header ---
   Above the 640px block because that block restyles these, and from below it
   they would win. The rest of the page is below it, under "Resource page". */

.resource-header {
  /* The positioning context for the What-we-know panel, which hangs off this
     header's own bottom edge rather than off the button that opens it -- see
     the open-state rule below. Hung off the trigger it would be a dropdown,
     and a dropdown is a different claim: what opens here is the rest of the
     thing you are already looking at. */
  position: relative;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1.5rem;
  flex-wrap: wrap;
  padding-bottom: 1rem;
  border-bottom: 1px solid var(--color-border);
}

/* flex-basis 0, not auto: with auto the identity block claims its text's
   full width and shoves the actions onto a second row, which is how a
   header turns into two. At zero it wraps its own text instead. */
.resource-header-identity {
  flex: 1 1 0;
  min-width: 0;
}

.resource-header-identity h1 {
  margin: 0;
  overflow-wrap: anywhere;
}

.resource-header-address {
  margin: 0.2rem 0;
  font-size: 0.95rem;
  overflow-wrap: anywhere;
}

.resource-header-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 0.4rem 0;
  color: var(--color-text-muted);
  font-size: 0.85rem;
}

.resource-header-actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-shrink: 0;
}

/* 640px -- phones. The 1024px rule above leaves the rail as a horizontal
   icon strip, which works while there is room for a row of labelled-enough
   glyphs. There is not at 390px: the strip becomes unlabelled icons sitting
   directly above the header's own controls, two navs competing and neither
   legible, while .app-main still spends 4rem of the viewport on padding and
   .resource-stats' auto-fit grid drops to one column and hands four numbers
   four full-width rows before the first resource.

   After the rules it restyles (.resource-stats, .page-header), because a media
   query adds no specificity of its own: from before them, those
   same-specificity rules would win and this block would silently do nothing. */
@media (max-width: 40rem) {
  /* The log drops to when/what: who and what-it-happened-to are both in the
     drawer, which takes the whole width here rather than a right-hand slice. */
  .history-table th:nth-child(3),
  .history-table td:nth-child(3),
  .history-table th:nth-child(4),
  .history-table td:nth-child(4) {
    display: none;
  }

  .history-when,
  .history-what,
  .history-who,
  .history-subject {
    width: auto;
    min-width: 0;
  }

  /* A phone is narrower than one unbroken domain plus a timestamp, and a
     table's min-content width is the sum of its columns' longest words --
     so let both break, rather than pushing the log sideways. */
  .history-table th,
  .history-table td {
    padding: 0.5rem;
    overflow-wrap: anywhere;
  }

  .history-when {
    white-space: normal;
  }

  .history-drawer {
    width: 100%;
    top: auto;
    height: 75vh;
    border-left: none;
    border-top: 1px solid var(--color-warm-border-strong);
  }

  .app-shell {
    --app-bar-height: 4.3rem;
    --app-gutter: 0.9rem;
    flex-direction: column;
    /* dvh, not vh: on mobile Safari vh is the *largest* viewport, so a
       100vh shell runs its last row under the browser toolbar. */
    min-height: calc(100dvh - var(--impersonation-strip-height));
  }

  /* What is pinned here is the 3rem bar, not the 4.3rem desktop header, so
     the root's scroll padding overshoots by the difference. Literal rather
     than var(--app-bar-height): that custom property is declared on
     .app-shell, and html is not inside it. */
  html {
    scroll-padding-top: calc(3rem + var(--impersonation-strip-height));
  }

  .app-main {
    max-width: none;
    /* Bottom padding clears the floating add button. */
    padding: 1rem var(--app-gutter) 5rem;
  }

  /* --- Page header -> floating add -------------------------------------
     The h1 stays in the header, as on a wider screen: the bar draws no
     title, so the h1 is where a phone reads the page's name (mobile.md
     B-04, product-docs 168). Only Add leaves it, to float over the page.
     Two classes, so it outranks popover_menu.css's position: relative,
     which loads after this file. */

  .page-header-add-menu.popover-menu {
    position: fixed;
    z-index: 55;
    right: var(--app-gutter);
    bottom: calc(env(safe-area-inset-bottom, 0px) + 1rem);
  }

  .page-header-add-menu summary {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 2.8rem;
    height: 2.8rem;
    padding: 0;
    border-radius: 999px;
    /* Not --color-warm-shadow, on purpose: that one lifts a card off the
       page, and this button floats over whatever scrolls beneath it, so it
       needs a darker, tighter shadow to read as above the content. */
    box-shadow: 0 8px 20px -6px rgba(150, 80, 30, 0.6);
    /* The trigger's label is "Add"; at this size the glyph carries it. */
    font-size: 0;
  }

  .page-header-add-menu summary::after {
    content: "+";
    font-size: 1.6rem;
    line-height: 1;
  }

  /* Opens upward -- there is nothing below it. */
  .page-header-add-menu .popover-menu-items {
    top: auto;
    bottom: calc(100% + 0.4rem);
    --menu-travel-y: -1;
    transform-origin: bottom right;
  }

  /* --- Dashboard ------------------------------------------------------- */

  /* Four numbers on one swipeable line, rather than four full-width rows
     before the first resource. */
  .resource-stats {
    display: flex;
    gap: 0.4rem;
    overflow-x: auto;
    scrollbar-width: none;
    margin: 0 calc(var(--app-gutter) * -1) 1rem;
    padding: 0 var(--app-gutter) 0.2rem;
  }

  .resource-stats::-webkit-scrollbar {
    display: none;
  }

  .resource-stat {
    flex: 0 0 auto;
    flex-direction: row;
    align-items: baseline;
    gap: 0.3rem;
    padding: 0.4rem 0.7rem;
    border-radius: 999px;
  }

  .resource-stat-number {
    font-size: 1rem;
  }

  .resource-stat-label {
    font-size: 0.75rem;
  }

  /* Boxes two by two rather than the strip of pills, for tiles that carry
     more than a number and a word: the uptime report's each carry a mark
     and a tip, which a pill has no room for. */
  .resource-stats--boxes {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.5rem;
    overflow: visible;
    margin: 0;
    padding: 0;

    & .resource-stat {
      flex-direction: column;
      align-items: stretch;
      gap: 0.2rem;
      /* The step below 0.85rem, not the one above: at 390px a half-width
         tile has one pixel to spare for "No figure" at the number's size,
         and the larger step wraps it onto two lines. */
      padding: 0.8rem;
      border-radius: var(--radius);
    }

    & .resource-stat-number {
      font-size: 1.3rem;
    }

    & .resource-stat-label {
      font-size: 0.8rem;
    }
  }

  /* Confirm/Delete are the whole point of an unconfirmed row, so they stay,
     stacked under the name where two buttons and a name will not share a
     390px line (.row-top wraps here). */
  .row-top > .row-acts {
    width: 100%;

    form {
      flex: 1;
    }

    .button {
      width: 100%;
      text-align: center;
    }
  }

  /* Notification Preferences (profile/notifications): a 3-column grid is
     the hard case on a phone, so the table is rebuilt rather than left to a
     horizontal scrollbar. The head row becomes a strip of channel masters,
     and each Message Type becomes a block whose rows are labelled from the
     cell's data-label. The section headers survive the reflow as the block
     titles -- the grouping's one real advantage here: the stacked rows
     arrive already labelled by resource, each with its weekly-summary
     checkbox still attached. */
  .notif-prefs-table,
  .notif-prefs-table thead,
  .notif-prefs-table tbody,
  .notif-prefs-table tr,
  .notif-prefs-table th,
  .notif-prefs-table td {
    display: block;
  }

  .notif-prefs-table thead tr {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
  }

  .notif-prefs-table thead th {
    flex: 1 1 7rem;
    border-bottom: 0;
  }

  .notif-prefs-table thead th.notif-prefs-corner {
    display: none;
  }

  .notif-prefs-table tbody tr {
    padding: 0.6rem 0;
    border-top: 1px solid var(--color-warm-border);
  }

  .notif-prefs-table tbody th,
  .notif-prefs-table tbody td {
    border-bottom: 0;
    padding: 0.3rem 0.9rem;
  }

  .notif-prefs-cell {
    display: flex;
    align-items: center;
    justify-content: space-between;
  }

  .notif-prefs-cell::before {
    content: attr(data-label);
    font-size: 0.85rem;
    color: var(--color-text-muted);
  }

  /* Horizontal inset only -- the vertical rhythm above the title is what
     separates one section from the next, so it must not be flattened. */
  .notif-prefs-section-head th {
    padding-left: 0.9rem;
    padding-right: 0.9rem;
  }

  /* --- Url page header ----------------------------------------------
     No room for the identity and the actions on one row. The actions keep
     their width (flex-shrink 0) and the identity got what was left, a
     column a few characters wide that stood the address on end and pushed
     the verdict off the first screen (mobile.md B-02). Here the header
     turns into two: the identity takes the whole row and the actions wrap
     under it (product-docs 167). */
  .resource-header-identity {
    flex-basis: 100%;
  }
}

/* --- Resource page -------------------------------------------------
   Three surfaces top to bottom -- header, action item card, tab card --
   and the rule that holds them together: exactly one of them is bordered
   at a time in any given band, so a row inside a card is never itself a
   card.

   Band 1, the resource header, is not here: it sits above the 640px
   block, which restyles it. */

.resource-show {
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
}

/* --- What we know: closed it is a button, open it is this header grown
   into a card ---

   The header GROWS rather than being replaced, and it does that without
   moving a pixel: every bit of box it gains when the panel opens is
   cancelled by a negative margin -- 1rem of top padding plus 1px of border
   against -1rem-1px of top margin, and the same on each side -- so the h1
   sits on exactly the pixel it sat on before the click. The bottom edge is
   untouched, so nothing below it moves either; the panel hangs off that edge
   absolutely positioned, in front of the action card and the tabs.

   That is the whole visual argument. A page that shoves the action card down
   the moment you ask a side question has made the side question the main
   event; a panel that hangs off a button in the corner has made the answer
   the button's, not the page's.

   :has() rather than a class a controller toggles -- [open] is already the
   state, and two other places here read a <details>'s open state from an
   ancestor (.app-shell, .row). */
.resource-header:has(.resource-facts[open]) {
  /* Over the sticky app header (20) and the rail (30): a card the page has
     stepped back from cannot pass under the furniture. */
  z-index: 32;
  /* !important on the top one only, and it is the reset near the top of this
     file it is answering: `:first-child { margin-top: 0 !important }`. This
     header is the first child of .resource-show on every resource page, so without
     it the vertical half of the cancellation is dropped while the horizontal
     half lands, and the header -- h1 included -- falls by exactly the top
     padding plus the border it just gained. Longhands rather than the
     shorthand so the one declaration that needs the flag is the only one
     carrying it. */
  margin-top: calc(-1rem - 1px) !important;
  margin-right: calc(-1.25rem - 1px);
  margin-bottom: 0;
  margin-left: calc(-1.25rem - 1px);
  padding: 1rem 1.25rem;
  border: 1px solid var(--card-border);
  /* The seam the panel butts against, and the one edge that keeps its old
     colour: it was the header's rule before the click and it is the fold in
     the card after it. */
  border-bottom-color: var(--color-border);
  border-radius: var(--card-radius) var(--card-radius) 0 0;
  background: var(--color-surface);
  box-shadow: var(--card-shadow);
}

/* The dimming that says the page behind has stepped back. Closing on an
   outside click and on Escape is popover_menu's job, not this element's --
   it is unreachable by the pointer, under the card and over everything else.
   ::before on the page column rather than anything inside the header: it has
   to cover the viewport, and a child of the header paints inside the
   header's own stacking context, which would dim the card itself. */
.resource-show:has(.resource-facts[open])::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 31;
  background: rgb(15 23 42 / 8%);
  /* Steps back while the card grows, rather than at once. */
  animation: resource-facts-dim 0.3s ease;
}

@keyframes resource-facts-dim {
  from {
    opacity: 0;
  }
}

.resource-facts-trigger {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  list-style: none;
  cursor: pointer;
}

.resource-facts-trigger::-webkit-details-marker {
  display: none;
}

.resource-facts-chevron {
  transition: transform 0.12s ease;
}

/* The label does not change when it opens -- the chevron turns instead.
   The cluster is right-aligned, so a wider label would nudge Snooze
   sideways, which is the one movement this panel exists to avoid. */
.resource-facts[open] .resource-facts-chevron {
  transform: rotate(90deg);
}

/* Held down while the card is open, because the card it opened is the same
   surface the button sits on: without this the one pressed control on the
   header is the only thing not saying anything happened. */
.resource-facts[open] > .resource-facts-trigger {
  border-color: var(--color-accent);
  background: var(--color-accent-bg);
  color: var(--color-accent-dark);
}

/* Flush under the header and exactly as wide as it. -1px on each side
   because these offsets are measured from the header's PADDING box and the
   card's edge is its border box; +1px on top so the header's bottom border
   stays visible as the fold rather than being covered by this. */
.resource-facts-panel {
  position: absolute;
  z-index: 33;
  top: calc(100% + 1px);
  left: -1px;
  right: -1px;
  padding: 1.25rem 1.25rem 1rem;
  background: var(--color-surface);
  border: 1px solid var(--card-border);
  border-top: 0;
  border-radius: 0 0 var(--card-radius) var(--card-radius);
  box-shadow: var(--card-shadow);

  /* GROWING STRAIGHT DOWN OUT OF THE HEADER (prototype/site-micro-
     interactions). The header turns into the card on the click itself -- no
     transition on it, the open-state rule above lands as it is -- and this
     panel opens downward from the fold: a clip, nothing sliding or fading
     inside it, at an even pace so the growing is seen. The clip reaches past
     the sides and the foot, where the card's shadow falls. A <details> takes
     the panel away at once on close. */
  .resource-facts[open] > & {
    animation: resource-facts-grow 0.36s cubic-bezier(0.4, 0, 0.2, 1);
  }
}

@keyframes resource-facts-grow {
  from {
    clip-path: inset(0 -2rem 100% -2rem);
  }

  to {
    clip-path: inset(0 -2rem -3rem -2rem);
  }
}

/* Snooze and the kebab, while the card is open. They no longer close it --
   they are controls standing on it -- so both lists now have to clear the
   panel hanging off the header's bottom edge, which a menu's usual z-index
   of 1 does not: without this, picking a snooze duration means reading the
   list through the card in front of it. */
.resource-header:has(.resource-facts[open]) .popover-menu-items {
  z-index: 34;
}

.resource-facts-cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: 1.5rem;
}

.resource-facts-heading {
  margin: 0 0 0.3rem;
}

.resource-facts-panel .form-field--dense {
  margin-top: 0.6rem;
}

/* Right-aligned under both columns, because the form spans both -- the
   button belongs to the card, not to the column whose one field is
   currently the editable one. */
.resource-facts-actions {
  display: flex;
  justify-content: flex-end;
  margin-top: 1rem;
}

.resource-found {
  margin: 0.5rem 0 0;
}

.resource-found-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.4rem;
  padding: 0.3rem 0;
  border-top: 1px solid var(--color-border);
}

.resource-found-row:first-child {
  border-top: 0;
}

.resource-found-row dt {
  flex: 1 1 9rem;
  color: var(--color-text-muted);
  font-size: 0.8rem;
}

.resource-found-row dd {
  flex: 1 1 10rem;
  margin: 0;
  font-size: 0.9rem;
  overflow-wrap: anywhere;
}

.resource-found-source {
  display: block;
  color: var(--color-text-muted);
  font-size: 0.75rem;
}

.resource-found-blank {
  color: var(--color-text-muted);
  font-style: italic;
}

/* --- Band 2, the action item card, is action_card.css --- */

/* --- Bands 3 and 4: one card, tabs inside it ---
   The card is what lets the panels' rows go flat. The strip is a segmented
   control sitting on the card's own surface rather than a tinted ledge
   across its top edge: a ledge reads as a titlebar, a heavy band of colour
   competing with the action card above it. No rule under it either -- the
   gap separates, and the panel's own first hairline is where the content
   starts. The element wears `card` for its frame; this is what the tab card
   draws differently, a deeper foot. */

.resource-tabs {
  display: flex;
  flex-direction: column;
  padding-block: 1rem 1.5rem;
}

/* ROWS REVEAL (prototype/site-micro-interactions; Site#show's tabs, which
   say so with --reveals). Nothing on the page moves when it loads but the
   open tab's rows, which land one by one, 220ms apart, as the signup
   Analysis's once did. Each lands still checking -- its mark the plain ring
   turning, as a row being checked draws it (icons.css) -- and 550ms later the
   ring goes and the mark and pill settle in.

   A row's place is its place in its list, so a tab with two sections
   reveals them side by side; the eleventh row on lands with the tenth.
   rows_reveal_controller.js sets data-revealed once the last has settled,
   so a tab opened afterwards draws its rows as they are. */
.resource-tabs--reveals:not([data-revealed])
  .resource-panel:not([hidden])
  > .row-stack
  > .resource-section
  > .row-list
  > .row {
  --reveal-at: 0ms;
  animation: rows-reveal-land 0.35s ease var(--reveal-at) backwards;

  &:nth-child(2) {
    --reveal-at: 220ms;
  }
  &:nth-child(3) {
    --reveal-at: 440ms;
  }
  &:nth-child(4) {
    --reveal-at: 660ms;
  }
  &:nth-child(5) {
    --reveal-at: 880ms;
  }
  &:nth-child(6) {
    --reveal-at: 1100ms;
  }
  &:nth-child(7) {
    --reveal-at: 1320ms;
  }
  &:nth-child(8) {
    --reveal-at: 1540ms;
  }
  &:nth-child(9) {
    --reveal-at: 1760ms;
  }
  &:nth-child(n + 10) {
    --reveal-at: 1980ms;
  }

  > .row-top > .row-glyph {
    position: relative;
    animation: rows-reveal-mark 0.25s ease calc(var(--reveal-at) + 550ms)
      backwards;

    /* The arc itself is icons.css's turning ring; this rule keeps only what
       the reveal changes about it. Grey, as the plain ring, and outside the
       mark's own ring, which the mark keeps. */
    &::after {
      --tone: var(--color-text-muted);
      inset: calc(var(--status-icon-ring) * -1);
      animation:
        status-icon-spin 0.8s linear infinite,
        rows-reveal-checked 0.25s ease calc(var(--reveal-at) + 550ms) forwards;
    }
  }

  > .row-top .row-pill {
    animation: rows-reveal-pill 0.25s ease calc(var(--reveal-at) + 600ms)
      backwards;
  }
}

@keyframes rows-reveal-land {
  from {
    opacity: 0;
    transform: translateY(4px);
  }
}

/* Still checking: no ring and no mark, only the turning ring over them. */
@keyframes rows-reveal-mark {
  from {
    border-color: transparent;
    color: transparent;
  }
}

@keyframes rows-reveal-checked {
  to {
    opacity: 0;
  }
}

@keyframes rows-reveal-pill {
  from {
    opacity: 0;
  }
}

.resource-tablist {
  align-self: flex-start;
  display: flex;
  gap: 0.2rem;
  padding: 0.3rem;
  border-radius: 999px;
  background: var(--color-bg);
  max-width: 100%;
  overflow-x: auto;
}

.resource-tab {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.9rem;
  border: 0;
  border-radius: 999px;
  background: none;
  color: var(--color-text-muted);
  font-size: 0.9rem;
  font-family: inherit;
  white-space: nowrap;
  cursor: pointer;
  /* A tab that is a link, where the reading is the server's (a broken-links
     Scan's ?group_by=). */
  text-decoration: none;
}

.resource-tab:hover {
  color: var(--color-text);
}

.resource-tab[aria-selected="true"] {
  background: var(--color-surface);
  color: var(--color-text);
  box-shadow: var(--shadow);
}

.resource-count {
  padding: 0.2rem 0.4rem;
  border-radius: 999px;
  background: var(--color-surface);
  color: var(--color-text-muted);
  font-size: 0.75rem;
  font-weight: 600;
}

.resource-tab[aria-selected="true"] .resource-count,
.resource-section-heading .resource-count {
  background: var(--color-accent-bg);
  color: var(--color-accent-dark);
}

/* A live update moved the count (live_update_shows_change.js). */
.resource-count[data-changed] {
  display: inline-block;
  animation: resource-count-changed 0.45s var(--ease-spring);
}

@keyframes resource-count-changed {
  40% {
    transform: translateY(-4px) scale(1.15);
  }
}

/* The count says how many things are in there; it cannot say that some of
   them are waiting on a decision, or that what is in there is on fire. The
   strip is the only surface that can advertise either.

   The colour is the tab's own tone when it has one -- Emails#show puts a tone
   class on the Deliverability tab -- and the accent when it does not, which is
   a Site's kind tabs' dot. One dot, two jobs, no second class: `--tone` is
   set by status-tone--* and by nothing else, so the fallback is the absence of
   a tone rather than a guess about which page this is. */
.resource-tab-dot {
  width: 0.45rem;
  height: 0.45rem;
  margin-left: -0.2rem;
  border-radius: 999px;
  background: var(--tone, var(--color-accent));
}

/* 640px, the phone layout. At the 125% root a two-tab strip, Summary ·
   Unmonitored, was 67px wider than a 390px phone's card, so Summary scrolled
   out of sight (site-unmonitored-tab 06: the fit is CSS, the label stays).
   Every tab gives up some padding, and a tab whose dot already says its count
   (`resource-tab--dot-counts`, set by sites/show/_tabs) drops the number, which
   stays in the dot's label. */
@media (max-width: 40rem) {
  .resource-tablist {
    padding: 0.2rem;
  }

  .resource-tab {
    gap: 0.3rem;
    padding: 0.5rem;
  }

  .resource-tab--dot-counts .resource-count {
    display: none;
  }
}

.resource-panel {
  padding: 1.25rem 0 0;
}

/* A website's Unmonitored tab (sites/show/_unmonitored): its list of kinds is
   the action card's list, drawn by action_card.css's shared block. What is
   here is the frame this card gives it. The list's column runs to the card's
   left, right and bottom edges: the bottom margin is .resource-tabs' foot, and
   !important to beat the :last-child reset. A long page address wraps in the
   narrowed panel, as it does on the action card.

   The action card clips its corners with overflow: hidden; this card does
   not, so its list rounds its own. Stacked (action_card.css's 58rem fold),
   the folded list is the card's top and squares off; opened, it is the
   card's foot and rounds both. */
.site-unmonitored {
  margin: 0 calc(var(--card-padding) * -1) -1.5rem !important;
  border-top: 1px solid var(--color-border);
  overflow-wrap: anywhere;

  .list {
    border-bottom-left-radius: var(--card-radius);
  }

  @media (max-width: 58rem) {
    .list {
      border-bottom-left-radius: 0;
    }

    .multiple.picking .list {
      border-radius: 0 0 var(--card-radius) var(--card-radius);
    }
  }
}

/* --- The Site's kind tabs, and the scan strip above them --- */

.resource-section + .resource-section {
  margin-top: 2.5rem;
}

/* The Site scan's strip: when it last looked on the left, what to do about it
   on the right. Wraps to two lines on a narrow viewport rather than squeezing
   the sentence, since the sentence is the half that has to be readable. */
.scan-strip {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem 1rem;

  p {
    flex: 1 1 18rem;
    margin: 0;
  }

  .actions {
    flex: none;
    display: flex;
    align-items: center;
    gap: 0.8rem;
  }

  /* button_to wraps its button in a form, which flex would otherwise stretch. */
  form {
    display: inline-flex;
    margin: 0;
  }
}

.resource-section-heading {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0;
  font-size: 0.95rem;
  font-weight: 700;
}

/* --- A list of rows ---
   Flat, not carded, and no tone stripe: the tab card is the card, and six
   coloured left edges in one list read as a status bar chart nobody asked
   for. The glyph is the status. */

.row-stack {
  display: flex;
  flex-direction: column;
  /* The drawer's contents line up with the ROW's words, not with the row's
     left edge, so a row and the panel it opens have one left margin to read
     down instead of two. The sum is whatever stands in front of the text:
     .row-glyph plus .row-top's gap. The glyph's term is icons.css's
     --row-glyph-size, the prop the mark itself is sized by, so a new mark
     size moves the gutter with it. onboarding.css reads this
     same variable for the same reason -- its screens are .row-stack too. */
  --row-gutter: calc(var(--row-glyph-size) + 0.9rem);
}

.row-stack-banner {
  display: flex;
  gap: 0.6rem;
  margin: 0;
  padding: 0.7rem 0.9rem;
  border-radius: var(--radius-sm);
  background: var(--color-unknown-bg);
  color: var(--color-unknown);
  font-size: 0.85rem;
  line-height: 1.5;
}

.row-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

/* A margin rather than a gap, which .resource-section's own margin would add
   to. Not a dialog, whose centring is its auto margins. */
.row-stack > * + :not(dialog) {
  margin-top: 1rem;
}

/* A list of link rows that starts or ends its card runs to that edge: each
   link's own padding holds its words off the border, as it does off the
   hairlines between them. !important to beat the :first-child and
   :last-child resets above. Not a list inside a drawer, which starts or ends
   the drawer and not the card, nor one nested under a row (rows.css), which
   ends its row. */
.card .row-list:not(.row-drawer .row-list, .row-list--nested):has(> .row > .row-top) {
  &:first-child {
    margin-top: calc(var(--card-padding) / -2) !important;
  }

  &:last-child {
    margin-bottom: calc(var(--card-padding) / -2) !important;
  }
}

.row + .row {
  border-top: 1px solid var(--color-border);
}

/* A row that is one link (reports#show, lifted from the index prototype's
   variant b; .scratch/broken-links-report/issues/06): the link is its
   .row-top. It leads with an icon on a tile (icons.css), then a
   name over one line, the same three columns on a phone with the line
   wrapping, where any other .row-top wraps. The whole row is the link, so it
   reads as text; the caret at its right edge says it opens before anyone
   points, which a phone needs, having no pointer. Its tint under the pointer
   and when pressed are .row--opens', and its name and caret take the accent. */
a.row-top {
  flex-wrap: nowrap;
  gap: 1rem;
  color: inherit;
  text-decoration: none;

  @media (hover: hover) {
    &:hover {
      .row-name,
      .row-right {
        color: var(--color-accent);
      }
    }
  }

  .row-name {
    font-size: 1.1rem;
    color: var(--color-text);
  }

  .row-what {
    font-size: 0.95rem;
  }

  .row-right {
    width: auto;
    color: var(--color-text-muted);
  }
}

/* A live update turned the row to another tone (live_update_shows_change.js):
   its words fade back in as its mark rings (icons.css). */
.row[data-changed] > .row-top {
  animation: live-changed-fade 0.5s ease;
}

@keyframes live-changed-fade {
  from {
    opacity: 0;
    filter: blur(2px);
  }
}

/* Tight is the default, the same step a link row and .row-list's edge take. A
   row that needs other spacing says so on a selector of its own. */
.row-top {
  display: flex;
  align-items: flex-start;
  gap: 0.9rem;
  margin-inline: calc(var(--card-padding) / -2);
  padding: calc(var(--card-padding) / 2);
}

/* A row led by a .row-glyph--tile centres on it: that rule is with the tile, in
   icons.css. */

.row-text {
  flex: 1;
  min-width: 0;
}

/* Wraps anywhere because a name can have no break point at all (an email
   address), and a flex item never shrinks below its longest word: one wider
   than a phone's column pushed the whole page sideways. `anywhere` rather
   than `break-word` because only it lowers the min-content width the flex
   item is sized from (site-services-tab 09). */
.row-name {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 0;
  font-weight: 700;
  overflow-wrap: anywhere;
}

.row-what {
  margin: 0.2rem 0 0;
  color: var(--color-text-muted);
  font-size: 0.9rem;
  line-height: 1.5;
  overflow-wrap: anywhere;
}

.resource-link {
  color: var(--color-accent);
  font-weight: 600;
  text-decoration: none;
}

.resource-link:hover {
  text-decoration: underline;
}

.row-right {
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.2rem;
  text-align: right;
}

.row-cadence {
  font-size: 0.85rem;
  font-weight: 600;
}

.row-checked {
  color: var(--color-text-muted);
  font-size: 0.8rem;
  white-space: nowrap;
}

/* --- A check's row is the control ---
   The whole row opens its drawer, and one underlined word at the bottom right
   of it says so. Everything here is scoped to .row--opens, which
   urls/_source_row sets on a row exactly when it was given a drawer or a link to open,
   and the Dashboard on every row, each of which opens its resource's page, and
   reports#show on every row, each of which opens its report
   (DashboardHelper#dashboard_row_tag): onboarding renders the same .row markup for rows that only report,
   and a row that only reports must not grow a hand cursor.

   SCOPING IT TO .row-stack DID NOT DO THAT, which is what this block used to
   do and what it said it was for. Onboarding's lists are .row-stack as well
   (onboarding/emails/_list), so their report-only rows were taking the hand
   cursor, the hover tint and the press tint from these rules -- while the Form
   step's rows (shared/rows/_found) do open a drawer, and take all three. The condition
   was never "which list is this" -- it is "does this row open", and only the
   row's own markup knows.

   THE HAND CURSOR IS THE DESKTOP HALF OF THE ANSWER -- the oldest "this does
   something" signal a browser has, and it costs nothing. It is only half: a
   phone has no pointer to change, which is why the word is drawn rather than
   implied, and why nothing here relies on the hover tint.

   Folded in from the Checks-tab row interaction prototype (2026-09-18,
   .claude/skills/prototype/UI.md) -- won out over five alternatives and
   today's row across nine rounds, including a hover edge built over three of
   them and retired with the variant it was answering for (don't rebuild it
   for this row); the full set stays on the throwaway branch
   prototype/url-check-row-interaction, and the reasoning is in
   .scratch/url-check-row-interaction/README.md. */

/* On the whole <li>, because .row-top is not the whole of the row: the
   <li> also owns the hairline between rows and the strip beside an open
   drawer, and a cursor that changes over MOST of a row is worse than one that
   never changes. The drawer takes it back -- its contents are things to read
   and type in. */
.row--opens {
  cursor: pointer;
}

.row--opens > .row-drawer {
  cursor: auto;
}

/* ALIGN-ITEMS: STRETCH is what puts the control on the bottom right. The
   columns come to a common height and .row-edit takes what is left
   over as a top margin. Not justify-content: space-between -- that column has
   three children and it would split the two meta lines, which are a pair. */
.row--opens > .row-top {
  align-items: stretch;
  border-radius: var(--radius-sm);
}

/* Both halves hold the same state at the same time, which is what makes them
   one object rather than a row with a thing under it. */
@media (hover: hover) {
  .row--opens:hover > .row-top,
  .row--opens:hover > .row-drawer {
    background: var(--color-bg);
  }

  /* THE VALUE FLIPS BECAUSE THE BACKDROP FLIPS, and that is the part to copy
     rather than the hex. A top-level row sits on --color-surface and separates
     from it by going to --color-bg; a row inside its drawer sits on that
     --color-bg, its parent being hovered too, so it separates by going the other
     way, and a row inside that one flips back. Same two tokens, same single
     step, direction set by how deep it sits, whichever kind of row it is.
     :active is left alone: a press is not a backdrop. */
  .row-drawer .row--opens:hover > .row-top,
  .row-drawer .row--opens:hover > .row-drawer {
    background: var(--color-surface);
  }

  .row-drawer .row-drawer .row--opens:hover > .row-top,
  .row-drawer .row-drawer .row--opens:hover > .row-drawer {
    background: var(--color-bg);
  }
}

.row--opens > .row-top:active {
  background: var(--color-accent-bg);
}

/* The seam is the drawer's own (rows.css). The radius flattens
   while the drawer is there, or .row-top's rounded bottom corners curve away
   from its straight top. Read off the row's OWN drawer: a row inside it may
   stay open after this one shuts. */
.row--opens > .row-top:has(+ .row-drawer:not([hidden])) {
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
}

/* --- The row's one control ---
   A real <button> wearing a link's clothes: the semantics belong to the
   disclosure it drives, the appearance to the thing people have been clicking
   since before the rest of this vocabulary existed.

   margin-top: auto is the bottom right (see .row-top above). Nothing
   else may set a margin-top on this element -- a rule that only wins by being
   later is a rule somebody has to measure. */

.row-edit {
  appearance: none;
  display: inline-flex;
  align-items: center;
  margin-top: auto;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--color-accent-dark);
  text-decoration: underline;
  text-underline-offset: 0.18em;
  cursor: pointer;
}

.row-edit:hover {
  color: var(--color-accent);
}

/* Both faces ship in the DOM and this picks one off the button's own state,
   so the swap is aria-expanded and no JavaScript knows about it. The caret
   FLIPS rather than rotating: a rotate(180deg) reads as the caret turning,
   when what it has to say is that the row's state is now the other one. */
.row-edit-face {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
}

.row-edit-face--close,
.row-edit[aria-expanded="true"] .row-edit-face--open {
  display: none;
}

.row-edit[aria-expanded="true"] .row-edit-face--close {
  display: inline-flex;
}

.row-fields,
.row-values {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: 0.9rem 1.5rem;
  margin: 0;
}

.row-values dt {
  font-size: 0.8rem;
  font-weight: 600;
}

.row-values dd {
  margin: 0.2rem 0;
  font-size: 0.9rem;
}

/* --- A COMPACT ROW: a reading inside a summarising row's drawer ---
   urls/_source_row's `compact: true`, and the whole of what that word buys.

   An `email_working` row's drawer holds FOUR lines, each with three controls
   of its own, and stacking all of that gave one address ~2100px of open
   drawer. Chris, on the drawing: "having the sub-concerns all expanded makes
   it feel overwhelming to the user." So a reading takes the shape a CLOSED
   top-level row has -- glyph, words, and a tail on the right -- and puts its
   controls behind a disclosure of its own. ~1300px, and from ~2.5 phone
   screens to well under 2.

   ONE STATEMENT, AND ONLY ONE: it is smaller. Everything else a reading does
   is .row's own and reaches it because a reading IS one -- the hairline
   between rows, the hand cursor, the hover, the tail's column, the control at
   the corner, the seam once it opens, the phone unwrap, the short last-checked
   swap. It used to restate all of that
   in a parallel .resource-found-check-* family over the shared .row-* inner
   pieces, which left one reading styled as both a row and a found entry.

   Drawn and chosen as variant A1
   (.scratch/url-show-email-concerns/issues/10-the-email-rows-anatomy.md), and
   converged onto the skeleton it was drawn from by
   .scratch/concern-row-consolidation's ticket 10. */

/* SMALLER IS ONE NUMBER PER PIECE, and the indents are derived rather than
   restated: --row-gutter is "whatever stands in front of the text",
   which here is this glyph plus this gap, so the nested drawer's left edge
   and the phone tail's follow the two values above them instead of being
   measured again. Each number is a property here, read by both places a
   row is compact, because a class can't be set at a width. The glyph's own
   size is with the other glyph sizes (icons.css). */
:root {
  --row-compact-gap: 0.7rem;
  --row-compact-padding: 0.5rem;
  --row-compact-name-size: 0.9rem;
  --row-compact-right-gap: 0.1rem;
}

.row--compact {
  --row-gutter: calc(var(--row-glyph-size) + var(--row-compact-gap));
}

.row--compact .row-top {
  gap: var(--row-compact-gap);
  padding-top: var(--row-compact-padding);
  padding-bottom: var(--row-compact-padding);
}

.row--compact .row-name {
  font-size: var(--row-compact-name-size);
}

.row--compact .row-right {
  gap: var(--row-compact-right-gap);
}

/* A PHONE DRAWS EVERY ROW INSIDE A DRAWER COMPACT, since its drawer no
   longer indents (rows.css, .row-drawer) and a full-size row read as the
   one that opened it. */
@media (max-width: 40rem) {
  .row-drawer .row {
    --row-gutter: calc(var(--row-glyph-size) + var(--row-compact-gap));

    .row-top {
      gap: var(--row-compact-gap);
      padding-top: var(--row-compact-padding);
      padding-bottom: var(--row-compact-padding);
    }

    .row-name {
      font-size: var(--row-compact-name-size);
    }

    .row-right {
      gap: var(--row-compact-right-gap);
    }
  }
}

/* Width only: border, padding, chevron and focus ring come from the app-wide
   `select` rule. Restyling it here once wiped that chevron (the `background`
   shorthand resets background-image) and left a plain box. */
.row-select,
.row-fields input[type="text"] {
  width: 100%;
  max-width: 20rem;
}

/* --- The Mailbox Connection band (Emails#show's More card) --- */

/* A band under BOTH columns of the More card rather than a row inside one.
   Emails#show is the only page with one today: every other fact in that card
   was measured and can only be read, and this one was granted -- the grant
   expires, gets revoked, and follows a person out of the company. It is the
   one thing in there a User can act on, so it holds the card's only button.

   The tone paints a left edge only. This is a fact about the resource, not an
   alarm; the action item card is where alarms belong, and a band that filled
   with red inside a popover would be competing with it from behind a click. */
.mailbox-connection {
  margin-top: 1.5rem;
  padding: 1rem;
  border: 1px solid var(--color-border);
  border-left: 3px solid var(--tone, var(--color-border));
  border-radius: var(--radius);
  background: var(--color-bg);
}

.mailbox-connection-head {
  display: flex;
  align-items: center;
  gap: 0.6rem;
}

/* Pushed to the far edge -- it is the only button in the card, and it should
   read as the card's one affordance rather than as part of the heading. */
.mailbox-connection-head .button {
  margin-left: auto;
  flex-shrink: 0;
}

.mailbox-connection-heading {
  margin: 0;
  font-size: 0.95rem;
  color: var(--color-warm-heading);
}

.mailbox-connection-facts {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.5rem;
  margin: 0.9rem 0 0;
}

.mailbox-connection-facts dd {
  margin: 0.1rem 0;
  font-size: 0.85rem;
}

.mailbox-connection > .note {
  margin-top: 0.8rem;
}

/* An address has no break point, and a flex item never shrinks below its
   longest word. */
.mailbox-connection-head .note {
  overflow-wrap: anywhere;
}

@media (max-width: 40rem) {
  .mailbox-connection-head {
    flex-wrap: wrap;
  }

  .mailbox-connection-head .button {
    margin-left: 0;
    width: 100%;
    text-align: center;
  }
}

/* --- The delivery record (Emails#show's Deliverability tab) --- */

/* The one panel in the app written in the past tense. Nothing here is a card:
   the tab card around it is the boundary, same as the Summary panel's rows. */
.resource-record-stat {
  margin: 0;
  font-size: 1.1rem;
  color: var(--color-warm-heading);
}

.resource-record-trend {
  margin: 0.2rem 0 0;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--tone, var(--color-text-muted));
}

/* Left to right is time passing. The strip wraps rather than scrolling, so a
   narrow screen gets two short rows of days instead of a horizontal scroller
   nobody discovers. */
.resource-record-strip {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin: 0.9rem 0;
  padding: 0;
}

.resource-run {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.3rem;
  min-width: 3.1rem;
  padding: 0.5rem 0.4rem;
  border-radius: var(--radius);
  border: 1px solid var(--color-border);
}

/* The tone lands on the GLYPH alone, not on the tile: the day underneath
   stays muted, so ten tiles read as ten days with a mark on each rather than
   as ten blocks of colour. The tile carries status-tone--*, which is what
   exports the two tokens -- one rule rather than four, because that pair is
   exactly what those classes exist to hand down. */
.resource-run-glyph {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.35rem;
  height: 1.35rem;
  border-radius: 999px;
  font-size: 0.8rem;
  font-weight: 700;
  background: var(--tone-bg, var(--color-bg));
  color: var(--tone, var(--color-text-muted));
}

.resource-run-day {
  font-size: 0.7rem;
  color: var(--color-text-muted);
  white-space: nowrap;
}

.resource-record-counts {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 0.9rem 0;
}

.resource-record-count {
  flex: 1 1 6rem;
  padding: 0.6rem 0.7rem;
  border-radius: var(--radius);
  border: 1px solid var(--color-border);
  background: var(--tone-bg, var(--color-bg));
}

/* Its dt is the Mailbox band's rule, above. */
.resource-record-count dd {
  margin: 0.1rem 0;
  font-size: 1.3rem;
  font-weight: 700;
  color: var(--tone, var(--color-warm-heading));
}

.resource-record-runs {
  list-style: none;
  margin: 1rem 0;
  padding: 0;
}

.resource-record-run {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.6rem 0;
  font-size: 0.9rem;
}

.resource-record-run + .resource-record-run {
  border-top: 1px solid var(--color-border);
}

.resource-record-run-when {
  min-width: 7.5rem;
  color: var(--color-text-muted);
}

@media (max-width: 40rem) {
  .resource-record-run {
    flex-wrap: wrap;
  }

  .resource-record-run-when {
    min-width: 0;
  }
}

/* --- Band 4: what is underneath --- */

@media (max-width: 52rem) {
  .row-top {
    flex-wrap: wrap;
  }

  .row-right {
    align-items: flex-start;
    text-align: left;
    width: 100%;
  }

  /* --- A check row on a phone ---
     Wrapped, .row-right starts at the ROW's left edge -- under the
     status glyph rather than under the words -- so a phone row carried two
     left margins, which is exactly what --row-gutter takes out of the
     drawer. Same variable, same reason: the words, the subline and the drawer
     all start at one margin.

     Left as a row rather than restacked: three short facts on one line is one
     line, and the alternative is three.

     THIS IS THE ONLY COPY. onboarding.css carried the same rules again, keyed
     to onboarding's two variants and starting at 40rem instead
     of 52rem -- written before the Summary list had a phone treatment at all,
     and left behind once it did. Its screens are .row-stack like any other
     list, so these reach them; the two values that differed there (width:auto
     and a 0.3rem column gap) now match the Summary list, which is the whole
     point of the rows being one shape. */
  /* Order, so a caret keeps the first line, beside the words. */
  .row-stack .row-right {
    order: 1;
    flex-direction: row;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.3rem 0.6rem;
    margin-top: 0.2rem;
    padding-left: var(--row-gutter);
  }

  /* A row that opens keeps its caret on the first line, beside the words, as
     .row-stack does above: the pill and buttons wrap under them, and left in
     source order the caret would follow them onto a line of its own. */
  .row--opens > .row-top > :is(.row-right, .row-acts) {
    order: 1;
  }

  /* The pill takes the whole first line, and the cadence keeps the second
     one with the last-checked. Left to wrap on its own the break moved
     with the WORDING -- "Online" leaves room for a cadence beside it,
     "Paused until 4:00 PM" does not -- so one row broke in two different
     places depending on what it had to say, and a list of them stepped in and
     out of alignment.

     THE WRAPPER IS WHY THE PILL HAS ONE. flex-basis: 100% on the pill itself
     would stretch the chip across the row, and clamping that back with
     max-content undoes the break as well as the width: a flex item's max-width
     is applied to its hypothetical main size, which is the size the container
     breaks lines on. So the full-width thing and the chip have to be two
     elements. The wrapper takes the line, the pill keeps its own width on it. */
  .row-stack .row-pill {
    flex-basis: 100%;
  }

  /* "every 6 hours · 🕐 about 2 hours ago" -- a subline, not a column that
     lost its column. NOTHING NEW IS RENDERED: the short string is already on
     the element, from the shipped helper, and has just never been shown
     here. */
  .row-stack .row-cadence {
    font-size: 0.8rem;
  }

  /* The separator is between two facts, so it is drawn only when the second
     one is there. `last_run:` became optional (onboarding keeps the cadence
     and drops the last-checked), and without :has this printed a "·" with
     nothing after it. Keyed on the row's own markup rather than on which
     list it is in -- same rule as .row--opens. */
  .row-stack .row-cadence:has(+ .row-checked)::after {
    content: "·";
    margin-left: 0.3rem;
    font-weight: 400;
    color: var(--color-text-muted);
  }

  /* No clock beside the short form: the clock is History's alone
     (iconography.md I1 -- One meaning per icon, ticket 110). */
  .row-stack .row-checked[data-checked-short] {
    font-size: 0; /* the shipped phrase; ::after prints the short one */
  }

  .row-stack .row-checked[data-checked-short]::after {
    content: attr(data-checked-short);
    font-size: 0.8rem;
  }

  /* The control keeps the far end of that line, which is the corner it holds
     at every width.

     A BUTTON'S TAP TARGET CAN BE ITS SHAPE; A LINK'S HAS TO BE INVISIBLE. A
     link centered in a 44px box is not a link any more -- it is a button with
     the border left off, and being a link is the whole of why this is one. So
     the ink keeps the subline's rhythm and the HIT AREA grows past it:
     padding out, the same padding straight back off as margin. It overlaps
     the line above and the row's bottom padding, and both of those open the
     same drawer, so there is nothing there for it to steal.

     2.2rem below is the 44px both platform guidelines have asked for for a
     decade: A REM IS 20px HERE, because :root is 125%. (Inside this query it
     is 16px -- a media query resolves against the initial font size, not the
     root's. Same unit, two meanings.) */
  .row-stack .row-edit {
    padding: 0.6rem 0.4rem;
    margin: -0.6rem -0.4rem -0.6rem auto;
  }

  /* ONE COLUMN, ALWAYS. repeat(auto-fit, minmax(15rem, 1fr)) does not shrink
     below its floor -- it OVERFLOWS -- and 15rem is 300px at this root, which
     is more than a 390px screen leaves inside an open drawer. A phone has one
     column anyway.

     NO RULE FOR THE FIELDS THEMSELVES, and the reason is worth leaving here:
     iOS Safari zooms the page when a field under 16px takes focus and does
     not zoom back out. A dense field is 0.9rem and the select 0.95rem -- 18px
     and 19px at this root, both clear of it. The 125% root is what saves
     them, so anyone who normalises it has to come back here. */
  .row-fields,
  .row-values {
    grid-template-columns: 1fr;
  }
}

/* 640px -- the shell's gutter is --app-gutter (0.9rem) here rather than the
   2rem .app-main spends at width, so the What-we-know card has to bleed less
   or it hangs off the side of the screen and pulls a scrollbar with it. Less
   bleed, same cancellation: whatever padding is added is still taken straight
   back off as margin, so the header still does not move. The panel needs no
   rule of its own -- it is pinned to the header's own edges. */
@media (max-width: 40rem) {
  .resource-header:has(.resource-facts[open]) {
    margin-left: calc(-0.8rem - 1px);
    margin-right: calc(-0.8rem - 1px);
    padding-left: 0.8rem;
    padding-right: 0.8rem;
  }
}

/* Country flags for the phone field's country picker.
   ATTRIBUTION IS A CONDITION OF THE LICENCE -- keep this notice with the rule.

   TwemojiCountryFlags.woff2 is a 78KB subset of Mozilla's Twemoji Mozilla
   build, taken from the country-flag-emoji-polyfill package (MIT). The
   artwork is Twemoji, (c) Twitter, Inc and other contributors, licensed
   CC-BY 4.0.

   Chromium on Windows renders no country flags at all and has said it does
   not plan to, so without this the picker shows bare letters there and a flag
   everywhere else. Shipping the font rather than detecting the gap also makes
   the flag the SAME artwork on every platform, instead of Apple's on a Mac
   and Twemoji's on Windows.

   unicode-range confines the face to the regional-indicator block, so a
   browser only downloads it on a page that actually paints one -- a page with
   no phone field on it never fetches the font. */
@font-face {
  font-family: "Twemoji Country Flags";
  src: url("/assets/TwemojiCountryFlags-468538bb.woff2") format("woff2");
  unicode-range: U+1F1E6-1F1FF;
  font-display: swap;
}
