Ctrl K
ComunidadGuías…

app/globals.css

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
@import "tailwindcss";
@import "./tokens.css";
/* Per-mode palettes chosen on the account (MIN-696). Their selectors outweigh
   the mode overrides below, so the import can stay with the others. */
@import "./theme-presets.css";
@import "streamdown/styles.css";
/* KaTeX, for the editor's equations (components/pages/blocks/math.ts). Tailwind
   inlines it and rebases its font URLs onto the package. */
@import "katex/dist/katex.min.css";
/* Streamdown (Numo markdown rendering): without these @sources, Tailwind does not generate
   classes used by components and everything renders in unstyled boxes. */
@source "../node_modules/streamdown/dist/*.js";
@source "../node_modules/@streamdown/code/dist/*.js";

/* App chrome follows the shared runtime policy, including tablet orientation.
   Ordinary content grids keep Tailwind's independent width breakpoints. */
@theme {
  --breakpoint-desktop: 1024px;
  --breakpoint-wide: 1200px;
}
@custom-variant app-mobile (&:where(html[data-app-layout="mobile"] *));
@custom-variant app-desktop (&:where(html[data-app-layout="desktop"] *));

/* Hover belongs to a fine pointer that hovers (MIN-836). A finger has no
   hover: the browser fakes one on tap and keeps it until the next tap
   elsewhere, so a pressed control looks selected. Tailwind's own `hover:`
   only checks `(hover: hover)`, which some phones and tablets report with a
   coarse primary pointer; this one also requires `(pointer: fine)`. Every
   compound follows it (`group-hover:`, `peer-hover:`, `data-*:hover:`).
   `touch:` is the exact complement: a control revealed on hover
   (`opacity-0 group-hover:opacity-100`) adds `touch:opacity-100` so it is
   always shown where nothing can hover. Hand-written `:hover` rules in the
   stylesheets sit inside the same media query. */
@custom-variant hover {
  @media (hover: hover) and (pointer: fine) {
    &:hover {
      @slot;
    }
  }
}
@custom-variant touch {
  @media (hover: none), (pointer: coarse) {
    @slot;
  }
}

/* One hover cursor for the whole app (MIN-587): buttons and every other
   interactive element show the default arrow, never the pointer hand. The
   adapted cursor still wins where it carries meaning — text fields, drag
   handles, zoom, disabled controls — through the `:not()` exemptions: an
   element carrying one of those utilities is simply left alone.
   Unlayered on purpose: cascade layers place Tailwind utilities below this
   rule, so a stray `cursor-pointer` can never reintroduce the hand. */
:where(
    button:not(:disabled),
    a,
    summary,
    label,
    [role="button"],
    [role="option"],
    [role="menuitem"],
    [role="menuitemradio"],
    [role="menuitemcheckbox"],
    [role="radio"],
    [role="checkbox"],
    [role="switch"],
    [role="tab"],
    [role="treeitem"]
  ):not(
    .cursor-grab,
    .cursor-grabbing,
    .cursor-text,
    .cursor-zoom-in,
    .cursor-zoom-out,
    .cursor-not-allowed
  ) {
  cursor: default;
}

/* Dependency chrome can retain compiled width utilities. Match its shell
   wrappers to the same layout attribute as the application components. */
:root[data-app-layout="desktop"] {
  .app-shell main { padding-bottom: 0; }

  .app-shell .desktop\:flex {
    display: flex !important;
  }

  .app-shell .desktop\:hidden {
    display: none !important;
  }

  /* The sidebar container is itself a direct child of the shell: it is not
     covered by the descending selector above. */
  .app-shell > .desktop\:flex {
    display: flex !important;
  }
}

:root[data-app-layout="mobile"] {
  .app-shell .desktop\:flex { display: none !important; }
  .app-shell .desktop\:hidden { display: flex !important; }
}

/* In the Electron app, the fixed strip that allows you to move the window covers
   the first 44 pixels. When the compact navigation panel
   navigation compact is stored, its edge must pass ABOVE this strip
   and become a web zone again; otherwise a hover of the top-left corner cannot
   never recall navigation. Once opened, it goes back under the panel
   itself and does not block any controls. */
/* The auth screens' brand clears the macOS window buttons (MIN-781): top
   left, centred on the buttons' row (trafficLightPosition 15,15; the group of
   three 14px buttons spans about 15-75px across and is centred at 22px down)
   with the breathing room of a toolbar item after them, at 96px. The column's
   32px padding is the reference. Read from the attribute
   the pre-paint script sets, so it is placed once. The desktop app's bundled
   self-hosting page (`components/desktop/server-choice.css`) repeats this
   rule, so the mark stays put between the two. */
html[data-desktop-platform="darwin"] .auth-logo-row {
  margin-top: -24px;
  padding-left: 64px;
}

/* The auth screens' first appearance (MIN-781): the form's blocks (heading,
   providers, separator, fields, button, footer link) arrive one after the
   other, and the showcase pane eases in from the side behind them. The
   landing's curve; `backwards` only, so nothing keeps a filter afterwards
   (see `.reveal`). Stopped by `prefers-reduced-motion`. */
@keyframes auth-rise {
  from {
    opacity: 0;
    transform: translateY(10px);
    filter: blur(6px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}
@keyframes auth-pane-in {
  from {
    opacity: 0;
    transform: translateX(24px) scale(0.985);
    filter: blur(8px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}
.auth-stagger > * > * {
  animation: auth-rise 0.6s cubic-bezier(0.16, 1, 0.3, 1) backwards;
}
.auth-stagger > * > :nth-child(2) { animation-delay: 0.06s; }
.auth-stagger > * > :nth-child(3) { animation-delay: 0.12s; }
.auth-stagger > * > :nth-child(4) { animation-delay: 0.18s; }
.auth-stagger > * > :nth-child(5) { animation-delay: 0.24s; }
.auth-stagger > * > :nth-child(6) { animation-delay: 0.3s; }
.auth-stagger > * > :nth-child(n + 7) { animation-delay: 0.36s; }
.auth-showcase-enter {
  animation: auth-pane-in 0.9s cubic-bezier(0.16, 1, 0.3, 1) 0.1s backwards;
}
@media (prefers-reduced-motion: reduce) {
  .auth-stagger > * > *,
  .auth-showcase-enter {
    animation: none;
  }
}

html[data-desktop-app] .sidebar-nav-hotzone {
  -webkit-app-region: no-drag;
}

/* Without the primary sidebar, a narrow macOS window still needs one native
   titlebar-height clearance so the traffic lights do not cover page content.
   Other platforms and every desktop-width layout reserve no header space. */
.compact-window-controls-clearance {
  display: none;
}
:root[data-app-layout="mobile"] {
  &[data-desktop-platform="darwin"] .compact-window-controls-clearance {
    display: flex;
  }
}

/* Keyboard keys need fonts that DRAW the shortcut glyphs (⌘ ⌥ ⇧ ⌃ ⏎ live in
   the Miscellaneous Technical block, U+2300–23FF) — Inter, the app's sans,
   has none of them, so every modifier fell back to an arbitrary font with
   misaligned metrics. Route keys to the fonts that design these glyphs
   natively: SF Pro on macOS (what system menus and Raycast draw), Segoe UI
   Symbol on Windows, Noto Sans Symbols 2 elsewhere. */
:root {
  --font-kbd: -apple-system, BlinkMacSystemFont, "Segoe UI Symbol",
    "Noto Sans Symbols 2", sans-serif;
}

/* ─── Floating surfaces ────────────────────────────────────────────────
   Popovers, menus, selects and custom suggestion lists are portaled or
   absolutely positioned, so their stacking context must be independent from
   the content that opened them. Keep dialogs and modals out of this group:
   their opaque card surfaces and their own z-index remain unchanged.

   The opacity is intentionally high enough to hide text behind a menu while
   still letting the surrounding surface show through. The stronger blur gives
   the translucent surface its separation without turning every menu into a
   heavy glass panel. The filter is limited to these short-lived surfaces and
   does not use `will-change`, which would keep large compositor layers alive.

   One system for primitives and app surfaces alike (`[data-floating-surface]`,
   `[role="menu"]`), which is why it lives here and not in each primitive; the
   palettes of app/theme-presets.css override it for transparency off and high
   contrast. */
:is(
  [data-slot="popover-content"],
  [data-slot="dropdown-menu-content"],
  [data-slot="dropdown-menu-sub-content"],
  [data-slot="select-content"],
  [data-floating-surface],
  [role="menu"]
) {
  background-color: color-mix(in oklab, var(--popover) 84%, transparent);
  -webkit-backdrop-filter: blur(16px);
  backdrop-filter: blur(16px);
  isolation: isolate;
}

/* Elevate floating surfaces above their surrounding content. Local panels can
   opt out when they intentionally sit below a confirmation dialog. */
:is(
  [data-slot="popover-content"],
  [data-slot="dropdown-menu-content"],
  [data-slot="dropdown-menu-sub-content"],
  [data-slot="select-content"],
  [data-floating-surface]:not([data-floating-surface-layer="local"]),
  [role="menu"]
) {
  z-index: 1000 !important;
}

/* A confirmation opened from a floating surface (the inbox popover's "Delete
   read") covers it, scrim included. Opt-in, through `overFloatingSurface` on
   the alert dialog: other dialogs keep their own z-index. */
:is([data-slot="alert-dialog-overlay"], [data-slot="alert-dialog-content"])[data-layer="over-floating"] {
  z-index: 1001 !important;
}

/* A hover card explains a row of the surface it opens from (a relation in a
   card's popover, a sub-issue in a menu): above the floating surfaces lifted
   to 1000 and a confirmation over them (1001), below tooltips (MIN-909). */
[data-slot="hover-card-content"] {
  z-index: 1050 !important;
}

/* Tooltips float above every elevated surface, not only the page content:
   inside the inbox popover the icon-button tooltips (settings, delete, mark as
   read) open over the popover's own surface, which the rule above lifts to
   1000. The tooltip keeps its old role of sitting above dialogs too. */
[data-slot="tooltip-content"] {
  z-index: 1100 !important;
}

/* A searchable command list lives inside its popover. Let the parent surface
   carry the glass treatment so the command root does not paint an opaque box
   over it. CommandDialog remains opaque because it is not inside one of these
   floating surfaces. */
:is(
  [data-slot="popover-content"],
  [data-slot="dropdown-menu-content"],
  [data-slot="dropdown-menu-sub-content"],
  [data-slot="select-content"],
  [data-floating-surface],
  [role="menu"]
) [data-slot="command"] {
  background-color: transparent;
}

/* App-authored Markdown uses the shared enriched link treatment. Forge-authored
   pull request content opts out so linked badges and image buttons stay intact. */
a.markdown-link {
  color: #0085FF;
  text-decoration: none;
}

a.markdown-link:focus-visible {
  color: #0085FF;
  text-decoration: none;
}
@media (hover: hover) and (pointer: fine) {
  a.markdown-link:hover {
    color: #0085FF;
    text-decoration: none;
  }
}

/* The site's icon is glued to the link's first word: a line never ends on the
   icon alone with the name on the next line. An `inline-block` is a break
   opportunity on both sides, so the icon is instead an INLINE box holding a
   no-break space (U+00A0), which allows a break before it (after the space
   that precedes the link) and never after it. The image sits in the box's
   start padding; the no-break space itself is the gap before the text. Its
   center matches the former 0.9em block lowered by 0.08em. */
a.markdown-link::before {
  content: "\a0";
  padding-inline-start: 0.9em;
  border-radius: 2px;
  background-image: var(
    --markdown-link-icon,
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%230085FF' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpath d='M2 12h20M12 2a15.3 15.3 0 0 1 0 20M12 2a15.3 15.3 0 0 0 0 20'/%3E%3C/svg%3E")
  );
  background-position: left center;
  background-repeat: no-repeat;
  background-size: 0.9em 0.9em;
}

/* Public guide citations render the matching article icon inside the link. */
a.markdown-link[data-documentation-citation]::before {
  display: none;
}

/* ── Inter arrows, which Google does not serve ──────────────────────────────
   `next/font/google` only loads from Inter the subsets published by
   Google, and NONE covers the arrow block: the Latin range puts “↑” there
   (U+2191) and “↓” (U+2193), but not “→” (U+2192) or “↔” (U+2194). THE
   next link in the stack is "Inter Fallback", which next/font defines as
   `local(Arial)` with a `size-adjust: 107.12%` — each arrow was therefore
   drawn by Arial, enlarged by 7%: 0.99 em of line, compared to 0.73 em for the
   real arrow of Inter. Hence the arrow which is too long and foreign to the text which
   surrounds it: that which poses “->” in the editors
   (components/editor-arrows.ts) like those written hard in the UI.
   Measured in the browser, at 16 px: 17.14 px in advance before, 15.22 px after.

   This @font-face has the SAME last name as next/font — `Inter`,
   real name and not hashed, since it is a Google font. Faces of the same
   family are added by character range: it fills the gap without
   that no font stack has to move, and the inheritance of the body like the
   utilitaires `font-sans` / `font-display` / `font-mono` en profitent d'un coup.

   The file is a subset of InterVariable 4.001 (rsms.me/inter, license
   OFL in app/fonts/), reduced to the two arrow ranges — 8 KB, 34 characters:

     pyftsubset InterVariable.woff2 --unicodes="U+2190-21FF,U+27F0-27FF" \
       --flavor=woff2 --layout-features='' --no-hinting \
       --output-file=app/fonts/inter-arrows.woff2

   It covers wider than just the “→”, and it is the same fault everywhere:
   “↗ ↳ ↵ ⇒ ⇧ ⟷” were not in ANY Inter side and fell to the
   system font (SF here, Segoe UI Symbol on Windows) — the system fonts
   palette shortcut are one of them. What Inter doesn't draw either
   (“⇄”, U+21C4) falls as before. */
@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("./fonts/inter-arrows.woff2") format("woff2");
  unicode-range: U+2190-21FF, U+27F0-27FF;
}

/* ── The Mention Pill (components/mention-chip.tsx) ───────────────────────
   It is TINTED BY TYPE: the background and the text are the same rested color
   with two lights, the one that sinks into the page and the one that can be read on it.
   The pill derives the three variables below from ITS hue, in syntax of
   relative color — hence CLARITIES and OPACITIES here, and not colors:
   the color comes from the type (`mentionTone`), and for an objective or a
   project it is a color that no style sheet knows in advance.

   These clarity cannot be deduced from one another from one theme to another:
   on a light background a pill darkens to read, on a dark background it
   clears up. Two games, therefore, and without arithmetical connection.

   The background remains TRANSLUCENT on both sides, and not a solid color: the
   pill can be placed on a comment card as well as on a line
   hovered over or on the surface of the Numo composer, and must take the shade of
   what carries it. Only one variable per role, therefore only one class `bg-*` /
   `text-*`: a caller who repaints the pill (the cat's dark bubble)
   remplace proprement via tailwind-merge.

   The `-hover` variant is for pills that LEAD somewhere (a ticket, a
   objective, a page — never a person, cf. lib/mention-target.ts): the same
   bottom, pressed a notch, so that the hover says “this clicks” without the
   pilule change de nature.

   The values ​​of `--mention-chip` and `--mention-chip-hover` set here are used
   of REPLI: they paint what borrows the geometry of the pill without
   be a — the pill of a command “/” from the Numo composer. */
:root {
  --mention-chip: oklch(0.21 0.012 265 / 6%); --mention-chip-hover: oklch(0.21 0.012 265 / 12%);
  --mention-chip-l: 0.62; --mention-chip-a: 13%; --mention-chip-a-hover: 22%; --mention-chip-ink-l: 0.47;
}
.dark {
  --mention-chip: oklch(0 0 0 / 25%); --mention-chip-hover: oklch(0 0 0 / 40%);
  --mention-chip-l: 0.72; --mention-chip-a: 18%; --mention-chip-a-hover: 28%; --mention-chip-ink-l: 0.84;
}

/* A pill placed on an INK surface rather than on the page: the bubble
   of a message sent, in the Numo composer as in the agent's thread.
   It keeps ITS color - it is indeed the same ticket, the same person as elsewhere
   — but takes it back to the clarity of the other theme: the bubble is in --foreground,
   so dark when the page is light, and light when the page is dark.
   Hence the inversion, and not a separate set of values. To place on the SURFACE,
   not on the pill: it applies to all those it carries. */
.mention-on-ink {
  --mention-chip-l: 0.8; --mention-chip-a: 22%; --mention-chip-a-hover: 32%; --mention-chip-ink-l: 0.88;
}
.dark .mention-on-ink {
  --mention-chip-l: 0.58; --mention-chip-a: 16%; --mention-chip-a-hover: 26%; --mention-chip-ink-l: 0.45;
}

/* A pill placed on a TINTED surface: the bubble of a message sent to Numo,
   in the brand's lavender. The pill's translucent tint would mix with it —
   a person's purple or an issue's blue all but vanish on lavender — so it is
   laid over the page's own background first, and reads exactly as it does
   on the page (MIN-879). */
.mention-on-tint [style*="--mention-chip:"] {
  background: linear-gradient(var(--mention-chip), var(--mention-chip)), var(--background);
}
@media (hover: hover) and (pointer: fine) {
  .mention-on-tint [style*="--mention-chip:"]:is(a, button):hover {
    background: linear-gradient(var(--mention-chip-hover), var(--mention-chip-hover)), var(--background);
  }
}

/* The search chip of a typed “/”, “@” or “$” (components/editor/suggestion-chip.ts):
   decorations over the open query, never content. It borrows the pill's
   radius, takes a background tinted with the text's own ink (the neutral
   `--mention-chip` is near invisible on the dark page), keeps the size and
   the ink of the text around it, and stays an inline box: a long query wraps like the
   words beside it, each line keeping its own rounded piece. An empty query
   joins its muted “Type to search” widget into one token. */
/* The chip's room comes from a spread shadow of its own color, not from
   padding: a shadow takes no place in the line, so the text around the query
   does not move when the menu opens, nor when it is cancelled. When the line
   breaks inside the chip or its “Type to search”, every piece keeps its four
   rounded corners and its spread (`box-decoration-break: clone`).
   The color is OPAQUE — the text's ink at 8% over the surface the editor
   sits on — so where the query's and the placeholder's spreads overlap, the
   two read as one continuous token rather than a darker seam. */
.ProseMirror {
  --suggestion-chip-bg: color-mix(in oklab, var(--foreground) 8%, var(--background));
}
:is([data-slot="sheet-content"], [data-slot="dialog-content"], [data-slot="side-panel-content"]) .ProseMirror {
  --suggestion-chip-bg: color-mix(in oklab, var(--foreground) 8%, var(--card));
}
.suggestion-chip,
.suggestion-chip-placeholder {
  border-radius: 0.3em;
  background: var(--suggestion-chip-bg);
  box-shadow: 0 0 0 0.2em var(--suggestion-chip-bg);
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}
.suggestion-chip {
  overflow-wrap: break-word;
}
/* An empty query and its “Type to search” join into one token: the joined
   sides are square, the outer ones rounded. */
.suggestion-chip-empty {
  border-start-end-radius: 0;
  border-end-end-radius: 0;
}
/* Its spread starts where the query's ends: drawn after the query, it would
   otherwise paint over the end of the “/” or “@”. */
.suggestion-chip-placeholder {
  border-start-start-radius: 0;
  border-end-start-radius: 0;
  margin-inline-start: 0.2em;
  color: var(--muted-foreground);
  pointer-events: none;
  user-select: none;
}

/* Fixed shades of mentions. An objective and a project do not have one here:
   they wear their own (the color of the objective, that of the project orb).
   Blue and indigo are those that the pastilles on the ticket and the page had;
   purple is people, and Numo takes --brand. */
:root {
  --mention-person: oklch(0.55 0.2 305);
  --mention-issue: oklch(0.55 0.2 258);
  --mention-page: oklch(0.55 0.2 277);
}

/* ── A ghost stays transparent ──────────────────────────────────────────────
   The button primitive paints the ghost variant `bg-background` — it assumes the
   button rests on a surface of its own color. On minddy's gray chrome that
   makes every quiet chrome button (new ticket, usage, scratchpad, board
   filter, create objective…) read as a floating white pill. A ghost's whole
   point is to borrow the surface it sits on.

   This out-of-layer rule beats every background UTILITY on a ghost button,
   whatever its state: the variant's own `hover:bg-control-hover` and
   `aria-expanded:bg-control-hover`, and the `bg-*` / `hover:bg-*` classes call
   sites pass, so ghost buttons show no hover background. Only `!important`
   utilities (`disabled:bg-control!`) and more specific out-of-layer rules
   (`.code-block-node-actions button:hover`, `.page-callout-icon-button:hover`)
   paint one.

   It stays here rather than in `buttonVariants` on purpose: moving it into a
   utility would revive those hover and call-site backgrounds (about thirty
   call sites, some through shared class constants), which is a design change
   to make deliberately, not as a side effect.

   Keyed on `data-variant` alone, not on the button's `data-slot`: a ghost
   wrapped in `TooltipTrigger asChild` (every sidebar button) carries the
   tooltip's `data-slot="tooltip-trigger"` instead. `button` bounds the rule
   to buttons — menu items and chips use their own data-variant values. */
button[data-variant="ghost"] {
  background-color: transparent;
}



/* ── Diff view of Pull Requests (MIN-66, MIN-181) ─────────────────────────────
   `@pierre/diffs` renders the diff in a Shadow DOM. Nothing is injected into it: the
   CUSTOM PROPERTIES cross the shadow border by inheritance, and
   the lib reads its ~50 `--diffs-*` variables into its `:host`. Ask ours
   here is enough to dress it - this is also why they live with the
   other tokens in the repository rather than in a JavaScript object.

   Warning: only variables that the lib does not DECLARE itself in its
   `:host` are inheritable (a declaration on the host takes precedence over what it
   comes from his parent). Hence the couple `--diffs-light-*` / `--diffs-dark-*`,
   which exists only to be supplied from the outside.

   Both halves of each pair carry the SAME value, and it is not a
   thoughtlessness: our tokens are already taken in the right theme (`.dark` the
   redefines), and the force component `color-scheme` according to the resolved theme of
   minddy. The branch that `light-dark()` chooses is therefore already the right one. */
.pr-diff-view {
  --minddy-diff-bg: var(--card);
  --diffs-font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  --diffs-font-size: 13px;
  --diffs-line-height: 20px;

  /* Background and text: those of the card which bears the diff, not a white/black one
     part — the diff is IN the file map. */
  --diffs-light-bg: var(--card);
  --diffs-dark-bg: var(--card);
  --diffs-light: var(--foreground);
  --diffs-dark: var(--foreground);

  /* Line numbers and unfolding bars: the gray of secondary texts and
     the neutral background of the rest of the app, so that the diff gutter reads
     like the other columns. The rest (background of the context lines, flat areas
     green/red, word-for-word highlighting) is DERIVED by the three-color lib
     below: we do not fix them one by one. */
  --diffs-fg-number-override: var(--muted-foreground);
  --diffs-bg-separator-override: var(--muted);

  /* Green added, red removed, blue mark for selection and “+” for
     comment. The first two are the already proven values ​​of sight
     from before: they keep their contrast placed on the background of the cards, in
     both themes. */
  --diffs-light-addition-color: rgb(35 178 62);
  --diffs-dark-addition-color: rgb(57 208 78);
  --diffs-light-deletion-color: rgb(255 65 55);
  --diffs-dark-deletion-color: rgb(255 71 62);
  --diffs-light-modified-color: var(--brand);
  --diffs-dark-modified-color: var(--brand);
}

/* A snippet of code placed IN a map rather than in the map of a file:
   the hunk of a review comment, in the activity feed. The block that
   door y is in `bg-background` — this is what detaches it from the review card
   around — and the diff must take this background, otherwise it goes there blank on
   gray. A variable, the rest comes from `.pr-diff-view`, which we carry with it. */
.pr-diff-view-inset {
  --minddy-diff-bg: var(--background);
  --diffs-light-bg: var(--background);
  --diffs-dark-bg: var(--background);
}

/* Checkerboard of the diff image panels: a transparent image must be seen
   AS transparent, not blending in with the background of the card. */
.pr-image-checker {
  --checker: color-mix(in oklch, var(--foreground) 7%, transparent);
  background-image:
    linear-gradient(45deg, var(--checker) 25%, transparent 25%),
    linear-gradient(-45deg, var(--checker) 25%, transparent 25%),
    linear-gradient(45deg, transparent 75%, var(--checker) 75%),
    linear-gradient(-45deg, transparent 75%, var(--checker) 75%);
  background-position: 0 0, 0 5px, 5px -5px, -5px 0;
  background-size: 10px 10px;
}

/* ── Shell surfaces ────────────────────────────────────────────────────────────
   Light-gray sidebars around a white content area and header. The AppShell
   root uses --background (white) and shows through the transparent <main>; the
   sidebar uses --sidebar while the header stays on bg-background. Cards/popovers
   stay white with the content surface. */
:root {
  --background: oklch(1 0 0);              /* content area — white */
  --sidebar: oklch(0.967 0.003 264);       /* chrome — light gray */
  --card: oklch(1 0 0);
  --popover: oklch(1 0 0);
  --sidebar-border: oklch(0.912 0.008 264);
  --app-tab-background: color-mix(in oklab, var(--sidebar-accent) 55%, transparent);
  --app-tab-active-background: var(--background);

  /* The primitives fill their borderless controls with an opaque gray
     (--control oklch(0.935)) that reads heavy next to our white surfaces —
     inputs and outline buttons sat as dark chips on the page. Since the
     controls carry no border anymore, the fill must stay whisper-light to
     keep contrast with white: barely grayer than the sidebar, hover one
     clear step below. */
  --control: oklch(0.972 0.005 264);
  --control-hover: oklch(0.938 0.008 264);
}
/* Keep content slightly lighter than both sidebar levels, including floating
   navigation. The shared surface scale below also covers cards and popovers. */
.dark[data-app-shell] {
  --background: #0f0f0f;
  --sidebar: oklch(0.165 0.004 264);
}
.dark {
  --background: oklch(0.165 0.004 264);
  --sidebar: #0f0f0f;
  --card: #0f0f0f;
  --popover: #0f0f0f;
  --sidebar-border: oklch(0.27 0.006 264);

  /* Darken the neutral scale together, preserving the lightness differences
     that distinguish surfaces, selected items, and hover states. */
  --muted: oklch(0.236 0.005 264);
  --sidebar-accent: oklch(0.236 0.005 264);
  --secondary: oklch(0.249 0.005 264);
  --accent: oklch(0.257 0.005 264);
  /* Unfocused tabs sink toward the chrome surface so the active one stands
     out clearly in dark mode (55% left them almost tied with the selection). */
  --app-tab-background: color-mix(in oklab, var(--sidebar-accent) 30%, transparent);
  --app-tab-active-background: color-mix(in oklab, var(--sidebar-accent) 94%, white);

  /* Fields and neutral badges stay above the surface; hover adds another step.
     One notch lighter than 0.7 kept them: the borderless fill must stay clearly
     readable on the dark background, and hover must be felt. */
  --control: oklch(0.262 0.006 264);
  --control-hover: oklch(0.298 0.006 264);
}

/* ── Floating content pane (Linear-style shell, test) ────────────────────────
   The chrome — tab strip, sidebar, and the bands they leave at the bottom and
   on the right — fills the window with --sidebar and carries no borders: the
   shared surface makes it one continuous frame. The content column (header
   clearance + <main>, the AppShell's right child) floats over it as a
   rounded card, clear of the right and bottom window edges, flush against the
   top bar above and the sidebar on its left. Desktop only; in mobile layout the
   shell keeps its classic full-bleed layout and floating pill navigation.

   `--app-pane-radius` is the single source of the pane's corner rounding: the
   content column and every pane-mirror modal (scratchpad notebook, wizard,
   what's-new, skill preview, Numo/agent shells in expanded mode) read it, so
   they all follow the same curve.

   The radius is concentric with the header controls: a 32px pill (16px
   radius) inset by the header padding below (9px) curves along a 25px pane
   corner, so the gap between the two arcs stays the same all the way round.
   `--app-pane-radius` is the single source of the pane's corner rounding: the
   content column and every pane-mirror modal (scratchpad notebook, wizard,
   what's-new, skill preview, Numo/agent shells in expanded mode) read it, so
   they all follow the same curve. */
:root {
  /* 16px pill radius + the 9px header inset (`--app-content-header-pad-x`). */
  --app-pane-radius: 25px;
  /* Half the 50px header strip minus the 16px radius of a 32px pill: the App
     Content Header's side inset. It matches the pills' vertical centering
     margin, so the visible margin from the pane edge is the same 10px on the
     top and both sides, on every page. */
  --app-content-header-pad-x: calc(
    (var(--app-content-header-height) - 32px) / 2
  );
}

:root[data-app-layout="desktop"] {
  .app-shell {
    background: var(--sidebar);
  }

  .app-shell > div.flex.min-h-0.min-w-0.flex-1.flex-col {
    margin-right: 12px;
    /* The bottom band is deep enough to host the Numo chrome button. */
    margin-bottom: 36px;
    border-radius: var(--app-pane-radius);
    /* Real 1px border on the card itself: children are clipped to the padding
       box, so the line can't be repainted or anti-aliased away by the
       scrolling content. box-shadow/outline variants sat under the pane's
       painted children and visibly cropped in places. */
    background: var(--background);
    border: 1px solid var(--border);
  }

  /* Hidden navigation keeps the same gutter on both sides. The margin and
     the pane's width travel on the SAME shared chassis curve as the
     sidebar's own width and slide, so the whole shell resizes live as one
     block instead of snapping when the surface lands. The transition rides
     the lifecycle attribute synced onto the shell main and <body> by
     SidebarNavOverlay: a `:has()` anchored that high would attach
     descendant-watching invalidation sets to it and broaden every unrelated
     style change into a full-document recalculation. */
  .app-shell > div.flex.min-h-0.min-w-0.flex-1.flex-col {
    transition: margin-left 280ms cubic-bezier(0.32, 0.72, 0, 1);
  }
  .app-shell[data-sidebar-hidden="true"] > div.flex.min-h-0.min-w-0.flex-1.flex-col {
    margin-left: 12px;
  }

  /* Pane-mirroring modals (scratchpad notebook, project-creation wizard,
     what's-new, skill preview, reading full-screen, Numo/agent shells in
     expanded mode): modals that VISUALLY REPLACE the content pane — the exact
     same box (below the tab strip, over the sidebar width, clear of the
     right/bottom window bands), same radius and border. Everything under
     !important: the pane mirror must not lose a single rule to the dialog
     primitives' centering/size classes — and to the `!`-prefixed utilities on
     them. */
  .scratchpad-shell-dialog,
  .dialog-pane-mirror {
    position: fixed !important;
    inset: 44px 12px 36px 320px !important;
    /* The dialog primitive is centered with the INDIVIDUAL properties
       (`-translate-x/y-1/2` in Tailwind v4 = `translate`), and its open/close
       animation runs on scale — neither is reached by `transform: none`. */
    transform: none !important;
    translate: none !important;
    scale: none !important;
    width: auto !important;
    height: auto !important;
    max-width: none !important;
    max-height: none !important;
    /* !important on the radius too: the pane mirror must not lose a single
       rule to the dialog primitives' own classes (e.g. !rounded-2xl). */
    border-radius: var(--app-pane-radius) !important;
    border: 1px solid var(--border) !important;
    background: var(--background) !important;
    box-shadow: none !important;
    transition: left 220ms cubic-bezier(0.32, 0.72, 0, 1);
  }

  body[data-sidebar-hidden="true"] .scratchpad-shell-dialog,
  body[data-sidebar-hidden="true"] .dialog-pane-mirror {
    left: 12px !important;
  }
}


/* Application tokens; the design tokens live in app/tokens.css. */
:root {
  --ticket-urgent: oklch(0.62 0.24 25);
}

/* ─── Scrollbars ─────────────────────── ───────────────────────

   That of the browser no longer appears ANYWHERE: everywhere a transparent rail
   and a thin, rounded thumb, which becomes tinted when hovered over it. Only one rule, on the
   universal selector, so that nothing escapes — a screen that keeps the bar
   gray of the system in the middle of an app which no longer has one is visible immediately.

   The thumb is painted in ALPHA, black in light and white in dark: it is placed
   both on a card and on the sidebar or the bottom of the content,
   without having to vary one tone per surface.

   A 10 px wide track for a comfortable pointer target, and a thumb
   which only occupies 4: the transparent border of 3 px thins it without
   shrink the area that can be caught (`background-clip: content-box`).

   PLEASE NOTE: styling a scrollbar causes it to lose its OVERLAY mode.
   On macOS, where it floated above the content without taking anything from it, it
   now reserves its 10 px as soon as there is something to scroll. This is the price of one
   bar that belongs to us; `no-scrollbar` (app/tokens.css) stays where we prefer
   have nothing at all, like the columns of the board.

   The thumb (`--scrollbar-thumb`, `--scrollbar-thumb-hover`) and the utility
   `scrollbar-quiet` come from app/tokens.css. What remains here is
   the only thing that the library leaves to the app: the decision to apply this
   bar on ALL surfaces, via the universal selector. */
@layer base {
  /* Standard (Firefox, Chrome 121+, Safari 18.2+). */
  * {
    scrollbar-width: thin;
    scrollbar-color: var(--scrollbar-thumb) transparent;
  }

  /* WebKit/Blink, for versions that don't yet read the two above.
     In @layer base, so any utility covers them — that's what
     lets `no-scrollbar` and `scrollbar-quiet` take control again. */
  *::-webkit-scrollbar {
    width: 10px;
    height: 10px;
  }
  *::-webkit-scrollbar-track,
  *::-webkit-scrollbar-corner {
    background: transparent;
  }
  *::-webkit-scrollbar-thumb {
    background-color: var(--scrollbar-thumb);
    background-clip: content-box;
    border: 3px solid transparent;
    border-radius: 9999px;
  }
  *::-webkit-scrollbar-thumb:hover {
    background-color: var(--scrollbar-thumb-hover);
  }

  /* Chromium re-evaluates `:focus-visible` on every keydown — including bare
     modifier presses like Shift and Space on a mouse-focused element. The
     element then starts matching `:focus-visible` and the browser paints its
     default (blue) UA focus ring on anything that has no author outline rule:
     sidebar rows, plain links, and any other focusable surface. Every
     interactive component here defines its own focus-visible treatment
     (background tint, ring, or border), so the UA outline is only ever noise.
     Replace it with a transparent one — same trick as Tailwind v4's
     `outline-hidden`: invisible normally, still rendered in forced-colors
     mode. Declared in @layer base so utilities (`focus-visible:outline-ring`,
     `focus-visible:ring-*`) and hand-written rules keep winning. */
  :focus-visible {
    outline: 2px solid transparent;
    outline-offset: 2px;
  }

  /* The clear button of a search field (`type="search"`: settings searches,
     the emoji picker's search…) is drawn by WebKit as a blue glyph that
     matches nothing else in the app. Redraw it as the hugeicons `Cancel01`
     icon used by every other close control, in the field's text color. */
  input[type="search"]::-webkit-search-cancel-button {
    -webkit-appearance: none;
    appearance: none;
    width: 0.875rem;
    height: 0.875rem;
    margin-inline-start: 0.375rem;
    cursor: pointer;
    background-color: currentColor;
    opacity: 0.55;
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none'%3E%3Cpath d='M18 6L6.00081 17.9992M17.9992 18L6 6.00085' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none'%3E%3Cpath d='M18 6L6.00081 17.9992M17.9992 18L6 6.00085' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
  }
  @media (hover: hover) and (pointer: fine) {
    input[type="search"]::-webkit-search-cancel-button:hover {
      opacity: 1;
    }
  }
}

/* Keyboard focus in every theme (WCAG 2.4.7, MIN-783). The base layer hides
   the UA outline, and many primitives declare `focus-visible:outline-none`
   with no replacement, so a keyboard user saw no focus at all. While Tab
   drives focus (`data-input-modality`, lib/keyboard-modality.ts) this draws
   the ring color around the focused control. Unlayered, it wins over those
   utilities; a click clears the attribute, so mouse use looks as before.

   Same exclusions as the high-contrast rule in app/theme-presets.css: menu
   rows and options show focus by their highlight, and containers that take
   focus when they open keep their edge. Text being edited shows its caret,
   script-only targets (`tabindex="-1"`, the skip link's `<main>`) are not
   controls, and the switch draws its own offset ring. */
html[data-input-modality="keyboard"] :focus-visible:not(
  [role^="menuitem"], [role="option"], [role="dialog"], [role="alertdialog"],
  [role="menu"], [role="listbox"], [data-slot$="-content"],
  .desktop-palette-surface, [contenteditable="true"], [tabindex="-1"],
  [role="switch"]
) {
  outline: 2px solid var(--ring);
  outline-offset: 1px;
}
/* Rows in the sidebar's scrolling column, and tabs in a clipped tab list,
   would lose an outside ring to their container's `overflow`: draw it
   inside. */
html[data-input-modality="keyboard"] :is(.sidebar-nav-panel, [role="tablist"]) :focus-visible {
  outline-offset: -2px;
}

/* ─── Numo (assistant) ──────────────────────────────────────────────── */

/* Numo compact ⇄ expanded morph. The panel keeps right/bottom anchoring
   (top/left auto) in BOTH modes, so toggling between the compact widget size
   and the expanded modal size interpolates these properties from px to px:
   the surface grows AND slides toward center in one fluid motion. Declared in
   @layer utilities so it overrides SheetContent's base `transition` utility
   (which doesn't include width/height/inset). The open/close keyframes
   (animate-in/out on transform+opacity) are independent and unaffected. */
@layer utilities {
  [data-slot="sheet-content"].assistant-panel-morph {
    transition-property: width, height, right, bottom, border-radius;
    transition-duration: 280ms;
    transition-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
  }

  /* Both modes use numeric dimensions and the same right/bottom anchors. */
  :root[data-app-layout="desktop"] {
    [data-slot="sheet-content"].assistant-panel-expanded {
      top: auto !important;
      left: auto !important;
      right: 12px !important;
      bottom: 36px !important;
      width: calc(100vw - 332px) !important;
      height: calc(100dvh - 80px) !important;
      transition: width 280ms cubic-bezier(0.16, 1, 0.3, 1),
        height 280ms cubic-bezier(0.16, 1, 0.3, 1),
        right 280ms cubic-bezier(0.16, 1, 0.3, 1),
        bottom 280ms cubic-bezier(0.16, 1, 0.3, 1),
        border-radius 280ms cubic-bezier(0.16, 1, 0.3, 1) !important;
    }

    body[data-sidebar-hidden="true"] [data-slot="sheet-content"].assistant-panel-expanded {
      left: auto !important;
      width: calc(100vw - 24px) !important;
    }
  }

  @media (prefers-reduced-motion: reduce) {
    [data-slot="sheet-content"].assistant-panel-morph {
      transition: none !important;
    }
  }
  /* Shared panels stay at the viewport edges after removing the shell frame. */
  html .ultrawide-canvas [data-slot="side-panel-content"][data-side="right"] {
    right: 12px !important;
    top: 12px !important;
    bottom: 12px !important;
  }
  html .ultrawide-canvas [data-slot="side-panel-content"][data-side="left"] {
    left: 12px !important;
    top: 12px !important;
    bottom: 12px !important;
  }
}

/* Agent edging (AgentBeam) permanently mounted (`keepMounted`, e.g. dial it
   Numo): when at rest, its wrapper must be completely inert. BorderBeam poses a
   `overflow: hidden` — useful for the border, but it would then crop the halo of
   focus (ring-2) of the wrapped surface. It is only restored when switched on and
   during fade-out ([data-active] / [data-fading]).
   Excluding @layer and by specificity (0,3,0) to beat the non-layered `<style>`
   that BorderBeam injects — a Tailwind utility is in @layer and
   perdrait. */
[data-beam]:not([data-active]):not([data-fading]) {
  overflow: visible;
}

/* Streamdown Shiki fallback — !important overrides its generated inline styles. */
.dark .shiki,
.dark .shiki span {
  color: var(--shiki-dark) !important;
  background-color: var(--shiki-dark-bg) !important;
  font-style: var(--shiki-dark-font-style) !important;
  font-weight: var(--shiki-dark-font-weight) !important;
  text-decoration: var(--shiki-dark-text-decoration) !important;
}

/* ── Shared code blocks (TipTap / lowlight) ───────────────────────────────
   Editable Page/notebook blocks and their read-only Markdown twin use the
   same lowlight tokens and palette. */
:root {
  --code-comment: #6e7781;
  --code-keyword: #cf222e;
  --code-string: #0a3069;
  --code-constant: #0550ae;
  --code-title: #8250df;
  --code-attr: #0550ae;
  --code-tag: #116329;
}
.dark {
  --code-comment: #8b949e;
  --code-keyword: #ff7b72;
  --code-string: #a5d6ff;
  --code-constant: #79c0ff;
  --code-title: #d2a8ff;
  --code-attr: #79c0ff;
  --code-tag: #7ee787;
}

.hljs-comment,
.hljs-quote {
  color: var(--code-comment);
  font-style: italic;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-doctag,
.hljs-formula,
.hljs-meta .hljs-keyword {
  color: var(--code-keyword);
}
.hljs-string,
.hljs-regexp,
.hljs-addition {
  color: var(--code-string);
}
.hljs-number,
.hljs-literal,
.hljs-symbol,
.hljs-bullet,
.hljs-link {
  color: var(--code-constant);
}
.hljs-title,
.hljs-section,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class {
  color: var(--code-title);
}
.hljs-attribute,
.hljs-attr,
.hljs-variable,
.hljs-template-variable,
.hljs-type,
.hljs-class,
.hljs-built_in,
.hljs-selector-attr,
.hljs-selector-pseudo {
  color: var(--code-attr);
}
.hljs-tag {
  color: var(--code-tag);
}
.hljs-emphasis {
  font-style: italic;
}
.hljs-strong {
  font-weight: 600;
}

/* The language badge of the static/fallback HTML representation: the
   extension mirrors the node's `language` attribute onto `data-language`, CSS
   does the rest. The live TipTap node view renders the same label in its
   header. */
.ProseMirror pre[data-language]:not([data-language=""]) {
  position: relative;
}
.ProseMirror pre[data-language]:not([data-language=""])::before {
  content: attr(data-language);
  position: absolute;
  top: 0.25rem;
  right: 0.5rem;
  padding: 0.0625rem 0.25rem;
  border-radius: 0.25rem;
  font-size: 0.65rem;
  line-height: 1.2;
  color: var(--muted-foreground);
  background: color-mix(in oklab, var(--muted) 72%, transparent);
  pointer-events: none;
}

/* Pages and Agent responses share this code-block surface. In TipTap, the code
   element remains the editable content DOM; the same surrounding chrome is
   reused with read-only highlighted content in an Agent conversation. */
.code-block-node-view {
  --code-block-background: #f6f8fa;
  --code-block-foreground: #1f2328;
  --code-block-muted: #59636e;
  --code-block-hover: rgb(31 35 40 / 8%);
  margin-block: 0.5rem;
  overflow: hidden;
  border-radius: 1.5rem;
  background: var(--code-block-background);
  color: var(--code-block-foreground);
}
.dark .code-block-node-view {
  --code-block-background: #202020;
  --code-block-foreground: #f5f5f5;
  --code-block-muted: #a3a3a3;
  --code-block-hover: rgb(255 255 255 / 8%);
}
.code-block-node-view > .code-block-node-header {
  display: flex;
  min-height: 2.5rem;
  align-items: center;
  justify-content: flex-start;
  gap: 0.5rem;
  background: transparent;
  padding: 1rem 1rem 0.25rem;
}
.code-block-node-language {
  min-width: 0;
  flex: 1;
  border: 0;
  outline: 0;
  background: transparent;
  color: var(--code-block-foreground);
  cursor: text;
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
  font-size: inherit;
  letter-spacing: 0.025em;
  line-height: 1.2;
  text-transform: lowercase;
}
.code-block-node-language-trigger,
.code-block-node-language-label {
  min-width: 0;
  width: max-content;
  max-width: min(24rem, calc(100vw - 2rem));
  flex: 0 1 auto;
  justify-content: flex-start;
  gap: 0.375rem;
  padding: 0;
  color: var(--code-block-muted);
  font-size: inherit;
  font-weight: 400;
  line-height: 1.2;
}
.code-block-node-language-trigger[data-state="open"] {
  color: var(--code-block-foreground);
}
@media (hover: hover) and (pointer: fine) {
  .code-block-node-language-trigger:hover {
    color: var(--code-block-foreground);
  }
}
.code-block-node-language-label {
  display: flex;
  height: 2rem;
  max-width: 18rem;
  align-items: center;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.code-block-node-language-trigger > span {
  min-width: 0;
  max-width: 18rem;
}
.code-block-node-language:read-only {
  cursor: default;
}
.code-block-node-actions {
  display: flex;
  margin-left: auto;
  align-items: center;
  gap: 0.125rem;
}
.code-block-node-actions button {
  color: var(--code-block-muted);
}
.code-block-node-wrap[data-active="true"] {
  background: var(--code-block-hover);
  color: var(--code-block-foreground);
}
@media (hover: hover) and (pointer: fine) {
  .code-block-node-actions button:hover {
    background: var(--code-block-hover);
    color: var(--code-block-foreground);
  }
}
.code-block-node-copy {
  margin-left: auto;
}
.code-block-node-copy[data-copied="true"] {
  color: var(--primary);
}
@media (hover: hover) and (pointer: fine) {
  .code-block-node-copy:hover {
    background: var(--code-block-hover);
    color: var(--code-block-foreground);
  }
}
.code-block-node-view > pre {
  margin: 0;
  overflow-x: auto;
  border: 0;
  border-radius: 0;
  background: transparent;
  padding: 0.5rem 1rem 1rem;
  color: var(--code-block-foreground);
  font-family:
    ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas,
    "Liberation Mono", monospace;
  font-size: inherit;
  font-variant-ligatures: none;
  line-height: 1.65;
}
.code-block-node-view[data-wrapped="true"] > pre {
  overflow-x: hidden;
  overflow-wrap: anywhere;
  white-space: pre-wrap;
}
.code-block-node-view[data-wrapped="false"] > pre {
  white-space: pre;
}
.code-block-node-view[data-overflowing="true"][data-expanded="false"] > pre {
  max-height: 26.25rem;
  overflow-y: hidden;
}
.code-block-node-view > pre > code {
  font-size: inherit;
}
.code-block-node-expand {
  display: flex;
  min-height: 2.5rem;
  align-items: center;
  justify-content: flex-start;
  background: transparent;
  padding: 0.25rem 1rem 1rem;
}
.code-block-node-expand > button {
  gap: 0.25rem;
  color: var(--code-block-muted);
  font-size: 0.75rem;
}

/* ─── Streamdown chrome trim (Numo assistant markdown) ───────────────────
   By default Streamdown wraps the code/table box inside a SECOND, sidebar-
   coloured frame (border + bg-sidebar + p-2) and adds a 1rem vertical margin,
   so blocks float in an oversized double-framed container. We flatten that
   redundant outer frame and tighten the margin; the inner code/table box (the
   part that already looks fine) is untouched, and header/copy-button heights
   are left intact so the floating actions stay aligned. */
.streamdown-compact :is(
    [data-streamdown="code-block"],
    [data-streamdown="table-wrapper"]
  ) {
  margin-block: 0.5rem;
  padding: 0;
  gap: 0.25rem;
  border: 0;
  border-radius: 0;
  background: transparent;
}

/* The table's scroll area pads the (already bordered) table with a wide band —
   drop it so the table fills the width instead of sitting inside empty space. */
.streamdown-compact
  [data-streamdown="table-wrapper"]
  div:has(> [data-streamdown="table"]) {
  padding: 0;
}

/* ─── Read-only data tables (MIN-947) ────────────────────────────────────
   The run history, Markdown tables in comments and pull request bodies, and
   the tables in Numo's answers look like a page database's table view
   (components/pages/page-database-view.tsx): no frame, a quiet header with a
   hairline under it, 40px rows split by faint lines, body columns split by
   fainter ones. `.data-table` carries the look for the tables we render;
   Streamdown owns the markup of Numo's tables, so they are matched by their
   `data-streamdown` attributes, including the fullscreen view it portals out
   of the message. Overriding Streamdown's `table` component instead would
   drop its copy, download and fullscreen controls. */
div:has(> [data-streamdown="table"]) {
  border: 0;
  border-radius: 0;
  background: transparent;
}

:is(.data-table, [data-streamdown="table"]) {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  border: 0;
  text-align: left;

  /* Streamdown separates its sections and rows with `divide-y` borders. */
  & > :is(thead, tbody),
  & > :is(thead, tbody) > tr {
    border: 0;
    background: transparent;
  }

  & th,
  & td {
    padding: 0.625rem 0.5rem;
    font-size: inherit;
    line-height: 1.25rem;
  }

  & th {
    border-bottom: 1px solid color-mix(in oklab, var(--border) 50%, transparent);
    color: var(--muted-foreground);
    font-weight: 400;
    white-space: nowrap;
  }

  & td {
    border-bottom: 1px solid color-mix(in oklab, var(--border) 40%, transparent);
  }

  & td + td {
    border-left: 1px solid color-mix(in oklab, var(--border) 30%, transparent);
  }
}

/* Rows that open something take the database row's hover tint. They keep the
   app's default arrow (MIN-587) over the whole row, text included, so the
   cursor never alternates between cells and the button in the first one. */
.data-table-interactive > tbody > tr {
  cursor: default;
  transition: background-color 120ms ease-out;

  &:focus-within {
    background: color-mix(in oklab, var(--muted) 20%, transparent);
  }
}

/* The hover tint needs a fine pointer: after a tap it would stay stuck. */
@media (hover: hover) and (pointer: fine) {
  .data-table-interactive > tbody > tr:hover {
    background: color-mix(in oklab, var(--muted) 20%, transparent);
  }
}

/* The fixed mobile bar owns its geometry. Keep the shell's reserve aligned
   with its 48px buttons and safe-area anchor, plus a small content gutter. */
:root {
  /* Three rem of buttons plus the safe bottom anchor. */
  --mobile-nav-height: calc(3rem + max(1rem, env(safe-area-inset-bottom)));
  /* What we keep free under the content: the pill and a trickle of air. */
  --mobile-nav-clearance: calc(var(--mobile-nav-height) + 0.75rem);
}

/* Reserve the actual navigation height whenever the mobile layout is active. */
:root[data-app-layout="mobile"] {
  .app-shell main {
    padding-bottom: var(--mobile-nav-clearance);
  }

  /* An ANCHORED footer (consisting of pull request, agent session) is
     now poses just above the pill - therefore IN the gradient of 8 rem
     which it projects to melt what passes beneath. A field does not have
     to fade, and its send button even less: this class takes it out.
     41 and not 50: above the bar (z-40), below the modals and menus. */
  .dock-above-nav {
    position: relative;
    z-index: 41;
  }
}

/* ─── Security zones: what `viewport-fit=cover` reveals ─────────
   Exporting `viewport` from the root layout (app/layout.tsx) renders the page with full bleed,
   which finally gives a value to the `env(safe-area-inset-*)` already written in the
   deposit. The counterpart is immediate: what touched an edge of the screen
   Now touch the notch or the home indicator bar. The centered dialogs take
   the insets off their own cap (components/ui/dialog.tsx, alert-dialog.tsx);
   the shell and the sheets are handled here.

   `display: standalone` in app/manifest.json: minddy is installing. This is where
   the TOP inset exists — in a Safari tab, the URL bar already occupies the
   place and the inset is 0. These rules therefore do not change anything in the browser, and
   all to the installed app. The lateral insets wake up in landscape.

   The shell first. It is in `h-dvh` (so full screen with full bleed) with a
   header `sticky top-0`: without these margins, the breadcrumbs go under the hour
   and the battery. `box-sizing: border-box` is global at Tailwind, so this
   padding is taken WITHIN 100 dvh instead of overflowing. Nothing for the bottom: the
   <main> already reserves `--mobile-nav-clearance`, which contains the inset. */
.app-shell {
  padding-top: env(safe-area-inset-top);
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
}

/* The drawers are GLUED to their edge — `inset-y-0 h-full` on the left and
   right, `bottom-0` at the bottom — so nothing separates them from anything. A
   padding, not a margin: the drawer's background runs to the edge, only its
   content moves back.

   These rules stay out of components/ui/sheet.tsx on purpose. They must beat a
   call site's `p-0` (an out-of-layer rule does, a utility would not), and lose
   to the mobile sheet rules below that set their own bottom padding (an
   `!important` utility would win there). No class expresses both. */
[data-slot="sheet-content"][data-side="bottom"] {
  padding-bottom: env(safe-area-inset-bottom);
}
[data-slot="sheet-content"][data-side="top"] {
  padding-top: env(safe-area-inset-top);
}
[data-slot="sheet-content"]:is([data-side="left"], [data-side="right"]) {
  padding-top: env(safe-area-inset-top);
  padding-bottom: env(safe-area-inset-bottom);
}
[data-slot="sheet-content"][data-side="left"] {
  padding-left: env(safe-area-inset-left);
}
[data-slot="sheet-content"][data-side="right"] {
  padding-right: env(safe-area-inset-right);
}

/* ─── Finger (MIN-284) ─────────────────────────────────────────────────────
   The touch targets, press feedback and tap delay of the primitives live in
   the primitives themselves (components/ui/touch-target.ts). What remains here
   is page-wide: the tap highlight, and the zoom-on-focus guard of hand-written
   fields. */
@layer base {
  /* The gray halo that WebKit and Chrome Android paints over anything you type.
     It arrives late, overflows the border radius, and has nothing left to say
     now that a press has its own feedback (components/ui/touch-target.ts). */
  html {
    -webkit-tap-highlight-color: transparent;
  }
}

@media (pointer: coarse) {
  /* Zoom to focus. iOS zooms into ANY field whose font is
     under 16 px — and it does not zoom out when exiting: the page remains crooked,
     the user must pinch to return. The primitives handle it (their fields
     are in `text-base md:text-sm`, therefore 16 px on mobile), but the fields
     handwritten on the product, they are in `text-sm` — the title of a cycle,
     the title of a brief, the times of the date picker.

     One rule rather than three fixes: it also applies to the next one
     handwritten field, which will otherwise start again. We ONLY aim for what is
     enters — a checkbox or slider has no text, and a button
     `<input type="submit">` does not have to grow.

     Editable hosts count too (MIN-841): iOS zooms into ANY focused element
     under 16 px, not only form fields. The home “Ask Numo” composer
     (components/assistant/chat-input.tsx) and the compact rich editor of
     descriptions and comments are `contenteditable` hosts in `text-sm`, so
     the page zoomed although every `<input>` was already at 16 px. The size
     goes on the host only: the mention chips inside it are
     `contenteditable="false"`, and headings and code keep their own sizes.

     Every editor host takes it, editable or not, and so does the plain-text
     fallback shown while the editor loads (MIN-935). An issue's description
     and plan open as a read-only preview, then as the live editor once its
     session attaches (which stays read-only until then): scoped to editable
     hosts only, the text painted at 14 px and grew to 16 px a moment later,
     like a late load. A clipped excerpt in a mention preview card is not
     read or written there, so it keeps the compact size. */
  .ProseMirror:not([data-compact-excerpt] *),
  [data-editor-fallback]:not([data-compact-excerpt] *),
  input:not(
      [type="checkbox"],
      [type="radio"],
      [type="range"],
      [type="color"],
      [type="button"],
      [type="submit"],
      [type="reset"],
      [type="file"],
      [type="image"]
    ),
  /* Title fields are larger than the floor: it would only shrink them. */
  textarea:not(.title-field),
  select,
  [contenteditable]:not([contenteditable="false"], .title-field) {
    font-size: 16px;
  }
}

/* The shell sets a lifecycle attribute on the document. A root :has() selector
   would make inserting any menu or dialog recheck the full workspace subtree.
   Disable app boundary bounce at the viewport and on each scrolling axis.
   The document scope also covers menus and dialogs portaled outside the shell.
   Extend overflow utilities so responsive variants and library components share
   the policy. Do not apply it to every element: an overflow-hidden wrapper or
   the unused axis of a horizontal table would trap normal parent scrolling.
   Public pages retain their browser behavior. */
html[data-app-shell] {
  --app-overscroll: none;
  overscroll-behavior: none;
}

/* Hover lands at once and only its exit fades. With the usual 150 ms
   `transition-colors`, gliding from one control to its neighbour (the
   notebook gutter's `+` and handle, a toolbar, a row of icons) faded the first
   out while the second faded in: for a few frames neither was lit, which read
   as a flicker. The transition in force is the one of the NEW state, so a
   zero duration under `:hover` makes the entry instant and leaves the exit
   animated. Unlayered on purpose, to win over the `transition-*` utilities;
   app only, so the marketing pages keep their hover animations. */
@media (hover: hover) and (pointer: fine) {
  html[data-app-shell] :is(button, a, [role="button"], [role="tab"], summary):hover {
    transition-duration: 0s;
  }
}

html[data-app-shell] body,
html[data-app-shell] textarea {
  overscroll-behavior: none;
}

/* A title typed in an auto-growing textarea (page, ticket, objective,
   feedback, creation dialogs). Descenders (p, g, y) hang below a tight line
   box: left as overflow they made the field scrollable with the wheel, and
   clipped they lost their tails. The vertical padding holds them inside the
   field, so `overflow: hidden` clips nothing and still forbids scrolling; the
   matching negative margin keeps the title where it was. The caret helpers
   and the live-caret overlay mirror the padding. */
@utility title-field {
  overflow: hidden;
  padding-block: 0.15em;
  margin-block: -0.15em;
}

@utility overflow-auto {
  &:where(html[data-app-shell] *) {
    overscroll-behavior: none !important;
  }
}
@utility overflow-scroll {
  &:where(html[data-app-shell] *) {
    overscroll-behavior: none !important;
  }
}
@utility overflow-x-auto {
  &:where(html[data-app-shell] *) {
    overscroll-behavior-x: none !important;
  }
}
@utility overflow-x-scroll {
  &:where(html[data-app-shell] *) {
    overscroll-behavior-x: none !important;
  }
}
@utility overflow-y-auto {
  &:where(html[data-app-shell] *) {
    overscroll-behavior-y: none !important;
  }
}
@utility overflow-y-scroll {
  &:where(html[data-app-shell] *) {
    overscroll-behavior-y: none !important;
  }
}

/* Mobile full-screen search sheet: the <CommandList> primitive ships a default
   max-height (max-h-72 ≈ 288px) sized for the desktop popover. In the full-
   screen sheet that squishes the command palette into the top third with a big
   empty void below — most options end up hidden behind a scroll, so it reads as
   "incomplete". The sheet's list is the flex-1 one (the desktop pill uses a
   separate raw cmdk list), so lift the cap there and let flex fill the sheet. */
[cmdk-list].flex-1 {
  max-height: none !important;
}

/* Issue pickers (MIN-718): matches by meaning are force-mounted rows, which
   cmdk's own count ignores — its "No results" line would sit right above a
   result. The semantic group carries a marker; while it is there, the empty
   line goes. */
[cmdk-list]:has([data-semantic-group]) [cmdk-empty] {
  display: none;
}

/* ─── Selection of chat bubble text (agent + Numo) ────────
   Two rules prevent selection: the global reset `user-select: none` of
   app/tokens.css, AND — above all — Streamdown (markdown rendering of the wizard) which poses
   `style="user-select:none"` INLINE on its content. An inline style beats all
   non-`!important` CSS rule (no matter how specific), hence the `!important` here:
   this is the only way to re-authorize the selection on the bubbles `.chat-selectable`
   and their descendants. We keep the general reset intact everywhere else.
   `.comment-selectable` is the same exception for the comments of every
   activity (issues, objectives, feedback, triage, pages, pull request
   reviews, the public board): their text is read and quoted, by readers too.
   `.content-selectable` extends it to the other text people write and read
   rather than click: a feedback request and its original submission, a
   published page's title, a file or skill opened for reading, the agent's
   reasoning, earlier versions of a pull request comment. */
.chat-selectable,
.chat-selectable
  *:not(input):not(textarea):not([contenteditable="true"]):not(pre):not(code),
.comment-selectable,
.comment-selectable
  *:not(input):not(textarea):not([contenteditable="true"]):not(pre):not(code),
.content-selectable,
.content-selectable
  *:not(input):not(textarea):not([contenteditable="true"]):not(pre):not(code) {
  user-select: text !important;
  -webkit-user-select: text !important;
}

/* Public articles override the design system reset, including nested text. */
.documentation-selectable,
.documentation-selectable *:not(button):not(button *):not(input):not(textarea) {
  user-select: text !important;
  -webkit-user-select: text !important;
}

/* Every diff surface is selectable, including PR files, review hunks, commit
   sheets, and agent-side diffs. The renderer receives the same override inside
   its shadow root because the app/tokens.css reset otherwise crosses the host. */
.diff-selectable,
.diff-selectable *:not(button):not(input):not(textarea) {
  user-select: text !important;
  -webkit-user-select: text !important;
}

/* Embedded diff chrome remains ordinary UI. This opt-out deliberately follows
   the selectable rule so badges and future controls do not inherit its forced
   text cursor while the surrounding source code stays copyable. */
.diff-selectable [data-diff-nonselectable],
.diff-selectable [data-diff-nonselectable] * {
  user-select: none !important;
  -webkit-user-select: none !important;
}

/* ─── Scratchpad (Notes) — always editable WYSIWYG surface ──────────────
   Check boxes (TaskList/TaskItem) and the “copy section” button
   (ProseMirror decoration placed on each title) do not have a style provided by
   the extensions: we dress them here, scoped at `.scratchpad-editor`. */
.scratchpad-editor .ProseMirror {
  outline: none;
  /* Above the section-highlight box (placed behind, z-index: 0) — the text remains
     readable over the tint. */
  position: relative;
  z-index: 1;
}

/* Tinted block placed BEHIND the section hovered over via the “copy the” button
   section » (components/scratchpad/section-copy-extension.ts) — identifie
   exactly what will be copied. */
.scratchpad-editor .scratchpad-section-box {
  position: absolute;
  left: -0.5rem;
  right: -0.5rem;
  z-index: 0;
  border-radius: 0.5rem;
  background: color-mix(in oklab, var(--muted) 65%, transparent);
  opacity: 0;
  transition: opacity 0.12s;
  pointer-events: none;
}
.scratchpad-editor .scratchpad-section-box.is-visible {
  opacity: 1;
}

/* Stylish tooltip of the “copy section” button (replaces the native title) —
   same skin as <TooltipContent> of components/ui/tooltip.tsx (dark bubble + arrow). */
.scratchpad-editor .scratchpad-section-tip {
  position: absolute;
  z-index: 60;
  transform: translate(-50%, calc(-100% - 8px));
  padding: 0.375rem 0.625rem;
  border-radius: 0.375rem;
  background: var(--foreground);
  color: var(--background);
  font-size: 0.75rem;
  line-height: 1rem;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.12s;
}
.scratchpad-editor .scratchpad-section-tip.is-visible {
  opacity: 1;
}
.scratchpad-editor .scratchpad-section-tip::after {
  content: "";
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  border: 4px solid transparent;
  border-top-color: var(--foreground);
}

/* Placeholder placed by the Placeholder extension on the current empty line
   (is-empty) and on the empty document (is-editor-empty) — one index per line. */
.scratchpad-editor .ProseMirror .is-empty::before,
.scratchpad-editor .ProseMirror .is-editor-empty::before {
  content: attr(data-placeholder);
  float: left;
  height: 0;
  pointer-events: none;
  color: var(--muted-foreground);
  opacity: 0.5;
}

/* The checkbox, the crossed out line and the menu ⋯ are rendered by the NodeView
   React (components/scratchpad/scratchpad-task.tsx) en classes Tailwind — ici,
   just resetting the list and indenting the subtasks. */
.scratchpad-editor .ProseMirror ul[data-type="taskList"] {
  list-style: none;
  margin: 0.5rem 0;
  padding: 0;
}
.scratchpad-editor .ProseMirror ul[data-type="taskList"] li[data-type="taskItem"] {
  margin: 0.125rem 0;
}
/* The paragraph of a task otherwise inherits the `[&_p]:my-2` from the editor, which
   shifts the text down and unaligns the box + the ⋯. */
.scratchpad-editor .ProseMirror li[data-type="taskItem"] p {
  margin: 0;
}
.scratchpad-editor
  .ProseMirror
  ul[data-type="taskList"]
  ul[data-type="taskList"] {
  margin: 0.125rem 0;
  padding-left: 1.5rem;
}

.scratchpad-editor .ProseMirror :is(h1, h2, h3) {
  position: relative;
}
.scratchpad-editor .ProseMirror .scratchpad-section-copy {
  position: absolute;
  top: 0.15rem;
  right: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 9999px;
  color: var(--muted-foreground);
  background: color-mix(in oklab, var(--background) 80%, transparent);
  opacity: 0;
  transition: opacity 0.12s, color 0.12s;
  cursor: default;
}
.scratchpad-editor .ProseMirror .scratchpad-section-copy:focus-visible {
  opacity: 1;
}
@media (hover: hover) and (pointer: fine) {
  .scratchpad-editor .ProseMirror :is(h1, h2, h3):hover .scratchpad-section-copy {
    opacity: 1;
  }
  .scratchpad-editor .ProseMirror .scratchpad-section-copy:hover {
    color: var(--foreground);
  }
}
/* Nothing hovers a heading on touch: the copy button is always there. */
@media (hover: none), (pointer: coarse) {
  .scratchpad-editor .ProseMirror .scratchpad-section-copy {
    opacity: 1;
  }
}

/* ─── Landing: hero entrance and scroll appearances ───────────────────
   Everything is pure CSS: the sections of the landing are Server Components
   and must remain so. The only JavaScript is one IntersectionObserver per block
   (components/marketing/reveal.tsx) which sets `data-revealed` — no library
   animation does not enter the public site bundle.

   Common curve: cubic-bezier(0.16, 1, 0.3, 1) — a very “ease-out”
   pronounced that starts quickly and stops smoothly. This is what makes a
   appearance is noticeable without making the visitor wait for it. */

/* ─── Hero: load-time cascade, no JS ─────────────────────────────────────
   Above the fold, so played on display and not on
   scroll: the hero is already visible when the page arrives, an observer
   would only add hydration latency.

   Non-cosmetic detail of the keyframe: the title starts from `opacity: 0.1`, NOT 0.
   Chrome excludes elements with zero opacity from the LCP calculation and would push back
   therefore the LCP at the END of the word-for-word shift. A floor at 0.1 combined with blur
   remains imperceptible to the eye but keeps the <h1> eligible from the first
   frame. The secondary elements (not the LCP) start from 0. */
@keyframes hero-word-in {
  from {
    opacity: 0.1;
    filter: blur(8px);
    transform: translateY(0.32em);
  }
  to {
    opacity: 1;
    transform: none;
  }
}
.hero-word {
  display: inline-block;
  animation: hero-word-in 0.62s cubic-bezier(0.16, 1, 0.3, 1) backwards;
  animation-delay: calc(var(--hero-i, 0) * 0.06s);
}

@keyframes hero-fade-up {
  from {
    opacity: 0;
    transform: translateY(14px);
    filter: blur(6px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}
/* Cascade which extends the word for word of the title: the next line starts before
   until the previous one is placed, otherwise the whole thing is lying around. */
.hero-reveal {
  animation: hero-fade-up 0.7s cubic-bezier(0.16, 1, 0.3, 1) backwards;
  animation-delay: calc(var(--hero-d, 0) * 1s);
}

/* The capture of the hero is the second LCP candidate (the first as soon as a real
   image replaces the template): it therefore starts from the same opacity floor as
   the title, otherwise his delay in the cascade would become the LCP score. */
@keyframes hero-fade-up-media {
  from {
    opacity: 0.1;
    transform: translateY(14px);
    filter: blur(6px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}
.hero-reveal-media {
  animation-name: hero-fade-up-media;
}

/* ─── Blocs qui apparaissent au scroll (<Reveal>, <RevealGroup>) ─────────
   Without JS and in server rendering, nothing is hidden: `.reveal-ready` is not set
   only during editing, so the landing remains entirely readable if the script does not
   don't leave. `[data-revealed]` is set by the observer on the first pass.

   FILLING `backwards`, NOT `both` — here as in the hero. An animation
   in `both` retains its arrival state: the landing thus kept 114 elements
   (including 51 title words) under `filter: blur(0px)` once the scroll has passed.
   `blur(0)` is not `none`: each filtered element remains a layer that the
   browser rasterizes separately, and a change of zoom redoes them all at once
   suddenly — hence the collapse in fluidity when zooming after scrolling.

   Removing `filter` from the last keyframe IS NOT ENOUGH: the browser generates
   then an implicit keyframe from the base value, and interpolation to
   `none` calcule justement `blur(0px)`, que `both` fige. Il faut donc que
   the animation DROPS the element at the end. `backwards` only fills the deadline
   waiting; the final state is carried by the base style (`opacity: 1`), which
   does not declare any filter. */
@keyframes reveal-in {
  from {
    opacity: 0;
    transform: translateY(18px);
    filter: blur(6px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

.reveal.reveal-ready {
  opacity: 0;
}
.reveal.reveal-ready[data-revealed="true"] {
  opacity: 1;
  animation: reveal-in 0.7s cubic-bezier(0.16, 1, 0.3, 1) backwards;
  animation-delay: calc(var(--reveal-d, 0) * 1s);
}

/* Group: only one observe for the container, direct children enter
   cascade. The offset lives in `--reveal-i` set by nth-child, which avoids
   to wrap each map of a grid in its own client component (and
   therefore to add a <div> which would break the grid). */
.reveal-group.reveal-ready > * {
  opacity: 0;
}
.reveal-group.reveal-ready[data-revealed="true"] > * {
  opacity: 1;
  animation: reveal-in 0.7s cubic-bezier(0.16, 1, 0.3, 1) backwards;
  animation-delay: calc(
    var(--reveal-d, 0) * 1s + var(--reveal-i, 0) * var(--reveal-step, 0.09s)
  );
}
.reveal-group > *:nth-child(2) { --reveal-i: 1; }
.reveal-group > *:nth-child(3) { --reveal-i: 2; }
.reveal-group > *:nth-child(4) { --reveal-i: 3; }
.reveal-group > *:nth-child(5) { --reveal-i: 4; }
.reveal-group > *:nth-child(6) { --reveal-i: 5; }
/* Beyond six, the offset is capped: a long list must not
   end up waiting a second after the first element. */
.reveal-group > *:nth-child(n + 7) { --reveal-i: 6; }

/* ─── Landing figure scenes (components/marketing/figures/story.tsx) ───
   The active scene's progress bar. Its duration and play state are set
   inline: it pauses with the figure's clock when the figure leaves the screen. */
@keyframes story-progress {
  from {
    transform: scaleX(0);
  }
  to {
    transform: scaleX(1);
  }
}
.story-progress {
  animation-name: story-progress;
  animation-timing-function: linear;
  animation-fill-mode: forwards;
}
@media (prefers-reduced-motion: reduce) {
  .story-progress {
    animation: none;
  }
}

/* ─── Section titles, verbatim (<RevealHeading>) ─────────────────────
   Same principle: observe on the title, the offset per word in CSS via
   `--reveal-i` placed inline. The readable text is carried by aria-label, the
   cutout in <span> is hidden from screen readers. */
.reveal-word {
  display: inline-block;
}
.reveal-heading.reveal-ready .reveal-word {
  opacity: 0;
}
.reveal-heading.reveal-ready[data-revealed="true"] .reveal-word {
  opacity: 1;
  animation: reveal-word-in 0.55s cubic-bezier(0.16, 1, 0.3, 1) backwards;
  animation-delay: calc(var(--reveal-i, 0) * var(--reveal-step, 0.05s));
}
@keyframes reveal-word-in {
  from {
    opacity: 0;
    transform: translateY(0.35em);
    filter: blur(6px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* Animate the inner wrapper: Radix disables outer transitions while measuring.
   Force-mounted answers stay in server HTML, including when collapsed. */
.faq-accordion [data-slot="accordion-content"] > div {
  display: grid;
  grid-template-rows: 0fr;
  visibility: hidden;
  opacity: 0;
  transition: grid-template-rows 280ms ease, opacity 200ms ease, visibility 280ms;
}

.faq-accordion [data-slot="accordion-content"][data-state="open"] > div {
  grid-template-rows: 1fr;
  visibility: visible;
  opacity: 1;
}

.faq-accordion [data-slot="accordion-trigger-icon"] {
  transition: transform 280ms ease;
}

.faq-accordion [data-slot="accordion-trigger-icon"]:last-child {
  display: none;
}

.faq-accordion [aria-expanded="true"] [data-slot="accordion-trigger-icon"]:first-of-type {
  display: block;
  transform: rotate(180deg);
}

@media (prefers-reduced-motion: reduce) {
  .faq-accordion [data-slot="accordion-content"] > div,
  .faq-accordion [data-slot="accordion-trigger-icon"] {
    transition: none;
  }
  .app-shell > div.flex.min-h-0.min-w-0.flex-1.flex-col {
    transition: none;
  }
}

/* ─── Settings section opened from ⌘K ──────────────────────────────
   The palette takes you to the right page AND the right tab, but a tab of
   settings is a column of cards that look similar: getting there doesn't say
   again WHICH we came to look for. The ring names it for the duration of a glance,
   puis `components/settings-shell.tsx` retire l'attribut.

   `box-shadow` rather than `outline`: it follows the radius of the card and fades
   in a single property, and it does not push anything — the column does not move one
   pixel for two seconds.

   No `fill-mode`: the arrival image is already transparent, and a shadow
   frozen would remain a layer composed for nothing. */
@keyframes settings-section-focus {
  0% {
    box-shadow:
      0 0 0 2px var(--ring),
      0 0 0 7px color-mix(in oklab, var(--ring) 22%, transparent);
  }
  55% {
    box-shadow:
      0 0 0 2px var(--ring),
      0 0 0 7px color-mix(in oklab, var(--ring) 22%, transparent);
  }
  100% {
    box-shadow:
      0 0 0 2px transparent,
      0 0 0 7px transparent;
  }
}
[data-settings-focus] {
  animation: settings-section-focus 2s cubic-bezier(0.16, 1, 0.3, 1);
}

/* “Less animation”: we don’t shorten it, we remove it. A duration almost
   null would still let the `both` fill apply the blur for
   the delay of each word — so a vague title until it starts. */
@media (prefers-reduced-motion: reduce) {
  .hero-word,
  .hero-reveal,
  .reveal.reveal-ready,
  .reveal.reveal-ready[data-revealed="true"],
  .reveal-group.reveal-ready > *,
  .reveal-group.reveal-ready[data-revealed="true"] > *,
  .reveal-heading.reveal-ready .reveal-word {
    opacity: 1;
    filter: none;
    transform: none;
    animation: none !important;
  }
  /* The ring REMAINS: it does not decorate, it designates. We take away his
     fading — the shell erases it after the same two seconds. */
  [data-settings-focus] {
    animation: none;
    box-shadow:
      0 0 0 2px var(--ring),
      0 0 0 7px color-mix(in oklab, var(--ring) 22%, transparent);
  }
}

/* ─── Text and background colors of pages (MIN-268) ────────────────────
   The document ONLY stores the name of the color (cf.
   components/pages/blocks/color.ts): here, and only here, is a name
   becomes a value — therefore a value per theme, and a change of theme
   which recolors the page without rewriting it.

   The ten source shades are those of the category labels
   (`CATEGORY_COLORS`, lib/category-colors.ts): minddy only has one palette.
   `lib/pages-color.test.ts` fails if the two diverge — it's the only link
   between a TS file and a style sheet, it cannot be typed.

   In dark, you don't put down a second palette: you lighten the first
   with the same gesture (`color-mix` towards white), to find the contrast that
   full tones lose on a black background. */
:root {
  --page-ink-red: #ef4444;
  --page-ink-orange: #f97316;
  --page-ink-amber: #eab308;
  --page-ink-green: #22c55e;
  --page-ink-teal: #14b8a6;
  --page-ink-blue: #3b82f6;
  --page-ink-indigo: #6366f1;
  --page-ink-violet: #a855f7;
  --page-ink-pink: #ec4899;
  --page-ink-gray: #6b7280;

  --page-color-red: var(--page-ink-red);
  --page-color-orange: var(--page-ink-orange);
  --page-color-amber: color-mix(in oklab, var(--page-ink-amber) 82%, black);
  --page-color-green: color-mix(in oklab, var(--page-ink-green) 88%, black);
  --page-color-teal: color-mix(in oklab, var(--page-ink-teal) 88%, black);
  --page-color-blue: var(--page-ink-blue);
  --page-color-indigo: var(--page-ink-indigo);
  --page-color-violet: var(--page-ink-violet);
  --page-color-pink: var(--page-ink-pink);
  --page-color-gray: var(--page-ink-gray);
}

.dark {
  --page-color-red: color-mix(in oklab, var(--page-ink-red) 72%, white);
  --page-color-orange: color-mix(in oklab, var(--page-ink-orange) 78%, white);
  --page-color-amber: color-mix(in oklab, var(--page-ink-amber) 82%, white);
  --page-color-green: color-mix(in oklab, var(--page-ink-green) 80%, white);
  --page-color-teal: color-mix(in oklab, var(--page-ink-teal) 80%, white);
  --page-color-blue: color-mix(in oklab, var(--page-ink-blue) 70%, white);
  --page-color-indigo: color-mix(in oklab, var(--page-ink-indigo) 68%, white);
  --page-color-violet: color-mix(in oklab, var(--page-ink-violet) 72%, white);
  --page-color-pink: color-mix(in oklab, var(--page-ink-pink) 74%, white);
  --page-color-gray: color-mix(in oklab, var(--page-ink-gray) 60%, white);
}

[data-page-text="red"] { color: var(--page-color-red); }
[data-page-text="orange"] { color: var(--page-color-orange); }
[data-page-text="amber"] { color: var(--page-color-amber); }
[data-page-text="green"] { color: var(--page-color-green); }
[data-page-text="teal"] { color: var(--page-color-teal); }
[data-page-text="blue"] { color: var(--page-color-blue); }
[data-page-text="indigo"] { color: var(--page-color-indigo); }
[data-page-text="violet"] { color: var(--page-color-violet); }
[data-page-text="pink"] { color: var(--page-color-pink); }
[data-page-text="gray"] { color: var(--page-color-gray); }

[data-page-back="red"] { --page-tint: var(--page-color-red); }
[data-page-back="orange"] { --page-tint: var(--page-color-orange); }
[data-page-back="amber"] { --page-tint: var(--page-color-amber); }
[data-page-back="green"] { --page-tint: var(--page-color-green); }
[data-page-back="teal"] { --page-tint: var(--page-color-teal); }
[data-page-back="blue"] { --page-tint: var(--page-color-blue); }
[data-page-back="indigo"] { --page-tint: var(--page-color-indigo); }
[data-page-back="violet"] { --page-tint: var(--page-color-violet); }
[data-page-back="pink"] { --page-tint: var(--page-color-pink); }
[data-page-back="gray"] { --page-tint: var(--page-color-gray); }

/* A background, not a highlighter: a very diluted shade, a radius, and
   `box-decoration-break` so that a passage that runs on two lines keeps its
   rounded corners on BOTH ends rather than in the middle. */
[data-page-back] {
  background-color: color-mix(in oklab, var(--page-tint) 16%, transparent);
  border-radius: 0.25rem;
  padding: 0.08em 0.18em;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}

.dark [data-page-back] {
  background-color: color-mix(in oklab, var(--page-tint) 24%, transparent);
}

/* In a code block a chosen text color overrides the syntax highlighting: a
   highlight span inside the color mark takes the mark's color (a mark inside
   a highlight span colors itself). "Default" sets no mark, so the
   highlighting stays; a background keeps the token colors. */
pre [data-page-text] [class*="hljs-"] {
  color: inherit;
}

/* Equations (components/pages/blocks/math.ts). KaTeX sets no color, so an
   equation takes the text color of its theme; its error red is the app's
   destructive token. A display equation wider than the column scrolls inside
   its block instead of overflowing the page. */
.page-math {
  border-radius: 0.375rem;
}
.page-math-inline {
  padding: 0 0.125rem;
}
.page-math-block {
  margin: 0.5rem 0;
}
.page-math-scroll {
  overflow-x: auto;
  overflow-y: hidden;
  padding: 0.25rem 0;
}
.page-math-scroll .katex-display,
.page-math-preview .katex-display {
  margin: 0;
}
.page-math.is-empty {
  color: var(--muted-foreground);
  font-size: 0.875em;
}
.page-math-block.is-empty {
  padding: 0.5rem 0.75rem;
  background-color: var(--control);
  text-align: center;
}
@media (hover: hover) and (pointer: fine) {
  .ProseMirror[contenteditable="true"] .page-math:hover:not(.ProseMirror-selectednode, .is-inline-selected) {
    background-color: var(--control);
  }
}
.page-math .katex-error,
.page-math-preview .katex-error {
  color: var(--destructive) !important;
}

/* A callout carries a palette name, never a fixed color. The same document is
   therefore legible in both themes, and the editor, published page, history,
   and print view all render the same semantic `<aside>`. */
[data-page-callout-color="red"] { --page-callout-tone: var(--page-color-red); }
[data-page-callout-color="orange"] { --page-callout-tone: var(--page-color-orange); }
[data-page-callout-color="amber"] { --page-callout-tone: var(--page-color-amber); }
[data-page-callout-color="green"] { --page-callout-tone: var(--page-color-green); }
[data-page-callout-color="teal"] { --page-callout-tone: var(--page-color-teal); }
[data-page-callout-color="blue"] { --page-callout-tone: var(--page-color-blue); }
[data-page-callout-color="indigo"] { --page-callout-tone: var(--page-color-indigo); }
[data-page-callout-color="violet"] { --page-callout-tone: var(--page-color-violet); }
[data-page-callout-color="pink"] { --page-callout-tone: var(--page-color-pink); }
[data-page-callout-color="gray"] { --page-callout-tone: var(--page-color-gray); }

.page-editor [data-type="callout"] {
  display: grid;
  grid-template-columns: 1.75rem minmax(0, 1fr);
  align-items: start;
  gap: 0.625rem;
  margin-block: 0.75rem;
  padding: 0.875rem 1rem;
  border: 0;
  border-radius: 0.75rem;
  background-color: color-mix(in oklab, var(--page-callout-tone) 12%, transparent);
}

.dark .page-editor [data-type="callout"] {
  background-color: color-mix(in oklab, var(--page-callout-tone) 20%, transparent);
}

.page-editor .page-callout-icon {
  display: flex;
  width: 1.75rem;
  height: 1.75rem;
  align-items: center;
  justify-content: center;
  transform: translateY(var(--page-callout-icon-offset, 0px));
  padding: 0;
  border: 0;
  border-radius: 0.375rem;
  background: transparent;
  font-size: 1.375rem;
  line-height: 1;
}

.page-editor .page-callout-icon-button {
  cursor: default;
  color: inherit;
}

@media (hover: hover) and (pointer: fine) {
  .page-editor .page-callout-icon-button:hover {
    background-color: color-mix(in oklab, var(--page-callout-tone) 16%, transparent);
  }
}

.page-editor .page-callout-icon-button:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
}

.page-editor .page-callout-content:only-child {
  grid-column: 1 / -1;
}

.page-editor .page-callout-content > :first-child {
  margin-top: 0;
}

.page-editor .page-callout-content > :last-child {
  margin-bottom: 0;
}

/* The paragraph of a task otherwise inherits the `[&_p]:my-2` of the body of the page, this
   which pushes the TEXT down and leaves the checkbox alone in
   high. The notebook has the same rule, for the same reason. */
.page-editor .ProseMirror li[data-type="taskItem"] p {
  margin: 0;
}

/* Find and replace (components/editor/find-bar.tsx): every match takes a
   light tint of the accent behind text that keeps its own color, like the
   text selection; the current one a stronger tint and an accent outline, so
   it stands out by shape as well as by color. A black or white accent gives
   gray tints, still told apart by the outline. */
.page-editor .ProseMirror .ProseMirror-search-match,
.page-editor .ProseMirror .ProseMirror-active-search-match {
  border-radius: 2px;
  box-decoration-break: clone;
  -webkit-box-decoration-break: clone;
  background-color: color-mix(in oklab, var(--primary) 22%, transparent);
}
.page-editor .ProseMirror .ProseMirror-active-search-match {
  background-color: color-mix(in oklab, var(--primary) 42%, transparent);
  box-shadow: 0 0 0 1px var(--primary);
}
@media (forced-colors: active) {
  .page-editor .ProseMirror .ProseMirror-search-match,
  .page-editor .ProseMirror .ProseMirror-active-search-match {
    background-color: Mark;
    color: MarkText;
  }
  .page-editor .ProseMirror .ProseMirror-active-search-match {
    outline: 2px solid Highlight;
  }
}

/* ─── The GUTTER flies over ITSELF ───────────────────────────────────
   The chrome of a block (the handle and the `+`) appears when hovering over the block, and
   not when hovering over the empty strip to your left — even though that's THERE we're going
   look for the handle. The gesture learned elsewhere is however: aim for the margin,
   the buttons appear. So it was necessary to cross the column of text
   first, then go back.

   The cause is in a nutshell: the extension listens to the `mousemove` of the VIEW
   ProseMirror (`handleDOMEvents`), and the gutter is outside of this
   box — no event is born there. However, she already knows what to do with a
   pointer outside the column: it folds the coordinates into the content before
   look for the block (`clampToContent`), so hover at the height of a block but
   to the left of him clearly indicates this block.

   It is therefore enough for the view box to ARRIVE there. A padding to
   left extends it under the gutter; the exact opposite negative margin
   put the text back where it was. Nothing moves on the screen, and the empty tape is
   now part of the listening surface.

   The two values ​​are the RESERVE column (`GUTTER_HOVER`,
   components/pages/block-actions.ts), not the width of the buttons: these do not
   hold only the 66 px stuck to the text, but the gutter that we see — and
   that we aim for — is the entire margin. The first version stopped at
   buttons, and left 42 dead px at the left edge, where the mouse arrives
   when it comes from the sidebar. Not a pixel more either: beyond that, the
   box would come out of the column.

   In mobile layout, the column is only 24px indented: the extended box would overflow
   of the container that passes by. And there's no hovering on a touchscreen. */
/* The gutter's handle and `+` fade in and out as the pointer moves from block
   to block. The extension toggles an inline `visibility`; the transition on
   it holds the buttons visible while their opacity falls, so the fade-out
   plays too. */
.page-editor .drag-handle {
  transition: opacity 120ms ease-out, visibility 120ms ease-out;
}
.page-editor .drag-handle[style*="visibility: hidden"] {
  opacity: 0;
}
/* A block dragged by its handle (components/pages/block-drag.ts): the grab
   cursor everywhere for the whole gesture, a translucent copy of the blocks
   under the pointer, and a line where they will land. */
html.block-dragging,
html.block-dragging * {
  cursor: grabbing !important;
  user-select: none !important;
}
/* The selection rectangle drawn from beside a page's text
   (components/editor/block-marquee.ts), the board's one. It sits above
   dialogs, since the task notebook is one, like the dragged blocks' copy.
   While a rectangle is drawn, here or on a board, the cursor stays the
   default arrow: neither the text cursor of the margin nor a card's hand. */
html.marquee-drawing,
html.marquee-drawing * {
  cursor: default !important;
}
.block-marquee {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 80;
  border: 1px solid color-mix(in oklab, var(--primary) 70%, transparent);
  border-radius: 3px;
  background: color-mix(in oklab, var(--primary) 15%, transparent);
  pointer-events: none;
}
.block-drag-ghost {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 80;
  max-height: 14rem;
  overflow: hidden;
  border-radius: 0.5rem;
  background: var(--background);
  box-shadow: var(--shadow-md, 0 4px 12px rgb(0 0 0 / 0.12));
  opacity: 0.6;
  pointer-events: none;
}
.block-drag-line {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 80;
  height: 2px;
  border-radius: 1px;
  background: var(--primary);
  pointer-events: none;
}

/* Outside its hover zone (the left margin and the left 70% of the text,
   components/pages/block-gutter.tsx) the gutter stays hidden even while the
   extension tracks a block: the right part belongs to the comment button. */
.page-editor[data-hover-zone]:not([data-hover-zone="gutter"]) .drag-handle {
  opacity: 0 !important;
  visibility: hidden !important;
  pointer-events: none !important;
}
@media (prefers-reduced-motion: reduce) {
  .page-editor .drag-handle { transition: none; }
}

:root[data-app-layout="desktop"] {
  .page-editor[data-gutter] .ProseMirror {
    padding-left: 96px;
    margin-left: -96px;
  }
  /* The right reserve of a page column (`app-desktop:pr-10`), where the
     hovered block's comment button sits. */
  .page-editor[data-comment-gutter] .ProseMirror {
    padding-right: 40px;
    margin-right: -40px;
  }
}

/* The comment button on the right of a hovered page block fades with the
   gutter, and is placed by script in the drag handle's wrapper — which
   ignores the pointer, so the button takes it back, as the handle does. */
.page-editor [data-block-comment-button] {
  position: absolute;
  z-index: 1;
  pointer-events: auto;
  transition: opacity 120ms ease-out, visibility 120ms ease-out;
}
.page-editor [data-block-comment-button][data-shown="false"] {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
  .page-editor [data-block-comment-button] { transition: none; }
}
/* While the comment button is hovered, its block wears the block-selection
   box (the "Selection boxes" rules below), so the button names its block. */
.page-editor [data-block-comment-preview] {
  position: absolute;
  /* Sized to the block by script; the box reaches past it as a selected
     block's does. */
  box-sizing: content-box;
  padding: var(--block-selection-y) var(--block-selection-x);
  translate: calc(-1 * var(--block-selection-x)) calc(-1 * var(--block-selection-y));
  border-radius: var(--block-selection-radius);
  background-color: var(--selection-block);
  pointer-events: none;
  transition: opacity 120ms ease-out;
}
.page-editor [data-block-comment-preview][data-shown="false"] {
  opacity: 0;
}

/* ─── The disclosure block (MIN-270) ─────────────────────────────────────
   TipTap renders the outer details node as a div with an empty fallback
   button, while the summary remains a native `<summary>`. The custom chevron
   supplies the control, so the browser's native summary marker must be removed;
   otherwise both indicators occupy the same left edge.

   The layout follows Notion: a chevron in the margin, a medium-weight summary
   beside it, and the content aligned below the summary. */
.page-editor [data-type="details"] {
  display: grid;
  grid-template-columns: 1.25rem 1fr;
  align-items: start;
  column-gap: 0.25rem;
  margin: 0.5rem 0;
}

/* The button lives in the first column, the rest of the block in the second. */
.page-editor .page-details-toggle {
  grid-column: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.25rem;
  /* The height of a line of text: the chevron centers on the summary, not
     on the top of the block. */
  height: 1.75rem;
  border-radius: 0.25rem;
  color: var(--muted-foreground);
  cursor: default;
  transition: background-color 0.12s, transform 0.15s;
}
@media (hover: hover) and (pointer: fine) {
  .page-editor .page-details-toggle:hover {
    background-color: var(--muted);
    color: var(--foreground);
  }
}
.page-editor .page-details-toggle svg {
  width: 0.875rem;
  height: 0.875rem;
}
.page-editor [data-type="details"].is-open > .page-details-toggle {
  transform: rotate(90deg);
}

.page-editor [data-type="details"] > div:not(.page-details-toggle) {
  grid-column: 2;
  min-width: 0;
}
.page-editor .page-details-summary {
  display: block;
  list-style: none;
  font-weight: 500;
  line-height: 1.75rem;
}
.page-editor .page-details-summary::-webkit-details-marker {
  display: none;
}
/* The folded body is hidden by the `hidden` attribute that the node view sets;
   unfolded, it breathes a little under its summary. */
.page-editor [data-type="detailsContent"] > :first-child {
  margin-top: 0.25rem;
}

@media (prefers-reduced-motion: reduce) {
  .page-editor .page-details-toggle {
    transition: none;
  }
}

/* The placeholder of an empty block (MIN-270). The plugin
   (components/pages/block-placeholder.ts) sets `data-placeholder` and class
   `is-empty` on the cursor block — it doesn't paint anything itself. The notebook has its
   twin ruler a few hundred lines higher; the two surfaces are
   scoped separately so that the typography of one does not overflow
   l'autre. */
.page-editor .is-empty::before {
  content: attr(data-placeholder);
  position: absolute;
  inset: 0 auto auto 0;
  pointer-events: none;
  color: var(--muted-foreground);
  opacity: 0.5;
}

.page-editor .is-empty {
  position: relative;
}

/* A one-line field (the comment composers) grows to hold a placeholder that
   wraps — on a phone, its 16px text often does — instead of clipping its
   second line under the field's edge. The placeholder becomes a float whose
   negative margin cancels its width: the caret's line still starts at the
   left over its first line, and the `flow-root` paragraph includes the
   float's height. Paragraphs only, since `flow-root` on a list item would
   drop its marker. */
.placeholder-grows .page-editor p.is-empty {
  display: flow-root;
}
.placeholder-grows .page-editor p.is-empty::before {
  position: static;
  float: left;
  width: 100%;
  margin-right: -100%;
}

/* ─── The blink of a block (MIN-270) ───────────────────────────────────
   Two surfaces use it, and for the same reason: after scrolling,
   nothing distinguishes the block we REQUESTED from its neighbors arriving on the screen
   at the same time. The anchor of a link “copy block link” turns it on
   opening the page; the table of contents turns it on when clicked.

   It's a BLINK, not a selection: it erases itself, otherwise we
   would believe the highlighted block for real, and it overflows text
   (`box-shadow`) to designate the entire block, not its words.

   The color does NOT come from --primary, and that's what was missing: under
   the Noir / Blanc presets --primary is INK — almost black in light, almost
   white in dark — and any accent preset can recolor it. An 18% ink
   background is a pale gray which, while scrolling, cannot be seen passing; the blink was there and did not
   meant nothing. It needs a shade of ATTENTION, which neither resembles the
   text nor to a selection: the blue of the block color register, diluted
   enough to remain soft, and already available in both themes.

   A PULSE rather than a solid background: two beats are better than a solid,
   because it is CHANGE that the eye catches — especially when the
   scroll just moved the whole page under it.

   Under `prefers-reduced-motion`, we do not reduce the duration to zero (this is
   which made the old version invisible to those who checked the box): we
   remove the beat and HOLD the bottom. It's `flashBlock`
   (components/pages/block-actions.ts) which sets the class and removes it. */
/* A COMMENTED block (MIN-282) — “there is a discussion here” — in the
   comments' yellow (the palette's amber). Placed by a ProseMirror decoration
   (components/pages/block-comments.ts), not by a mark: it is not content, it
   reaches neither the database nor the Markdown projection. The block keeps
   its neighbors' layout, and the gutter its place: its badge in the right
   margin and its passages say it. */
.page-block-commented {
  position: relative;
  /* The tint behind a commented passage, and the badge's ink. */
  --page-comment-accent: var(--page-ink-amber);
  --page-comment-ink: var(--page-color-amber);
}

/* A frozen excerpt is underlined and tinted only while the same text still
   exists in the commented block. Like the badge, this is an editor
   decoration rather than document content. */
.page-commented-passage {
  text-decoration-line: underline;
  text-decoration-color: var(--page-comment-accent);
  text-decoration-thickness: 2px;
  text-underline-offset: 0.2em;
  /* One unbroken line, drawn over the descenders of y, g or q. */
  text-decoration-skip-ink: none;
  border-radius: 0.2em;
  background-color: color-mix(in oklab, var(--page-comment-accent) 26%, transparent);
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}

/* The BADGE of a commented block: the new-comment button's twin
   (components/pages/block-gutter.tsx) — the same button, at the same place,
   with the message count in its corner — and it stays while the pointer is
   elsewhere. Out of flow, in the RIGHT margin, so a commented block keeps
   exactly its neighbors' layout; components/pages/block-comments.ts places
   it, as the gutter places the new-comment button. */
.page-block-comment-host {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 1;
  user-select: none;
}

.page-block-comment-badge {
  color: var(--page-comment-ink);
  cursor: default;
}

@media (hover: hover) and (pointer: fine) {
  .page-block-comment-badge:hover {
    color: var(--page-comment-ink);
    background-color: color-mix(in oklab, var(--page-comment-accent) 18%, transparent);
  }
}

.page-block-comment-count {
  position: absolute;
  top: -0.25rem;
  right: -0.3125rem;
  min-width: 0.875rem;
  height: 0.875rem;
  padding: 0 0.1875rem;
  border-radius: 999px;
  font-size: 0.5625rem;
  font-weight: 600;
  line-height: 0.875rem;
  text-align: center;
  font-variant-numeric: tabular-nums;
  color: var(--background);
  background-color: var(--page-comment-ink);
}

/* Below the width where the right margin exists, the pad returns INTO the stream,
   at the end of the block: better a shift of one line than a button that comes out of
   the screen and you can no longer click. */
@media (max-width: 48rem) {
  .page-block-comment-host {
    position: static;
    display: inline-block;
    margin-left: 0.375rem;
    vertical-align: middle;
  }
}

/* Where a dragged file will land (components/editor/file-drop.ts): a gap
   between two top-level blocks. The widget has no height, so nothing below it
   moves while the file is dragged; the line is painted over the gap. */
.editor-file-drop-line {
  position: relative;
  height: 0;
  pointer-events: none;
}
.editor-file-drop-line::before {
  content: "";
  position: absolute;
  inset-inline: 0;
  top: -1px;
  height: 2px;
  border-radius: 1px;
  background: var(--ring);
}

.page-block-target,
.inbox-row-target {
  position: relative;
  border-radius: 0.375rem;
  animation: page-block-pulse 1.6s ease-in-out;
}

@keyframes page-block-pulse {
  0%,
  100% {
    background-color: transparent;
    box-shadow: 0 0 0 6px transparent;
  }
  12%,
  62% {
    background-color: color-mix(in oklab, var(--page-color-blue) 26%, transparent);
    box-shadow: 0 0 0 6px
      color-mix(in oklab, var(--page-color-blue) 26%, transparent);
  }
  37% {
    background-color: color-mix(in oklab, var(--page-color-blue) 9%, transparent);
    box-shadow: 0 0 0 6px
      color-mix(in oklab, var(--page-color-blue) 9%, transparent);
  }
}

@media (prefers-reduced-motion: reduce) {
  .page-block-target,
  .inbox-row-target {
    animation: none;
    background-color: color-mix(in oklab, var(--page-color-blue) 26%, transparent);
    box-shadow: 0 0 0 6px
      color-mix(in oklab, var(--page-color-blue) 26%, transparent);
  }
}

/* ── PRINTING a document (MIN-283) ─────────────────────────────────────────
   The PDF of a page is what the browser prints: no PDF engine
   server, so no second definition of the layout to hold. What he
   you have to give it, it’s the document ONLY — without the application chrome or
   that of the public page.

   Masking goes through `visibility` and not through `display: none`: a
   `display: none` on the ancestors would take the document with them, and a
   container in `position: fixed` would only print on the first sheet.
   `visibility` leaves the flow intact, so the pagination too.

   `data-print-mode` is set by the print view (page-print-view.tsx);
   on a published page there is no chrome to hide and a plain ⌘P
   is enough — hence the unconditional rules that follow. */
@media print {
  html[data-print-mode] body * {
    visibility: hidden;
  }
  html[data-print-mode] [data-print-root],
  html[data-print-mode] [data-print-root] * {
    visibility: visible;
  }
  html[data-print-mode] [data-print-root] {
    position: absolute;
    inset-inline: 0;
    top: 0;
    max-width: none;
    padding: 0;
  }

  /* A link remains readable; its address is not printed — a document
     riddled with URLs in parentheses is unreadable, and minddy's are not
     lead nowhere for whoever receives the PDF. */
  a {
    text-decoration: none;
    color: inherit;
  }

  /* No background or shadow: the ink costs, and a block of code on a gray background comes out
     dirty solid color on most printers. */
  body {
    background: #fff;
  }
}

/* ── Desktop app: where the window moves ─────────────────────────────
   MIN-291. The shell hides the native title bar (`hiddenInset`) so that
   minddy does not have a gray band above its own header. In exchange,
   macOS no longer knows where to enter the window: `-webkit-app-region` is a
   CSS property, so the answer can only come from here. Without these rules, the
   window does not move AT ALL.

   The attribute is set to <html> by components/desktop-chrome.tsx, only
   when the bridge exists. In a browser, nothing in this block applies. */

/* ── THE GAND, AND SHE’S THE ONLY ONE ───────────────────── ─────────────────────
   MIN-292. The socket lived in the shell header and in the mark line of
   the sidebar — that is to say in the two pieces of furniture that half of the
   screens do not have. An audit found SIX without any sockets, and the window there
   was strictly still:

     zen mode (it removes the header AND the bar), legal pages readable in
     the app, public board `/f/`, published page `/p/`, shared view `/share/`,
     and `not-found`.

   Searching for the screens one by one to add a strip to them means signing for
   start again at the next one. Hence the inversion: **a single strip, in the layout
   root, on everything the window displays.**

   Its 44 px height matches the native application bar. Authenticated desktop
   layouts disable this fallback while their own bar provides the drag region.

   It starts from `top: 0`: the traffic lights are drawn by macOS
   ABOVE the web view and it is the system that serves them, not the page. */

.desktop-drag-band {
  display: none;
}
html[data-desktop-platform="darwin"] .desktop-drag-band {
  display: block;
  position: fixed;
  inset: 0 0 auto 0;
  height: 44px;
  /* Under the veils of dialogue (50): a strip that would move the window
     over an open modal would be a hold where the app says "reply
     d'abord ». */
  z-index: 30;
  -webkit-app-region: drag;
}

/* ⚠ **A `drag` zone SWALLOWS the event**, including what is drawn
   over: regions are rectangles sent to the system, resolved
   before the page sees anything. Without this global exception, everything
   what clicks in the first 44 pixels of any screen
   would become a window handle — the palette, the “New” button, the
   tabs of a board, the link of a legal page.

   Global, therefore, and not scoped to the band: `no-drag` on an element which is not
   in any region `drag` does nothing at all. The cost is zero, and the scope
   is what makes the rule true without having to know WHO is at the top of the screen.

   It is written AFTER the tape, and the order of the document matters as much as
   that of the sheet: Chromium traverses the layout tree and a
   `no-drag` digs the `drag` encountered before it. This is why the band is
   rendered before `{children}` in app/layout.tsx. */
html[data-desktop-platform="darwin"]
  :is(
    a,
    button,
    input,
    select,
    textarea,
    label,
    summary,
    [role="button"],
    [role="link"],
    [role="menuitem"],
    [role="combobox"],
    [role="tab"],
    [role="option"],
    [role="checkbox"],
    [role="switch"],
    [contenteditable="true"],
    [tabindex]
  ) {
  -webkit-app-region: no-drag;
}

/* Content-pane headers remain a window handle on macOS. Declaring the drag
   region on the header itself lets its interactive descendants reliably carve
   out clickable no-drag areas while every empty part still moves the window. */
html[data-desktop-platform="darwin"] .app-content-header {
  -webkit-app-region: drag;
}

/* A recalled navigation panel overlaps content headers. Chromium's native
   draggable-region hit testing can otherwise keep the covered header active,
   making the sidebar rows under that band visible but unclickable. */
html[data-desktop-app] .sidebar-nav-panel {
  -webkit-app-region: no-drag;
}

/* The no-drag above is not enough, though: regions are collected in layout
   order and the content header sits AFTER this panel in the DOM (the sidebar
   renders before the pane's children), so a `no-drag` only digs `drag` rects
   met earlier. Over the overlap the header's `drag` rect wins, and with the
   sidebar hidden the header spans the whole window — the panel's top strip
   (search, filter, new) stays visible but inert on macOS. While the floating
   panel is actually shown, the header therefore gives up its window handle;
   the app top bar and the rest of the chrome keep dragging the window. */
html[data-desktop-platform="darwin"] body[data-sidebar-floating="true"]
  .app-content-header {
  -webkit-app-region: no-drag;
}

html[data-desktop-platform="darwin"] .app-content-header
  :is(
    a,
    button,
    input,
    select,
    textarea,
    label,
    summary,
    [role="button"],
    [role="link"],
    [role="menuitem"],
    [role="combobox"],
    [role="tab"],
    [role="option"],
    [role="checkbox"],
    [role="switch"],
    [contenteditable="true"],
    [tabindex]
  ) {
  -webkit-app-region: no-drag;
}

/* The secondary sidebar is teleported into the shell gutter. His line
   of title is therefore not in the same subtree as the fixed band of
   displacement, which otherwise covers it with its z-index. Place it above
   this strip allows the Search, Filter and Add buttons to receive
   their events; interactive descendants explicitly remain out
   of the travel area. */
html[data-desktop-platform="darwin"] .secondary-sidebar-header {
  position: relative;
  z-index: 31;
  -webkit-app-region: drag;
}

html[data-desktop-platform="darwin"] .secondary-sidebar-header
  :is(
    a,
    button,
    input,
    select,
    textarea,
    label,
    summary,
    [role="button"],
    [role="link"],
    [role="menuitem"],
    [role="combobox"],
    [role="tab"],
    [role="option"],
    [role="checkbox"],
    [role="switch"],
    [contenteditable="true"],
    [tabindex]
  ) {
  -webkit-app-region: no-drag;
}

/* ⚠ **The SAILS are dug, too** (MIN-308).

   The digging above only targets FOCUSABLE elements. Radix content
   carries `tabindex="-1"`, it is therefore taken; but the sails, no — they don’t
   only carry a `data-slot`, and the one on the palette comes from its own package
   and wore nothing at all. But “click next to close” is a click ON
   the veil: in the first 44 pixels, it fell into the band, macOS
   swallowed it, and the window began to slide under the cursor instead of the
   dialog closes. Deterministic, a hundred times out of a hundred, and only above
   44 px.

   Assumed counterpart: sail open, the strip no longer moves the window.
   This is the same choice as that already made for the band itself (`z-index: 30`,
   under sail at 50) — a catch where the app says “answer first”. */
html[data-desktop-platform="darwin"]
  :is(
    [data-slot="dialog-overlay"],
    [data-slot="alert-dialog-overlay"],
    [data-slot="sheet-overlay"],
    [data-slot="side-panel-overlay"],
    [data-slot="drawer-overlay"],
    .desktop-palette-overlay
  ) {
  -webkit-app-region: no-drag;
}

/* The sidebar inset now contains creation actions beneath the application bar. */
.sidebar-brand-row { -webkit-app-region: no-drag; }

/* ─── Toasts (sonner) — Geist-style solid status pills ──────────────────────
   Repaints every typed toast as a self-contained colored pill with white
   icon + text, after Vercel's Geist design system: blue for informational,
   blue for confirmations, orange for warning, red for error — instantly
   scannable instead of the previous neutral card look shared by all types.

   HOW: sonner gates its per-type colors behind `richColors` and injects its
   stylesheet into <head> at RUNTIME, i.e. potentially AFTER this sheet. Its
   type rules ([data-rich-colors]…) and its base rule both sit at
   specificity (0,2,0), so equal-specificity overrides here would race the
   injection order and lose. These selectors add enough specificity and set
   the final properties directly, so they win regardless of order — and
   `richColors` stays off, nothing else about sonner changes.

   Bare `toast(...)` calls (data-type="default") use the same blue pill as
   informational toasts: the app has no semantic success state for these
   generic confirmations.

   One palette for both themes: the pill carries its own foreground, so it
   interacts with neither page. Retune here, everything follows.
   Reference for design handoff:
     info    #0070F3 · success #0070F3 · warning #DC5A00 · error #EE0000 */
:root {
  --toast-info: #0070f3;
  --toast-success: #0070f3;
  --toast-warning: #dc5a00;
  --toast-error: #ee0000;
}

/* Corner radius: sonner ships 8px via `--border-radius` on the toaster
   (runtime-injected at (0,1,0)); `[data-y-position]` is always present and
   lifts the override to (0,2,0), so it wins regardless of injection order. */
[data-sonner-toaster][data-y-position] {
  --border-radius: 14px;
}

[data-sonner-toaster]
  [data-sonner-toast][data-styled="true"] {
  padding-right: 48px;
  border-radius: 14px;
}

/* Keep the dismiss control inside the toast at the right edge. */
[data-sonner-toaster][data-y-position]
  [data-sonner-toast][data-styled="true"]
  [data-close-button] {
  position: absolute;
  top: 50%;
  right: 12px;
  left: auto;
  width: 24px;
  height: 24px;
  color: currentColor;
  background: transparent;
  border: 0;
  border-radius: 6px;
  transform: translateY(-50%);
}

@media (hover: hover) and (pointer: fine) {
  [data-sonner-toaster][data-y-position]
    [data-sonner-toast][data-styled="true"]
    [data-close-button]:hover {
    background: rgb(255 255 255 / 16%);
  }
}

[data-sonner-toaster]
  [data-sonner-toast][data-styled="true"]:not([data-type]),
[data-sonner-toaster]
  [data-sonner-toast][data-styled="true"][data-type="default"] {
  background: var(--toast-info);
  border-color: var(--toast-info);
  color: #fff;
}

[data-sonner-toaster]
  [data-sonner-toast][data-styled="true"][data-type="info"] {
  background: var(--toast-info);
  border-color: var(--toast-info);
  color: #fff;
}

[data-sonner-toaster]
  [data-sonner-toast][data-styled="true"][data-type="success"] {
  background: var(--toast-success);
  border-color: var(--toast-success);
  color: #fff;
}

[data-sonner-toaster]
  [data-sonner-toast][data-styled="true"][data-type="warning"] {
  background: var(--toast-warning);
  border-color: var(--toast-warning);
  color: #fff;
}

[data-sonner-toaster]
  [data-sonner-toast][data-styled="true"][data-type="error"] {
  background: var(--toast-error);
  border-color: var(--toast-error);
  color: #fff;
}

/* The portal lands in the pane, outside <main>, so scrolling never moves it.
   Sidebar resizing and the ultrawide canvas resize this containing block. */
.app-shell > div.flex.min-h-0.min-w-0.flex-1.flex-col {
  position: relative;
}

.content-toaster[data-sonner-toaster][data-x-position="center"] {
  position: absolute;
  left: 50%;
  right: auto;
  bottom: 1rem;
  width: calc(100% - 2rem);
  max-width: 26rem;
  transform: translateX(-50%);
  /* Keep action feedback readable above elevated dialogs and menus. */
  z-index: 1200;
}

.content-toaster[data-sonner-toaster] [data-sonner-toast][data-styled="true"] {
  left: 50%;
  right: auto;
  translate: -50% 0;
  width: fit-content;
  max-width: 100%;
  min-height: 32px;
  padding: 6px 40px 6px 12px;
  gap: 8px;
  border-radius: 9999px;
}

.content-toaster[data-sonner-toaster] [data-sonner-toast][data-styled="true"] [data-content] {
  display: flex;
  flex: 1;
  min-width: 0;
  overflow: hidden;
  flex-direction: row;
  align-items: center;
  gap: 8px;
}

.content-toaster[data-sonner-toaster] [data-sonner-toast][data-styled="true"] [data-title],
.content-toaster[data-sonner-toaster] [data-sonner-toast][data-styled="true"] [data-description] {
  flex: 0 1 auto;
  min-width: 0;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 13px;
  line-height: 20px;
  color: inherit;
}

.content-toaster[data-sonner-toaster] [data-icon],
.content-toaster[data-sonner-toaster] [data-button] {
  flex-shrink: 0;
}

:root[data-app-layout="mobile"] {
  .content-toaster[data-sonner-toaster][data-x-position="center"] {
    bottom: calc(var(--mobile-nav-clearance) + 1rem);
  }
}

/* The full-width application bar and lower workspace share one viewport. */
.app-workspace > .app-shell {
  height: auto;
  min-height: 0;
  flex: 1;
  max-height: none;
  max-width: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  padding-top: 0;
}
.app-workspace { padding-top: env(safe-area-inset-top); }
.app-titlebar-safe-area { height: 44px; }
/* The workspace fills the viewport at every screen size. */
html:not([data-desktop-app]) .ultrawide-canvas {
  display: block;
  padding: 0;
}
html:not([data-desktop-app]) .app-workspace {
  width: 100%;
  max-width: none;
  max-height: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
}

:root[data-app-layout="desktop"] {
  .app-top-bar { display: flex; }
  &[data-desktop-app][data-app-top-bar] .desktop-drag-band { display: none; }
}
html[data-desktop-app] .app-top-bar,
html[data-desktop-app] .app-content-header { -webkit-app-region: drag; }
html[data-desktop-app] :is(button, a, input, textarea, select, [role="tab"], [role="tablist"], [role="menu"], [role="menuitem"], [role="tooltip"], [role="dialog"], [contenteditable="true"], [tabindex]) {
  -webkit-app-region: no-drag;
}
html[data-integrated-caption-controls]:not([data-desktop-platform="darwin"]) .app-titlebar-safe-area {
  margin-left: env(titlebar-area-x, 0px);
  width: env(titlebar-area-width, 100%);
  flex: none;
}
html[data-integrated-caption-controls]:not([data-app-top-bar]) .desktop-drag-band {
  display: block;
  position: fixed;
  inset: 0 0 auto;
  height: 44px;
  z-index: 30;
  -webkit-app-region: drag;
}
:root[data-app-layout="mobile"] {
  &[data-integrated-caption-controls]:not([data-desktop-platform="darwin"]) .app-workspace::before {
    content: "";
    display: block;
    height: 44px;
    flex-shrink: 0;
    -webkit-app-region: drag;
  }
}
/* ── Desktop window material (MIN-810) ─────────────────────────────────────
   In the desktop app on macOS and Windows 11, the window is transparent and
   the system paints a blurred view of the desktop behind it
   (`lib/desktop/window-material.ts`). The chrome around the content pane
   (tab strip, sidebar, the right and bottom bands, the Windows caption area)
   lets it through; the floating content pane keeps its opaque background.

   `--sidebar` becomes transparent inside the workspace only, so every
   `bg-sidebar` surface of the chrome clears at once without stacking veils.
   A single veil on <body> — painted on the canvas — keeps the chrome's tint
   and its text contrast on a very light or very dark wallpaper; the native
   material follows the page theme (`nativeTheme.themeSource`), so the veil
   never fights it. A navigation panel floating over the pane stays opaque.

   The desktop layout and the app shell only: auth pages, public pages and
   the narrow mobile shell keep their opaque full-bleed surface. High-contrast
   palettes, forced colors and reduced transparency keep the opaque window
   too, whatever the device setting says. */
@media not ((forced-colors: active) or (prefers-reduced-transparency: reduce)) {
  html[data-desktop-material][data-app-shell][data-app-layout="desktop"]:is(
    :not(.dark):not([data-theme-light="high-contrast-light"]),
    .dark:not([data-theme-dark="high-contrast-dark"])
  ) {
    --desktop-sidebar-solid: var(--sidebar);
    /* The light material is itself near white, like light sidebars: the same
       veil as in dark mode would hide it. Dark mode needs more veil to keep
       the blur from reading as see-through. */
    --desktop-material-veil: 35%;
    &.dark {
      --desktop-material-veil: 70%;
    }

    & body {
      background: color-mix(in oklab, var(--desktop-sidebar-solid) var(--desktop-material-veil), transparent);
    }
    & .app-workspace {
      --sidebar: transparent;
    }
    /* A solid light-gray highlight vanishes on a light wallpaper behind the
       veil. A light tint of the text color darkens whatever lies behind it by
       the same step, so hovered rows keep their contrast on any desktop.
       Dark mode already reads well against its denser veil. */
    &:not(.dark) .app-workspace {
      --sidebar-accent: color-mix(in oklab, var(--foreground) 6%, transparent);
      /* The selected tab and the selected sidebar row read alike: the opaque
         page white, set apart by the firmer tint of the unselected tabs.
         No edge or lift around them: over the window material it drew a
         dark outline that plain chrome never shows. */
      --app-tab-background: color-mix(in oklab, var(--foreground) 7%, transparent);
    }
    /* The selected sidebar row wears the selected tab's own color, in both
       themes, so the chrome shows one selection. */
    & .app-workspace .sidebar-row-active {
      background-color: var(--app-tab-active-background);
    }
    /* One hover for every control of the transparent chrome (tabs, the new
       tab and window buttons, sidebar rows, Numo, the notebook): a share of
       the selection's color, so hovering reads short of selecting yet stays
       visible on the material. It replaces their own hover fills and the
       tabs' brightness filter. A tab's own inner controls, a composite
       row's (the row hovers), a control nested in another (which would double the
       fill), a selected control and a filled button keep theirs. */
    & .app-workspace {
      --desktop-chrome-hover: color-mix(in oklab, var(--app-tab-active-background) 55%, transparent);
      /* The sidebar's command band, filter, usage and what's-new controls
         hover through an important utility (`hover:!bg-sidebar-accent`),
         which a layered rule wins over any unlayered one: the token itself
         takes the hover color in the chrome. */
      & :is(.app-top-bar, aside[data-sidebar-navigation]) {
        --sidebar-accent: var(--desktop-chrome-hover);
      }
      /* Pointer hovers only: a touch screen keeps no hover state. */
      @media (hover: hover) and (pointer: fine) {
        & .app-tab:not([data-active]):hover {
          background-color: var(--desktop-chrome-hover);
          filter: none;
        }
        & :is(.app-top-bar, aside[data-sidebar-navigation]) :is(button, a, [role="button"]):not(.app-tab *):not([data-sidebar-row] *):not(:is(button, a, [role="button"]) *):not(.sidebar-row-active):not([data-variant="default"]):not(:disabled):hover {
          background-color: var(--desktop-chrome-hover) !important;
        }
        /* A row of several controls (a page with its chevron and actions, a
           project group header, a Numo conversation) hovers as one line, not
           its link alone (SIDEBAR_COMPOSITE_ROW_HOVER_CLASS). */
        & [data-sidebar-row]:not(.sidebar-row-active):hover {
          background-color: var(--desktop-chrome-hover);
        }
      }
      & [data-sidebar-row]:not(.sidebar-row-active):focus-within {
        background-color: var(--desktop-chrome-hover);
      }
    }
    & .sidebar-nav-panel[data-floating="true"] {
      background: var(--desktop-sidebar-solid);
    }
    /* Menus opened from the chrome sit over the transparent window: their
       backdrop blur has no page behind it to frost, so the desktop showed
       through as is. Their translucent share is composited over the solid
       chrome color instead. Glass turned off keeps its plain --popover. */
    &:not([data-transparency="off"]) :is(
      [data-slot="popover-content"],
      [data-slot="dropdown-menu-content"],
      [data-slot="dropdown-menu-sub-content"],
      [data-slot="select-content"],
      [data-floating-surface],
      [role="menu"]
    ) {
      background-color: color-mix(in oklab, var(--popover) 84%, var(--desktop-sidebar-solid));
    }
  }
}

.app-update-slot button { max-width: 100%; }
@media print {
  .app-top-bar { display: none !important; }
}

/* The running sheen on a masked logo: a provider's during an AI review, and
   minddy's own on the post-sign-in redirect screen. */
.review-logo-shimmer {
  background-image: linear-gradient(90deg, currentColor 35%, var(--muted-foreground) 50%, currentColor 65%);
  background-size: 300% 100%;
  animation: text-shimmer 1.8s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  .review-logo-shimmer { animation: none; }
}

/* vaul's bottom drawers (<Dialog> and <SidePanel> below 480px) sit above the
   software keyboard like every other sheet. vaul's own `repositionInputs`
   measured the keyboard as `innerHeight - visualViewport.height`, which iOS
   Safari now reports as 0, so it is off and the drawers read the same
   tokens as the sheets below (MIN-870). */
[data-vaul-drawer][data-vaul-drawer-direction="bottom"] {
  bottom: var(--mobile-keyboard-room, 0px);
  max-height: min(92dvh, var(--mobile-sheet-room, 92dvh));
}

/* Mobile sheets share one breakpoint and viewport budget. Wide data surfaces
   (boards, tables, code and diffs) retain their own intentional scroll areas.

   The budget is three tokens, so every sheet below sizes itself the same way
   whether it is glued to the bottom edge or floats above it:
   - `--mobile-sheet-float`, the gap between the sheet and the bottom edge;
   - `--mobile-sheet-room`, the height left to a sheet under the notch, once
     that gap is taken;
   - `--mobile-sheet-safe-bottom`, the inset a sheet's own padding must clear.
     A glued sheet runs under the home indicator, so its content steps back;
     a floating one already sits above it.

   The software keyboard is two more, the only ones a surface reads to make
   room for it (MIN-870). lib/use-mobile-layout.ts publishes the keyboard,
   components/editor/mobile-editor-toolbar.tsx the strip it adds on top:
   - `--mobile-keyboard-room`, what the keyboard and that strip cover at the
     bottom: the `bottom` of anything anchored there;
   - `--mobile-visible-height`, the height they leave: the cap of anything
     that measures the screen. */
:root[data-app-layout="mobile"] {
  --mobile-keyboard-duration: 380ms;
  --mobile-keyboard-ease: cubic-bezier(0.25, 0.8, 0.25, 1);
  @media (prefers-reduced-motion: reduce) {
    --mobile-keyboard-duration: 0ms;
  }
  /* The editor's strip only counts above an open keyboard: a strip left
     behind by a keyboard Safari closed must not hold the sheets up. */
  --mobile-keyboard-strip: 0px;
  --mobile-keyboard-room: calc(var(--mobile-keyboard-inset, 0px) + var(--mobile-keyboard-strip));
  --mobile-visible-height: calc(var(--mobile-viewport-height, 100dvh) - var(--mobile-keyboard-strip));
  --mobile-sheet-float: 0px;
  --mobile-sheet-safe-bottom: env(safe-area-inset-bottom);
  /* A sheet's own bottom padding: clear of the home indicator, and tight
     above an open keyboard, where the field typed in sits right on it. */
  --mobile-sheet-bottom: max(1rem, var(--mobile-sheet-safe-bottom));
  --mobile-sheet-room: calc(var(--mobile-visible-height) - max(1rem, env(safe-area-inset-top)) - var(--mobile-sheet-float));
}
/* The keyboard covers the home indicator: nothing steps back from it. */
:root[data-app-layout="mobile"][data-mobile-keyboard] {
  --mobile-sheet-safe-bottom: 0px;
  --mobile-sheet-bottom: 0.5rem;
  --mobile-keyboard-strip: var(--mobile-editor-toolbar, 0px);
}
/* No keyboard, no keyboard strip: the editor keeps the focus when Safari
   closes the keyboard some ways, and the strip stayed on screen alone. */
:root[data-app-layout="mobile"]:not([data-mobile-keyboard]) [data-mobile-editor-toolbar] {
  display: none;
}
/* The strip appears with the keyboard: from under the screen's edge, so it
   rises with the keys instead of waiting for them at its place. `!important`
   beats its inline `bottom` for that first frame only. */
@starting-style {
  :root[data-app-layout="mobile"] [data-mobile-editor-toolbar] {
    bottom: -64px !important;
  }
}

:root[data-app-layout="mobile"] {
  .app-workspace, .app-shell main, #app-tab-content {
    min-width: 0;
    max-width: 100%;
    overflow-x: clip;
  }

  .mobile-bottom-sheet[data-slot="sheet-content"],
  .mobile-assistant-sheet[data-slot="sheet-content"],
  [data-slot="alert-dialog-content"] {
    inset: auto 0 calc(var(--mobile-keyboard-room) + var(--mobile-sheet-float));
    width: 100%;
    max-width: none;
    max-height: var(--mobile-sheet-room);
    height: auto;
    margin: 0;
    translate: none;
    transform: none;
    border-radius: 24px 24px 0 0;
    overflow-x: hidden;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding-bottom: var(--mobile-sheet-bottom);
  }

  /* Writing in a sheet: it takes the whole height above the keyboard, so the
     text being typed has room. Its anchor and cap already clear the editor's
     keyboard strip through the keyboard tokens. */
  /* `:focus-within`, not the editor's own focus: tapping the sheet's send
     button moves the focus to it, and the sheet must not shrink under the
     finger before the tap lands. */
  .mobile-bottom-sheet[data-slot="sheet-content"]:has(.ProseMirror):focus-within,
  [data-slot="alert-dialog-content"]:has(.ProseMirror):focus-within {
    height: var(--mobile-sheet-room);
  }

  .mobile-bottom-sheet[data-slot="sheet-content"] {
    padding-top: 8px;
    border: 0;
    background: var(--card);
  }
  .mobile-bottom-sheet [data-calendar] { width: 100%; }
  .mobile-bottom-sheet [data-calendar] :is(.rdp-weekday, .rdp-day) {
    flex: 1 1 0;
    min-width: 0;
    width: auto;
  }
  .mobile-bottom-sheet [data-calendar] :is(.rdp-day, button[data-day]) { height: 44px; }
  .mobile-bottom-sheet [data-calendar] button[data-day] { width: 100%; }
  .mobile-bottom-sheet [data-calendar] :is(.rdp-button_previous, .rdp-button_next) { width: 44px; height: 44px; }
  .mobile-bottom-sheet [data-calendar] .rdp-month_caption { height: 44px; }
  .mobile-bottom-sheet :is([data-slot="dialog-header"], [data-slot="sheet-header"]),
  .mobile-bottom-sheet > [data-slot="sheet-title"]:not(.sr-only) {
    min-height: 44px;
    justify-content: center;
    padding-right: 44px;
  }
  .mobile-bottom-sheet > [data-slot="sheet-title"]:not(.sr-only) {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    flex-shrink: 0;
  }
  .mobile-bottom-sheet [data-mobile-sheet-header] {
    min-height: 44px;
    padding-right: 60px;
    padding-top: 0;
    padding-bottom: 0;
  }
  .mobile-bottom-sheet :is([data-mobile-sheet-close], [data-slot="sheet-close"], [data-slot="side-panel-close"]) {
    position: absolute;
    z-index: 30;
    width: 44px;
    height: 44px;
    min-width: 44px;
    top: 8px;
    right: 8px;
    margin: 0;
    padding: 0;
    border: 0;
    border-radius: 50%;
    color: var(--muted-foreground);
    background: transparent;
  }
  .mobile-bottom-sheet :is([data-mobile-sheet-header], [data-mobile-sheet-toolbar], [data-slot="dialog-header"], [data-slot="sheet-header"], [data-slot="side-panel-header"]) button {
    min-height: 44px;
    min-width: 44px;
  }
  .mobile-bottom-sheet [data-mobile-sheet-header-actions] { margin: 0; gap: 8px; flex-shrink: 0; }
  .mobile-bottom-sheet [data-mobile-sheet-toolbar] { gap: 8px; }
  /* Icon buttons in a pane header or a sheet are aimed with a thumb: they
     grow to 40px, their icon to 20px. `min-*` so a button that is already
     larger (a sheet's 44px close) keeps its size. */
  :is(.app-content-header, .mobile-bottom-sheet) :is([data-size="icon-sm"], [data-size="icon"]) {
    min-width: 40px;
    min-height: 40px;
  }
  :is(.app-content-header, .mobile-bottom-sheet) :is([data-size="icon-sm"], [data-size="icon"]) > svg {
    width: 20px;
    height: 20px;
  }
  /* A split button's chevron is one of those icon buttons: its action grows
     with it, or the chevron stood taller than the button it opens. */
  :is(.app-content-header, .mobile-bottom-sheet) [data-slot="split-button"] > * {
    min-height: 40px;
  }
  .mobile-assistant-sheet .app-content-header > div { padding-right: 60px; }
  .mobile-assistant-sheet .app-content-header button { min-height: 44px; min-width: 44px; }
  [data-property-row] { min-height: 52px; }
  /* A checkbox is a button too, but it keeps its box: its touch area is its
     own wider `::after` (components/ui/checkbox.tsx). */
  [data-property-row] :is(button, a[role="button"]):not([role="checkbox"]) { min-height: 44px; min-width: 44px; justify-content: flex-end; }
  [data-bulk-issue-actions] button { min-height: 44px; min-width: 44px; }
  /* No mobile-only blur on the scrims: every modal layer shares the one
     `--overlay-scrim` tint, on a phone as on a desktop (MIN-931). */
  .mobile-bottom-sheet [data-mobile-sheet-close-wrapper] { display: contents; }
  .mobile-bottom-sheet [data-mobile-sheet-close-legacy] { display: none; }
  .mobile-bottom-sheet [data-mobile-sheet-toolbar] { top: 8px; right: 60px; min-height: 44px; }
  [data-mobile-sidebar-workspace] { background: var(--sidebar); }
  .mobile-sidebar-reveal :is([data-sidebar-navigation-item], [data-sidebar-filter-result], [data-navigation-href], [data-sidebar-back]) { min-height: 44px; }
  .mobile-sidebar-reveal [data-sidebar-filter-result] { min-width: 44px; }
  .mobile-sidebar-reveal :is([data-mobile-collection-navigation], [data-sidebar-browse-collection]) button { min-height: 44px; }
  .mobile-sidebar-reveal [class~="group/project"] button { min-height: 44px; min-width: 44px; opacity: 1; pointer-events: auto; }
  .mobile-sidebar-reveal :is([data-sidebar-footer], [data-sidebar-project-context], .secondary-sidebar-header) button { min-height: 44px; min-width: 44px; }
  .mobile-sidebar-reveal [data-sidebar-page-row] button { min-height: 44px; min-width: 44px; }
  .mobile-sidebar-reveal [data-sidebar-page-row-actions] { opacity: 1; }
  .mobile-sidebar-reveal .sidebar-brand-row button { min-height: 44px; }
  .mobile-sidebar-reveal [data-sidebar-footer] {
    padding-bottom: max(10px, env(safe-area-inset-bottom));
  }
  .mobile-sidebar-reveal [data-mobile-sheet-scroll] > div { touch-action: pan-y; }
  .mobile-sidebar-reveal [data-mobile-sidebar] { touch-action: pan-y; }
  .mobile-sidebar-reveal [data-scroll-fade] { --card: var(--sidebar); }
  .mobile-sidebar-reveal [data-mobile-sheet-scroll] { --card: var(--sidebar); }
  .mobile-sidebar-reveal {
    position: absolute;
    inset: 0 auto auto 0;
    height: var(--mobile-viewport-height, 100dvh);
    /* The sidebar is glued to the left edge: in landscape the notch (or the
       rounded corner of an unfolded screen) sits there and covered the start
       of every row, the New issue button among them (MIN-841). The footer
       clears the bottom inset itself. */
    padding-top: env(safe-area-inset-top);
    padding-left: env(safe-area-inset-left);
    z-index: 0;
    touch-action: pan-y;
    overflow: hidden;
    transform-origin: center;
    transform: scale(calc(0.94 + var(--mobile-sidebar-progress, 0) * 0.06));
    opacity: calc(0.65 + var(--mobile-sidebar-progress, 0) * 0.35);
    transition: transform 260ms cubic-bezier(0.22, 1, 0.36, 1), opacity 260ms cubic-bezier(0.22, 1, 0.36, 1);
    will-change: transform, opacity;
  }
  .mobile-sidebar-reveal[data-mobile-sidebar-dragging="true"] { transition: none; }
  .app-shell[data-mobile-sidebar-surface] {
    position: relative;
    z-index: 1;
    transform: translate3d(var(--mobile-sidebar-offset, 0px), 0, 0);
    background: var(--background);
    overflow: hidden;
    border-radius: var(--mobile-sidebar-radius, 0px) 0 0 var(--mobile-sidebar-radius, 0px);
    transition: transform 260ms cubic-bezier(0.22, 1, 0.36, 1), border-radius 260ms cubic-bezier(0.22, 1, 0.36, 1);
    will-change: transform;
    touch-action: pan-y pinch-zoom;
  }
  &:has([data-mobile-sidebar-surface]) { overscroll-behavior-x: none; }
  .app-shell[data-mobile-sidebar-open="true"] { box-shadow: -8px 0 32px rgb(0 0 0 / 0.08); }
  .app-shell[data-mobile-sidebar-dragging="true"] { transition: none; }
  [data-mobile-sidebar-dismiss] {
    position: absolute;
    inset: 0 0 0 auto;
    z-index: 2;
    width: auto;
    border: 0;
    padding: 0;
    background: transparent;
    touch-action: pan-y;
  }
  @media (prefers-reduced-motion: reduce) {
    .app-shell[data-mobile-sidebar-surface] { transition: none; }
    .mobile-sidebar-reveal { transform: none; opacity: 1; transition: none; }
  }
  .mobile-bottom-sheet :is([data-slot="side-panel-header"], [data-slot="side-panel-footer"]) {
    border: 0;
    padding: 8px 16px;
  }
  .mobile-bottom-sheet [data-slot="side-panel-header"] { padding-right: 60px; }
  .mobile-bottom-sheet [data-slot="side-panel-header"] { min-height: 44px; align-items: center; padding-top: 0; }
  .mobile-bottom-sheet :is([cmdk-separator], [data-slot="separator"], hr) { display: none; }
  .mobile-bottom-sheet :is([cmdk-root], [data-slot="command"]) {
    background: var(--card);
    border-radius: 0;
    padding: 0;
  }
  .mobile-bottom-sheet :is([data-slot="dialog-footer"], [data-slot="side-panel-footer"]),
  [data-slot="alert-dialog-footer"] {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 8px;
    border: 0;
  }
  .mobile-bottom-sheet :is([data-slot="dialog-footer"], [data-slot="side-panel-footer"]) > :last-child:only-child,
  [data-slot="alert-dialog-footer"] > :last-child:only-child {
    grid-column: 1 / -1;
  }
  .mobile-bottom-sheet [data-sheet-auxiliary-action] { grid-column: 1 / -1; }
  .mobile-bottom-sheet :is([data-slot="dialog-footer"], [data-slot="side-panel-footer"]) > button,
  [data-slot="alert-dialog-footer"] > button {
    min-height: 44px;
    height: auto;
    min-width: 0;
    width: 100%;
    white-space: normal;
    margin: 0;
  }
  [data-slot="alert-dialog-footer"]:has(> :nth-child(3)) > :last-child { grid-column: 1 / -1; }
  [data-slot="alert-dialog-footer"] > * { order: 0; min-width: 0; }
  [data-slot="alert-dialog-cancel"] { border: 0; background: transparent; box-shadow: none; }
  [data-slot="alert-dialog-content"] {
    border: 0;
    padding: 16px;
    translate: 0 0 !important;
  }
  [data-slot="alert-dialog-header"] { text-align: left; place-items: start; }
  .mobile-bottom-sheet [data-mobile-sheet-actions] {
    position: sticky;
    bottom: 0;
    flex-direction: row-reverse;
    gap: 8px;
    padding-block: 16px 0;
    background: var(--card);
  }
  .mobile-bottom-sheet [data-mobile-sheet-actions] > button {
    flex: 1;
    min-height: 44px;
    height: auto;
  }
  .mobile-bottom-sheet [data-mobile-sheet-actions]::before {
    content: "";
    position: absolute;
    inset: -16px 0 auto;
    height: 16px;
    pointer-events: none;
    background: linear-gradient(to bottom, transparent, var(--card));
  }
  .mobile-picker-sheet[data-slot="sheet-content"] {
    max-height: min(75dvh, calc(var(--mobile-visible-height) - 1rem - var(--mobile-sheet-float)));
    overflow: hidden;
    padding: 52px 16px var(--mobile-sheet-bottom);
  }
  .mobile-picker-sheet:has([data-sheet-search], [data-slot="command-input-wrapper"])[data-slot="sheet-content"] {
    padding-top: 8px;
  }
  .mobile-picker-sheet > .mobile-picker-title {
    position: absolute;
    inset: 8px 60px auto 16px;
  }
  .mobile-picker-sheet:has([data-sheet-search], [data-slot="command-input-wrapper"]) > .mobile-picker-title {
    position: absolute;
    width: 1px;
    height: 1px;
    min-height: 0;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  .mobile-bottom-sheet :is([data-sheet-note], [data-sheet-section]) { border: 0; }
  .mobile-picker-sheet :is([data-sheet-search], [data-slot="command-input-wrapper"]) {
    display: flex;
    align-items: center;
    min-height: 44px;
    padding: 0 44px 0 0;
    flex-shrink: 0;
    border: 0;
  }
  .mobile-picker-sheet [data-slot="input-group"] {
    flex: 1;
    border: 0;
    background: transparent;
    box-shadow: none;
  }
  .mobile-picker-sheet [cmdk-root] {
    display: flex;
    min-height: 0;
    flex-direction: column;
    overflow: hidden;
  }
  .mobile-picker-sheet [cmdk-list] {
    min-height: 0;
    max-height: min(50dvh, calc(var(--mobile-visible-height) - 7rem - var(--mobile-sheet-float)));
    overflow-y: auto;
    overscroll-behavior: contain;
  }
  .mobile-picker-sheet [cmdk-item] {
    min-height: 44px;
  }
  .mobile-assistant-sheet[data-slot="sheet-content"] {
    height: var(--mobile-sheet-room);
    overflow: hidden;
    transition: none;
    border: 0;
  }
  .mobile-assistant-sheet > div {
    min-height: 0;
  }
  .mobile-assistant-sheet [data-mobile-sheet-close]:not([data-slot="sheet-close"]) { display: none; }
  .mobile-command-palette[data-slot="sheet-content"] {
    height: min(85dvh, calc(var(--mobile-visible-height) - 1rem - var(--mobile-sheet-float)));
    padding: 8px 0 var(--mobile-sheet-bottom);
    gap: 0;
    overflow: hidden;
    --cp-bg: var(--card);
  }
  [data-mobile-navigation] {
    bottom: var(--mobile-keyboard-inset, 0px);
  }
  /* Typing in the workspace itself (a composer, a page): the shell shrinks to
     the visible height, so what sits at its bottom rises above the keyboard
     without the document scrolling (lib/use-mobile-layout.ts pins it). A field
     in a sheet leaves the shell alone: the background stays still behind it
     (MIN-870). */
  &[data-mobile-keyboard="shell"] :is(.app-workspace, .app-workspace .app-shell) {
    height: var(--mobile-visible-height);
  }
  /* What makes room for the keyboard moves with it: the hook announces the
     keyboard as soon as a field takes the focus, and these follow it over
     its animation instead of jumping once Safari reports it. Only while it
     opens or closes: a keyboard changing size while open (a suggestion bar)
     is followed at once, not over 380 ms. */
  &[data-mobile-keyboard-moving] :is(
    .app-workspace,
    :is(.mobile-bottom-sheet, .mobile-assistant-sheet)[data-slot="sheet-content"],
    [data-slot="alert-dialog-content"],
    [data-mobile-editor-toolbar]
  ),
  &[data-mobile-keyboard-moving] body::after {
    transition-property: bottom, height, max-height;
    transition-duration: var(--mobile-keyboard-duration);
    transition-timing-function: var(--mobile-keyboard-ease);
  }
  /* A docked comment composer gives its room to the field being typed in,
     and comes back with the keyboard down. */
  &[data-mobile-keyboard] [data-keyboard-dock]:not(:focus-within) {
    display: none;
  }
  /* The one in use sits right above the keyboard: no home indicator to clear. */
  &[data-mobile-keyboard] [data-keyboard-dock] {
    padding-bottom: 0.5rem;
  }
  /* The navigation pill hides while typing (components/mobile-navigation.tsx):
     its reserve under the content goes back to the field. */
  &[data-mobile-keyboard="shell"] {
    --mobile-nav-clearance: 0px;
  }
  /* Between a sheet and the keyboard, iOS leaves its form bar floating on
     a transparent band, and the editor's strip floats with margins: show the
     sheet there, not the page behind it. Above every sheet and its overlay,
     under the strip (z-80). For as long as a sheet is open, not only while
     one of its fields types: the keyboard closing or reopening under it
     uncovered the page too. */
  &[data-mobile-surface] body::after {
    content: "";
    position: fixed;
    inset: auto 0 0;
    z-index: 79;
    height: var(--mobile-keyboard-room);
    background: var(--card);
    pointer-events: none;
  }
  /* Writing brings the keyboard up: the bottom navigation steps aside for the
     text (components/mobile-navigation.tsx tracks the focused field), and for
     the editor's keyboard toolbar (components/editor/mobile-editor-toolbar.tsx). */
  &[data-mobile-toolbar-open] [data-mobile-navigation],
  [data-mobile-navigation][data-typing] {
    display: none;
  }
  .app-content-header > div {
    width: 100%;
    min-width: 0;
  }
}

:root[data-app-layout="mobile"] {
  .mobile-side-panel-sheet[data-slot="sheet-content"] {
    height: var(--mobile-sheet-room);
    overflow: hidden;
    padding-bottom: var(--mobile-sheet-safe-bottom);
  }
  .mobile-side-panel-sheet [data-slot="side-panel-body"] {
    min-height: 0;
    overscroll-behavior: contain;
  }
}

:root[data-app-layout="mobile"] {
  .mobile-inbox-sheet[data-slot="sheet-content"] {
    height: min(85dvh, calc(var(--mobile-visible-height) - 1rem - var(--mobile-sheet-float)));
    max-height: calc(var(--mobile-visible-height) - 1rem - var(--mobile-sheet-float));
    padding-top: 3rem;
  }
  .mobile-inbox-sheet .divide-y > :not(:last-child) { border-bottom-width: 0; }
}

.creation-field-label { display: none; }
.creation-title { display: block; }

:root[data-app-layout="mobile"] {
  .creation-sheet[data-slot="sheet-content"] {
    height: min(92dvh, calc(var(--mobile-visible-height) - 1rem - var(--mobile-sheet-float)));
    padding: 8px 1rem var(--mobile-sheet-bottom);
    gap: 1rem;
    overflow: hidden;
  }
  .creation-form {
    flex: 1;
    min-height: 0;
    overflow: hidden;
  }
  .creation-form-body {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    overflow-x: hidden;
    overscroll-behavior: contain;
    padding: 0 2px 1rem;
  }
  .creation-field-label {
    display: block;
    margin: 1rem 0 0.5rem;
    font-size: 0.75rem;
    color: var(--muted-foreground);
  }
  .creation-title, .creation-description {
    margin: 0;
    border: 1px solid var(--border);
    border-radius: 12px;
    padding: 0.75rem;
    font-size: 1rem;
    line-height: 1.5;
  }
  .creation-description { min-height: 6rem; }
  .creation-description .ProseMirror { font-size: 1rem; }
  .creation-title:focus, .creation-description:focus-within { border-color: var(--ring); }
  .creation-properties {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.5rem;
  }
  .creation-property {
    display: flex;
    flex-direction: column;
    min-width: 0;
    border: 1px solid var(--border);
    border-radius: 12px;
    padding: 0.5rem 0.5rem 0;
  }
  .creation-property > button {
    width: 100%;
    min-width: 0;
    min-height: 44px;
    height: auto;
    padding-inline: 0;
    justify-content: flex-start;
    overflow: hidden;
  }
  .creation-property > button span:not(.sr-only) {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .creation-form-footer {
    flex-shrink: 0;
    margin-top: 0;
    padding-top: 0.5rem;
    border-top: 0;
    gap: 0.5rem;
  }
  .creation-form-footer > button, .creation-submit button {
    min-height: 44px;
    min-width: 44px;
  }
  .creation-submit { width: 100%; }
  .creation-submit > :not(.creation-dictation), .creation-submit button[type="submit"] { flex: 1; }
  .creation-submit > .creation-dictation { flex: none; }
}

:root[data-app-layout="mobile"] {
  .creation-property > button,
  .creation-property > button:hover,
  .creation-property > button:focus-visible,
  .creation-property > button[aria-expanded=true] {
    background-color: transparent;
  }
  .creation-property:focus-within { border-color: var(--ring); }
  .creation-property > button:focus-visible {
    outline: 2px solid var(--ring);
    outline-offset: -2px;
  }
  .creation-smart-fill {
    grid-column: 1 / -1;
    display: flex;
    min-height: 56px;
    align-items: center;
    gap: 1rem;
    border: 1px solid var(--border);
    border-radius: 12px;
    padding: 0.75rem;
  }
}

/* A bottom sheet on a wide screen (MIN-841). The mobile layout also serves an
   unfolded phone, a tablet in portrait and a phone in landscape
   (lib/app-layout.ts), where a sheet spanning the whole width reads as a slab.
   Like the sheets of iPadOS, it stays a bottom sheet — anchored at the
   bottom, sliding up, never a centered dialog — but floats: a width that
   follows the screen up to a reading measure, side gutters that also clear
   the landscape notch, a gap above the home indicator and four rounded
   corners. Below 640 px, a phone in portrait keeps its full-width sheet.

   One rule for every bottom sheet: they all go through `MobileSheetContent`
   (components/ui/mobile-sheet.tsx) or carry one of the two other selectors.
   The vaul drawers of components/ui/dialog.tsx and side-panel.tsx only exist
   under 480 px, so they never reach this width. */
@media (min-width: 640px) {
  :root[data-app-layout="mobile"] {
    --mobile-sheet-float: max(0.75rem, env(safe-area-inset-bottom));
    --mobile-sheet-safe-bottom: 0px;

    :is(.mobile-bottom-sheet, .mobile-assistant-sheet)[data-slot="sheet-content"],
    [data-slot="alert-dialog-content"] {
      width: min(100% - 2 * max(1rem, env(safe-area-inset-left), env(safe-area-inset-right)), 40rem);
      margin-inline: auto;
      border-radius: 24px;
    }
  }
}

/* Single-column boards use persistent pagination instead of a scrollbar. */
@media (width < 640px) {
  .mobile-kanban-scroller { scrollbar-width: none; }
  .mobile-kanban-scroller::-webkit-scrollbar { display: none; }
}

/* Keep the article readable beside the documentation help conversation. */
.documentation-numo-panel {
  transform: translate3d(100%, 0, 0);
  visibility: hidden;
  will-change: transform;
  transition: transform 300ms cubic-bezier(0.4, 0, 0.2, 1), visibility 0s linear 300ms;
}

.documentation-numo-panel[data-state="open"] {
  pointer-events: auto;
  transform: translate3d(0, 0, 0);
  visibility: visible;
  transition-delay: 0s;
}

@starting-style {
  .documentation-numo-panel[data-state="open"] {
    transform: translate3d(100%, 0, 0);
  }
}

:root[data-app-layout="mobile"] .documentation-numo-sheet[data-slot="sheet-content"] {
  height: min(85dvh, calc(var(--mobile-visible-height) - 1rem - var(--mobile-sheet-float)));
  overflow: hidden;
  padding: 0;
}

@media (min-width: 1024px) {
  html[data-app-layout="desktop"] [data-documentation-contents] {
    transition: opacity 150ms ease, visibility 0s;
  }
  html[data-app-layout="desktop"] [data-documentation-numo-open="true"] [data-documentation-main] { padding-right: 24rem; }
  html[data-app-layout="desktop"] [data-documentation-numo-open="true"] [data-documentation-contents] {
    pointer-events: none;
    opacity: 0;
    visibility: hidden;
    transition-delay: 0s, 150ms;
  }
  html[data-app-layout="desktop"] [data-documentation-numo-open="true"] [data-documentation-main] > div { padding-left: 1.5rem; padding-right: 1.5rem; }
  html[data-app-layout="desktop"] [data-documentation-numo-open="true"] [data-documentation-mobile-contents] { right: 24rem; }
}

@media (prefers-reduced-motion: reduce) {
  .documentation-numo-panel,
  html[data-app-layout="desktop"] [data-documentation-contents] {
    transition: none;
  }
  .documentation-numo-panel { will-change: auto; }
  .documentation-numo-sheet[data-slot="sheet-content"] { animation: none; transition: none; }
  @starting-style {
    .documentation-numo-panel[data-state="open"] { transform: translate3d(0, 0, 0); }
  }
}

@keyframes mobile-navigation-progress {
  from { transform: translateX(-100%); }
  to { transform: translateX(300%); }
}
.mobile-navigation-progress > div { animation: mobile-navigation-progress 1.2s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
  .mobile-navigation-progress > div { animation: none; width: 100%; }
}

/* ─── Placeholder tabs while the first tab list loads ─────────────────────────────
   A soft band sweeps left to right across muted tab shapes in the top bar,
   in the reading direction like `text-shimmer`. Reduced motion keeps the
   shapes still. */
.app-tab-skeleton {
  background: linear-gradient(90deg,
    var(--sidebar-accent) 0%,
    color-mix(in oklab, var(--sidebar-accent), var(--sidebar-foreground) 8%) 50%,
    var(--sidebar-accent) 100%);
  background-size: 300% 100%;
  animation: app-tab-skeleton-sweep 1.4s ease-in-out infinite;
}
@keyframes app-tab-skeleton-sweep {
  from { background-position: 100% 0; }
  to { background-position: 0% 0; }
}
@media (prefers-reduced-motion: reduce) {
  .app-tab-skeleton { animation: none; }
}

/* ─── Collaborators' carets and selections (MIN-688) ──────────────────────────────
   Drawn by components/pages/page-collaboration.ts (body decorations) and
   components/pages/page-title-carets.tsx (title overlay). `--collab-color` is the
   collaborator's stable hue; the label keeps white text on it, which the palette
   in page-collaboration.ts is chosen to keep legible in both themes. The caret
   takes no width in the line, so a remote caret never reflows the text. */
.page-collab-caret {
  position: relative;
  display: inline-block;
  width: 0;
  height: 1.2em;
  margin: 0 -1px;
  vertical-align: text-bottom;
  border-left: 2px solid var(--collab-color);
  pointer-events: none;
  user-select: none;
}
.page-collab-caret.absolute {
  position: absolute;
  margin: 0;
}
.page-collab-caret-label {
  position: absolute;
  bottom: 100%;
  left: -2px;
  z-index: 10;
  padding: 1px 5px;
  border-radius: 4px 4px 4px 0;
  background: var(--collab-color);
  color: #fff;
  font-family: var(--font-sans, ui-sans-serif, system-ui, sans-serif);
  font-size: 11px;
  font-weight: 500;
  font-style: normal;
  letter-spacing: normal;
  line-height: 1.4;
  white-space: nowrap;
  max-width: 12rem;
  overflow: hidden;
  text-overflow: ellipsis;
}
.page-collab-caret-label:empty {
  display: none;
}
.page-collab-selection {
  background-color: color-mix(in oklab, var(--collab-color) 22%, transparent);
}
.dark .page-collab-selection {
  background-color: color-mix(in oklab, var(--collab-color) 35%, transparent);
}

/* ─── Selection boxes (block selection, node selection, atoms) ────────────
   Everything a selection holds as a whole wears one look on every editor
   surface (components/editor/block-selection.ts): the text selection's own
   color (`--selection-block`, app/tokens.css) with slightly rounded corners,
   so selected blocks read as blocks rather than as highlighted text.

   - A block of a block selection (`.is-block-selected`), a block in a node
     selection, and a code block or media a text selection covers whole get
     an overlay painted OVER the block: a code block's or a callout's own
     background, or an image, cannot hide it. The text highlight under it
     goes, so the two never stack.
   - An inline atom (an equation, a mention) or a whole inline-code run in a
     text selection (`.is-inline-selected`), and a selected inline equation,
     get an inline rounded background.

   The box reaches OUTSIDE the block by `--block-selection-x` and `-y`, so
   the text has room inside it without moving: an overlay adds no
   indentation. The vertical reach stays under half the 8px between two
   paragraphs, so two selected blocks still read as two. Every block gets
   the same small corners, a block drawn as a card (a code block, a callout)
   included: the box frames it rather than echoing its own corners. */
:root {
  --block-selection-x: 0.5rem;
  --block-selection-y: 0.1875rem;
  --block-selection-radius: 0.3125rem;
}
.ProseMirror .is-block-selected,
.ProseMirror .ProseMirror-selectednode:not(span) {
  position: relative;
  outline: none;
}
.ProseMirror .is-block-selected::after,
.ProseMirror .ProseMirror-selectednode:not(span)::after {
  content: "";
  position: absolute;
  inset: calc(-1 * var(--block-selection-y)) calc(-1 * var(--block-selection-x));
  border-radius: var(--block-selection-radius);
  background-color: var(--selection-block);
  pointer-events: none;
}
/* A list item's marker hangs in the list's indent: the box takes it in. */
.ProseMirror :is(ul:not([data-type="taskList"]), ol) > li.is-block-selected::after {
  left: calc(-1.25rem - var(--block-selection-x));
}
/* A divider is one line tall, and the browser clips its children: the box
   reaches above and below the line. */
.ProseMirror hr:is(.is-block-selected, .ProseMirror-selectednode) {
  overflow: visible;
}
.ProseMirror hr:is(.is-block-selected, .ProseMirror-selectednode)::after {
  inset: -0.375rem calc(-1 * var(--block-selection-x));
}
.ProseMirror .is-block-selected ::selection,
.ProseMirror .is-block-selected::selection,
.ProseMirror .is-inline-selected ::selection,
.ProseMirror .is-inline-selected::selection,
.ProseMirror.has-block-selection ::selection {
  background-color: transparent;
}
.ProseMirror.has-block-selection {
  caret-color: transparent;
}
.ProseMirror .is-inline-selected,
.ProseMirror span.ProseMirror-selectednode {
  border-radius: 0.25rem;
  background-color: var(--selection-block);
  outline: none;
}
/* An inline code's box is its element, padding included; the decoration
   only wraps its text. */
.ProseMirror code:has(.is-inline-selected) {
  background-image: linear-gradient(var(--selection-block), var(--selection-block));
}
.ProseMirror code .is-inline-selected,
.ProseMirror .is-inline-selected:has([style*="--mention-chip"]) {
  background-color: transparent;
}
/* Inside a selected block, only the block's box is painted. */
.ProseMirror .is-block-selected :is(.is-block-selected, .ProseMirror-selectednode)::after {
  content: none;
}
.ProseMirror .is-block-selected :is(.is-inline-selected, span.ProseMirror-selectednode),
.ProseMirror .is-block-selected code:has(.is-inline-selected) {
  background-color: transparent;
  background-image: none;
}
.ProseMirror .is-block-selected .mention-selected [style*="--mention-chip"] {
  background-image: none;
}

/* ─── Mentions inside a selection (MIN-734) ────────────────────────────────
   A mention pill is an atom: the browser does not paint it as selected text,
   so a selection seemed to skip it. The editor marks every pill the
   selection covers (`mention-selected`, components/markdown-mention.tsx), and
   the chip inside (the element carrying its tone) is overlaid with the
   selection color, over its own background. */
.mention-selected [style*="--mention-chip"] {
  background-image: linear-gradient(var(--selection-block), var(--selection-block));
}

/* A drag keeps its grabbing cursor over everything it crosses
   (components/drag-cursor.tsx). */
html[data-dragging],
html[data-dragging] * {
  cursor: grabbing !important;
}