@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
; 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 +
, 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 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 `
`) 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 `