@layer base {
  /* Chalet's touch-and-chrome plumbing (sweep, 2026-08-24): controls answer
     the first tap (no double-tap-zoom wait); rotating a phone does not
     inflate the type the layout was measured in; a long press on a face or
     a thumbnail asks nothing about saving images; and every scrollbar in
     the app is the thin one, in the hairline's ink. */
  html {
    -webkit-text-size-adjust: none;
    text-size-adjust: none;
    scrollbar-width: thin;
    scrollbar-color: var(--color-line) transparent;
  }

  :is(button, input, textarea, select, summary, .btn, .input) {
    touch-action: manipulation;
  }

  :is(.btn, .avatar, .message__media) img {
    -webkit-touch-callout: none;
    user-select: none;
  }

  html {
    /* 16px root everywhere, deliberately: Fizzy bumps this to 19px past 100ch
       and every rem in the app inflates with it. Ours stays put and the type
       scale is fluid instead (_global.css), so spacing, bar heights and the
       panel keep the measurements the rest of this file assumes. */
    font-size: 100%;
    interpolate-size: allow-keywords;
  }

  body {
    background-color: var(--color-canvas);
    color: var(--color-ink);
    font-family: var(--font-sans);
    font-size: var(--text-normal);
    line-height: 1.45;
    min-block-size: 100dvh;
  }

  a {
    color: var(--color-link);
    text-decoration-thickness: 1px;
    text-underline-offset: 0.15em;

    &:hover {
      text-decoration-thickness: 2px;
    }
  }

  h1,
  h2,
  h3,
  h4 {
    line-height: 1.2;
    text-wrap: balance;
  }

  /* The UA's h1 is 2em/700 — the one type size in the app nobody chose, and
     outside the six steps HANSHI allows. x-large at 600 is the step the scale
     already has for a page title (user, 2026-08-29, on the message board's
     header). Two surfaces wear an h1's metrics so that editing a title in
     place moves nothing: the rename input (inline-rename.css) and the todo
     headline composer (todo-form.css). All three move together or none do. */
  h1 {
    font-size: var(--text-x-large);
    font-weight: 600;
  }

  small {
    font-size: var(--text-small);
  }

  code,
  kbd,
  pre {
    font-family: var(--font-mono);
    font-size: 0.9em;
  }

  /* Cable subscriptions are plumbing with no box to draw, but as a bare
     custom element the UA gives them `inline` — and wherever one lands in a
     grid or flex parent it steals a cell or a gap slot. The footer grid lost
     its center column to one. Fizzy says the same in its base.css. */
  turbo-cable-stream-source {
    display: contents;
  }

  /* Scoped to things that are actually controls, not `:focus-visible` bare.
     A universal rule also matches contenteditable regions, which meant the
     embedded Lexxy editor got a shu ring painted inside its writing area and
     every composer had to suppress it and re-add one on a wrapper. Fizzy
     scopes the same way; Basecamp does it per component. Anything focusable we
     add later — a menu item, a tree node — opts in by name rather than
     inheriting a ring it then has to fight.

     `:not([contenteditable])` is the other half of that scoping, and it was
     missing: Lexxy stamps `tabindex="0"` onto its writing div at runtime, so
     `[tabindex]` caught the very element the paragraph above says to leave
     alone — a shu rectangle around the text with the toolbar stranded outside
     it, inside the ring the composer had already drawn (user, 2026-08-07).
     The gem's own `:where(.lexxy-editor__content) { outline: 0 }` lives in
     this same layer and loses on specificity, so it cannot save us.

     `[tabindex="-1"]` is excluded for a plainer reason: it means "not reachable
     by the keyboard", so a ring there can never have been earned by keyboard
     navigation. Those elements exist only to catch focus we move ourselves — a
     landing, not a stop — and a shu rectangle appearing around a whole section
     the instant a menu opens is the accent spent on nothing. That is the same
     ruling palette.css already made for the field the palette opens on; this
     is it said once, for every landing rather than per component. Fizzy reaches
     the same place from the other end, with `&[tabindex="-1"]:focus-visible {
     outline: unset }` written out in popup.css and again in lightbox.css. */
  :is(a, button, input, textarea, select, summary, [tabindex], .btn, .input):not([contenteditable], [tabindex="-1"]) {
    &:where(:focus-visible) {
      outline: var(--focus-ring);
      outline-offset: var(--focus-offset);
    }
  }

  ::selection {
    background-color: var(--color-selected);
  }

  /* The flash pill is exempt: its appear-then-fade is a pure opacity change —
     reduced motion asks for stillness, not for silence — and its end is what
     takes the pill off the page. Stilled, it either lingered over the sheet
     swallowing clicks or needed a JS clock standing in for the animation's.
     (Fizzy avoids the whole class of problem by stilling with
     `animation-duration: 0.01ms` instead of `none`, so animationend always
     fires; if a second animationend consumer ever appears, switch to that
     idiom rather than adding a second exemption.) */
  @media (prefers-reduced-motion: reduce) {
    *:not(.flash),
    *::before,
    *::after {
      animation: none !important;
      transition: none !important;
    }
  }
}
