/*!
* future-css -- https://github.com/estith/future-css
* A styling system for multi-language, multi-platform systems
*
* Regenerate with: npm run build
* This file is generated; edit the sources in src/ instead.
*/
/*!
* future-css -- layer order
*
* This file exists to be imported FIRST and on its own, so the cascade layer
* order is established before any layer's contents arrive. Consumers who load
* pieces separately must import this first:
*
* import "future-css/layers";
* import "future-css/tokens";
* import "future-css/base";
*
* Layer order determines precedence regardless of source order, specificity, or
* `!important` inside a lower layer. The sequence below is the whole cascade
* strategy of the system:
*
* reset zeroes out what browsers get wrong. Zero specificity.
* tokens the variables. Everything downstream reads from these.
* base element defaults. Zero specificity, so never fights a class.
* layout structural primitives.
* components individual components.
* utilities the escape hatch; beats components on equal specificity.
* overrides a reserved final layer for consumer-level escape hatches.
*
* The two properties that matter most:
*
* 1. `base` at zero specificity means a component class always wins over an
* element rule, with no `!important` and no specificity war.
* 2. `utilities` last means an author can always override a component with a
* single class, which is what makes the system debuggable in production.
*/
@layer fc.reset, fc.tokens, fc.base, fc.layout, fc.components, fc.utilities, fc.overrides;
/*!
* future-css -- reset layer
*
* Opinionated but narrow. It removes browser defaults that are actively harmful
* in a multi-language, multi-platform system, and leaves alone everything that
* users may have configured deliberately.
*
* Everything here is wrapped in :where() so it contributes ZERO specificity.
* That means a single class selector in any later layer always wins, no matter
* how the source is ordered. This is what makes the reset safe to ship
* unconditionally.
*/
@layer fc.reset {
*,
*::before,
*::after {
box-sizing: border-box;
}
/*
* Margin removal. Default margins are physical and asymmetric in ways that do
* not survive translation, and they are the single largest source of
* "why is there a gap here" bugs. Vertical rhythm comes from --fc-space-*
* tokens applied deliberately, or from the flow classes in the layout layer.
*/
:where(html, body, h1, h2, h3, h4, h5, h6, p, figure, blockquote, dl, dd, ul, ol, pre) {
margin-block: 0;
}
/*
* Unset lists. `list-style: none` strips the marker, which is a real
* accessibility regression for anyone who relies on a list to convey
* structure, so we only remove it when the author has opted in via a class.
*/
:where(ul, ol)[role="list"] {
list-style: none;
padding-inline: 0;
}
/*
* Media defaults. `display: block` fixes the legacy inline-image baseline gap,
* and max-inline-size prevents a full-bleed image from forcing horizontal
* scroll on narrow viewports.
*/
:where(img, picture, video, canvas, svg) {
display: block;
max-inline-size: 100%;
height: auto;
}
/*
* Form elements. Controls do not inherit typography by default, which makes
* a text input visibly smaller than surrounding text -- a frequent and
* avoidable accessibility complaint.
*/
:where(input, button, textarea, select) {
font: inherit;
color: inherit;
letter-spacing: inherit;
margin: 0;
}
/*
* Buttons do not inherit text alignment or cursor, and Safari renders a
* button's letter-spacing differently from body copy.
*/
:where(button) {
text-align: inherit;
cursor: pointer;
}
/*
* Explicit opt-out of the pointer cursor for controls where a pointer cursor
* misrepresents the interaction (e.g. a disabled control, or a button that is
* a link in disguise).
*/
:where(button)[disabled],
:where([aria-disabled="true"]) {
cursor: not-allowed;
}
/*
* Remove the default fieldset/popover affordances while keeping the grouping
* semantics that assistive technology depends on.
*/
:where(fieldset) {
border: 0;
padding: 0;
min-inline-size: 0;
}
:where([hidden]) {
display: none !important;
}
/*
* Autofill styling. Chrome/Safari inject their own background and a yellow
* text, which breaks dark mode and the surface tokens. `-internal-autofill-*`
* are vendor pseudo-elements and must be prefixed individually -- there is no
* groupable `::-webkit-autofill` alias that works in all engines.
*/
:where(input):autofill {
/* Delay the transition so the injected style paints and is then overridden. */
transition: background-color 9999s ease-out 0s;
}
:where(input):-webkit-autofill,
:where(input):-webkit-autofill:hover,
:where(input):-webkit-autofill:focus {
-webkit-text-fill-color: var(--fc-color-text-default);
box-shadow: 0 0 0 1000px var(--fc-color-bg-surface) inset;
}
/*
* Tabular data. Numbers in tables must align on the decimal, in both
* LTR and RTL (where the digit run itself mirrors).
*/
:where([data-numeric]) {
font-variant-numeric: tabular-nums;
font-feature-settings: "tnum";
}
/*
* Respect the user's language-specific line breaking. CJK text has no spaces,
* and long URLs and German compounds need to be allowed to break rather than
* overflowing their container.
*/
:where(p, li, dd, dt) {
overflow-wrap: break-word;
}
/*
* Reduced motion. Components are expected to consult this block; it is
* declared here so the media query is guaranteed to exist in the bundle even
* before any animated component ships.
*/
/*
* Spinners are excluded by name, and that is the only way to do it: an
* !important declaration in an earlier layer beats every declaration in a
* later one, important or not, so no component rule can loosen this block
* after the fact. A spinner is the sole cue that work is in progress; frozen,
* it reads as a hung page. Each spinner slows itself down in its own
* reduced-motion branch instead.
*/
@media (prefers-reduced-motion: reduce) {
:where(*:not(.fc-spinner, .fc-spinner *, .fc-btn__spinner), *::before, *::after) {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
}
/*!
* future-css -- base layer
*
* Element-level defaults, all zero-specificity via :where(). This layer is what
* makes semantic HTML look finished before a single class is written: an <h1> is
* a heading, a <table> has structure, a <fieldset> groups its controls.
*
* The guiding principle: a document using only semantic HTML with no classes at
* all should already be legible and usable. Classes enhance; they do not rescue.
*/
@layer fc.base {
html {
/*
* Never set a fixed px font-size here: it would override the browser's
* minimum/default font size setting that many users raise deliberately.
* Everything else is sized in rem, so it follows this value.
*/
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
/* Smooth scrolling is opt-in via a utility class, not the default:
* honouring prefers-reduced-motion matters more than the polish. */
scroll-behavior: auto;
/*
* `color-scheme` is deliberately NOT declared here.
*
* It used to be, with the comment "declares that both schemes are supported".
* That made the theme switcher dead: this rule sits in `fc.base`, and the
* tokens layer -- which owns `[data-fc-theme]` -- is ordered *before* base.
* Equal specificity, so layer order decided it, and `light dark` always won.
* Setting `data-fc-theme="light"` on <html> computed to `light dark` and
* changed nothing.
*
* Nothing broke visibly, because the OS preference still worked and
* `light-dark()` still resolved -- the page just refused to be overridden.
* The declaration belongs in the tokens layer alone, where a consumer's
* attribute can actually take effect.
*/
}
body {
/* Canvas tokens set the page surface, which is what makes dark mode reach
* the areas no component touches (overscroll, empty space). */
background-color: var(--fc-color-bg-canvas);
color: var(--fc-color-text-default);
font: var(--fc-type-body);
letter-spacing: var(--fc-type-body-tracking);
font-kerning: normal;
text-rendering: optimizeLegibility;
/* WebKit-only: dark text on a dark canvas looks smeared without it. */
-webkit-font-smoothing: antialiased;
/*
* Line length. A measure of roughly 65-75 characters is comfortable for
* sustained reading; without a max-width, prose is unreadable on a wide
* monitor. Applied here rather than to <p> so it does not constrain
* full-bleed layouts.
*/
max-inline-size: var(--fc-size-content-max);
margin-inline: auto;
padding-inline: var(--fc-space-4);
}
/*
* Headings. Size, weight and space all come from the same scale. Note the
* logical margin longhands: margin-block-end behaves identically in LTR and
* RTL and, unlike margin-top, does not flip when page direction changes.
*/
:where(h1, h2, h3, h4, h5, h6) {
font-weight: var(--fc-font-weight-semibold);
line-height: var(--fc-line-height-tight);
/* Evens out ragged headline wrapping. Ignored where unsupported. */
text-wrap: balance;
}
:where(h1) {
font: var(--fc-type-heading-1);
letter-spacing: var(--fc-type-heading-1-tracking);
margin-block-end: var(--fc-space-4);
}
:where(h2) {
font: var(--fc-type-heading-2);
letter-spacing: var(--fc-type-heading-2-tracking);
margin-block-end: var(--fc-space-3);
}
:where(h3) {
font: var(--fc-type-heading-3);
letter-spacing: var(--fc-type-heading-3-tracking);
margin-block-end: var(--fc-space-3);
}
:where(h4) {
font: var(--fc-type-heading-4);
letter-spacing: var(--fc-type-heading-4-tracking);
margin-block-end: var(--fc-space-2);
}
:where(h5) {
font: var(--fc-type-heading-5);
letter-spacing: var(--fc-type-heading-5-tracking);
margin-block-end: var(--fc-space-2);
}
:where(h6) {
/*
* h6 is not made smaller than body text; it is distinguished by weight and
* colour instead. Shrinking it produces illegible headings, and relying on
* letter-spacing alone would misbehave in scripts that have no such
* tradition (Arabic, CJK).
*/
font: var(--fc-type-heading-6);
letter-spacing: var(--fc-type-heading-6-tracking);
color: var(--fc-color-text-muted);
}
/*
* Body text gets `pretty`, which lets the browser adjust line breaks to
* avoid orphans. Unlike `balance`, this is specifically for running prose.
*/
:where(p, li) {
text-wrap: pretty;
}
/*
* Prose rhythm: block elements inside a flow get spacing without wrapper
* divs. The general-sibling combinator is direction-agnostic.
*/
:where(p, ul, ol, dl, blockquote, pre, table, figure) {
margin-block-end: var(--fc-space-4);
}
/*
* Links. Underlined by default and always. Removing underlines breaks WCAG
* 1.4.1 for anyone who cannot perceive the colour difference between link and
* body text: colour alone is not a sufficient affordance.
*/
:where(a) {
color: var(--fc-color-accent-text);
text-decoration: underline;
text-underline-offset: 0.2em;
text-decoration-thickness: from-font;
}
:where(a:hover) {
text-decoration-thickness: var(--fc-border-width-thick);
}
/* Visited links are deliberately NOT recoloured. A visited-state hue that
* passes contrast on one background often fails on another, and the privacy
* value of a distinct visited colour is minor by comparison. */
/*
* Focus. One ring definition for every focusable element in the document,
* driven by :focus-visible so a mouse user never sees it but a keyboard user
* always does. `outline-offset` gives the ring breathing room.
*/
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
outline: var(--fc-border-width-focus) solid var(--fc-color-focus-ring);
outline-offset: var(--fc-space-0-5);
/*
* The offset colour creates a gap between the component and the ring, so
* the ring stays distinguishable even when the component's own colour is
* close to the focus colour.
*/
box-shadow: 0 0 0 var(--fc-space-0-5) var(--fc-color-focus-ring-offset);
}
/*
* Remove the ring only for non-keyboard focus. The lint step fails the build
* on `outline: none` anywhere that is not inside a `:not(:focus-visible)`
* selector, so this pattern cannot be used to silently delete focus styling.
*/
:where(a, button, input, select, textarea, summary, [tabindex]):focus:not(:focus-visible) {
outline: none;
}
/* Inline code. */
:where(code, kbd, samp, pre) {
font-family: var(--fc-font-family-mono);
/* Code is optically smaller at the same point size. */
font-size: 0.9375em;
}
:where(code):not(pre code) {
background-color: var(--fc-color-bg-subtle);
padding-inline: 0.25em;
border-radius: var(--fc-radius-sm);
}
/*
* Block quotes. `border-inline-start` is the logical form: it draws on the
* left in LTR and the right in RTL with no extra rule.
*/
:where(blockquote) {
border-inline-start: var(--fc-border-width-thick) solid var(--fc-color-border-default);
padding-inline-start: var(--fc-space-4);
color: var(--fc-color-text-muted);
font-style: italic;
}
:where(hr) {
border: 0;
border-block-start: var(--fc-border-width-thin) solid var(--fc-color-border-default);
margin-block: var(--fc-space-6);
}
/*
* Tables. `border-collapse` is set on the table itself, since it does not
* inherit reliably in every engine. `text-align: start` keeps numeric columns
* aligned to the reading edge, which is what makes one table work in both LTR
* and RTL without a separate stylesheet.
*/
:where(table) {
border-collapse: collapse;
inline-size: 100%;
max-inline-size: 100%;
}
:where(caption) {
color: var(--fc-color-text-muted);
text-align: start;
padding-block-end: var(--fc-space-2);
}
:where(th, td) {
padding: var(--fc-space-2) var(--fc-space-3);
text-align: start;
border-block-end: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
vertical-align: top;
}
:where(thead th) {
font-weight: var(--fc-font-weight-semibold);
color: var(--fc-color-text-muted);
border-block-end-color: var(--fc-color-border-default);
}
:where(tbody tr:last-child td) {
border-block-end: 0;
}
/*
* Form elements. Placeholders are given a real contrast ratio rather than the
* browser default, which is frequently far too faint to read.
*/
:where(input, textarea, select) {
background-color: var(--fc-color-bg-surface);
border: var(--fc-border-width-thin) solid var(--fc-color-border-interactive);
border-radius: var(--fc-radius-md);
padding: var(--fc-space-2) var(--fc-space-3);
inline-size: 100%;
}
:where(textarea) {
/* Never lock the height: a fixed-height textarea is a common cause of
* content being silently unreachable. */
min-block-size: 6lh;
resize: vertical;
}
:where(::placeholder) {
color: var(--fc-color-text-subtle);
opacity: 1; /* Firefox lowers this by default. */
}
/*
* Invalid state, driven by :user-invalid rather than :invalid so the field is
* not flagged before the user has typed anything. Colour is never the only
* signal: the component layer also renders a message, and the field carries
* aria-invalid plus aria-describedby pointing at it.
*/
:where(input, textarea, select):user-invalid {
border-color: var(--fc-color-danger-border);
}
:where(summary) {
cursor: pointer;
font-weight: var(--fc-font-weight-medium);
}
:where(abbr[title]) {
text-decoration: underline dotted;
cursor: help;
}
/*
* Selection. `Highlight`/`HighlightText` would follow the OS but are not
* universally supported; tokens keep the result consistent with the theme.
*/
::selection {
background-color: var(--fc-color-accent-bg-subtle);
color: var(--fc-color-accent-text);
}
}
/*!
* future-css -- layout layer
*
* A deliberately tiny set of layout primitives, chosen so the common structural
* problems are solved once, correctly, in every writing direction.
*
* Constraints that shaped this layer:
* - Direction-agnostic. Every primitive uses logical properties or
* direction-agnostic mechanisms (grid auto-placement, flex flow). Nothing
* here needs an [dir="rtl"] override, because there is nothing physical
* left to override.
* - Intrinsic sizing. Primitives size to their content and grow with
* translated strings, so a German compound noun never silently overflows.
* - No specificity wars. Single class selectors, so any later layer wins.
*
* These are primitives, not a grid framework. They cover what appears in nearly
* every interface; genuinely bespoke layout is left to the author.
*/
@layer fc.layout {
/*
* .fc-stack -- the most-used primitive. Uniform spacing between children on
* the block axis by default. Gap rather than margins, so there is no margin
* collapse to reason about and no stray space at the container's edges.
*/
.fc-stack {
display: flex;
flex-direction: column;
gap: var(--fc-stack-gap, var(--fc-space-4));
}
.fc-stack--inline {
/*
* Auto-flow wraps, so a translated label that is twice as long wraps to a
* second row rather than overflowing its container.
*/
flex-direction: row;
flex-wrap: wrap;
align-items: var(--fc-stack-align, center);
}
/* Spacing steps reference tokens rather than arbitrary numbers. */
.fc-stack--0 { --fc-stack-gap: var(--fc-space-0); }
.fc-stack--1 { --fc-stack-gap: var(--fc-space-1); }
.fc-stack--2 { --fc-stack-gap: var(--fc-space-2); }
.fc-stack--3 { --fc-stack-gap: var(--fc-space-3); }
.fc-stack--4 { --fc-stack-gap: var(--fc-space-4); }
.fc-stack--6 { --fc-stack-gap: var(--fc-space-6); }
.fc-stack--8 { --fc-stack-gap: var(--fc-space-8); }
/*
* .fc-cluster -- a group of items that stay together when there is room and
* wrap when there is not. Flexbox row + wrap is inherently direction-aware:
* the first item sits on the right in RTL with no extra rule.
*/
.fc-cluster {
display: flex;
flex-wrap: wrap;
gap: var(--fc-cluster-gap, var(--fc-space-3));
align-items: var(--fc-cluster-align, center);
justify-content: var(--fc-cluster-justify, flex-start);
}
.fc-cluster--0 { --fc-cluster-gap: var(--fc-space-0); }
.fc-cluster--1 { --fc-cluster-gap: var(--fc-space-1); }
.fc-cluster--2 { --fc-cluster-gap: var(--fc-space-2); }
.fc-cluster--3 { --fc-cluster-gap: var(--fc-space-3); }
.fc-cluster--4 { --fc-cluster-gap: var(--fc-space-4); }
.fc-cluster--6 { --fc-cluster-gap: var(--fc-space-6); }
.fc-cluster--center { --fc-cluster-justify: center; }
.fc-cluster--between { --fc-cluster-justify: space-between; }
.fc-cluster--end { --fc-cluster-justify: flex-end; }
.fc-cluster--top { --fc-cluster-align: flex-start; }
.fc-cluster--stretch { --fc-cluster-align: stretch; }
/*
* .fc-sidebar -- the classic two-column layout that collapses to one column
* when space runs out.
*
* The basis is expressed in rem rather than px so it scales with the user's
* font size: someone reading at 24px gets the collapse earlier, which is
* what they want.
*/
.fc-sidebar {
display: flex;
flex-wrap: wrap;
gap: var(--fc-space-4);
}
.fc-sidebar > * {
/* flex-basis rather than flex-grow alone, so the basis is content-driven
* until there is genuinely spare room. */
flex-grow: 1;
flex-basis: var(--fc-sidebar-basis, 20rem);
}
/* The main child claims the remainder, keeping the sidebar narrow. */
.fc-sidebar > :is(.fc-sidebar__main, [data-fc-main]) {
flex-basis: 0;
flex-grow: 999;
}
/*
* min-inline-size: 0 is the fix for the classic flexbox overflow bug. A flex
* item's default min-width:auto refuses to shrink below its content, so one
* unbreakable string (a URL, a German compound) forces the entire page to
* scroll sideways.
*/
.fc-sidebar > *,
.fc-cluster > *,
.fc-stack > * {
min-inline-size: 0;
}
/*
* .fc-grid-auto -- an auto-fitting responsive grid. No media queries and no
* breakpoint classes: the column count follows the available inline size.
* auto-fit + minmax is what makes this identical in LTR and RTL and at any
* container width.
*
* Named `-auto` rather than `.fc-grid` because the data grid component
* already owns that name. Two blocks sharing a class name in different layers
* is not a merge: the later layer wins, so the data grid silently inherited
* `grid-template-columns` from here and rendered 4 columns of data as 2.
* The collision was invisible because both rules are individually correct.
* tools/lint-css.mjs now rejects a class declared in two files.
*/
.fc-grid-auto {
display: grid;
gap: var(--fc-grid-auto-gap, var(--fc-space-4));
grid-template-columns: repeat(
auto-fit,
minmax(min(var(--fc-grid-auto-min, 16rem), 100%), 1fr)
);
}
.fc-grid-auto--0 { --fc-grid-auto-gap: var(--fc-space-0); }
.fc-grid-auto--2 { --fc-grid-auto-gap: var(--fc-space-2); }
.fc-grid-auto--4 { --fc-grid-auto-gap: var(--fc-space-4); }
.fc-grid-auto--6 { --fc-grid-auto-gap: var(--fc-space-6); }
.fc-grid-auto--dense { --fc-grid-auto-min: 12rem; }
.fc-grid-auto--wide { --fc-grid-auto-min: 24rem; }
/*
* .fc-container -- centres content and enforces a maximum measure.
* inline-size/margin-inline are the logical forms of width/auto margins, so
* the container centres correctly in RTL with no extra rule.
*/
.fc-container {
inline-size: 100%;
max-inline-size: var(--fc-container-max, 80rem);
margin-inline: auto;
padding-inline: var(--fc-space-4);
}
.fc-container--narrow { --fc-container-max: 48rem; }
.fc-container--wide { --fc-container-max: 90rem; }
.fc-center {
display: grid;
place-items: center;
text-align: center;
}
/*
* .fc-truncate -- ellipsis on a single line.
* overflow:hidden is required for text-overflow to apply at all. Pair with a
* `title` attribute or tooltip in markup when the full value matters,
* because the truncated text is not otherwise exposed.
*/
.fc-truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-inline-size: 0;
}
/*
* .fc-clamp-2 / .fc-clamp-3 -- multi-line truncation. The -webkit-box display
* is the widely-supported implementation; `line-clamp` is the standard form.
* Where neither is supported the text simply shows in full, which is the
* correct failure mode: content stays reachable rather than disappearing.
*/
.fc-clamp-2,
.fc-clamp-3 {
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
}
.fc-clamp-2 { -webkit-line-clamp: 2; line-clamp: 2; }
.fc-clamp-3 { -webkit-line-clamp: 3; line-clamp: 3; }
/*
* .fc-scroll-x -- horizontal scroll region for tab lists and chip groups.
* scroll-snap is used because when a keyboard user tabs into a scroll
* container, the focused item should not be left off-screen.
*/
.fc-scroll-x {
overflow-x: auto;
overflow-y: hidden;
scroll-snap-type: inline proximity;
overscroll-behavior-inline: contain;
}
.fc-scroll-x > * {
scroll-snap-align: start;
}
/* Hide the scrollbar without losing scrollability (chip rows). */
.fc-scroll-x--hide {
scrollbar-width: none;
}
.fc-scroll-x--hide::-webkit-scrollbar {
display: none;
}
}
/*!
* future-css -- button component
*
* The reference component, and the template for every other component. The
* pattern repeated here:
*
* 1. Style the REAL element (`button`, not a div). A button is focusable,
* activatable with Space and Enter, and announced as a button for free.
* 2. Variants and sizes are separate classes, so they compose freely:
* `.fc-btn--danger.fc-btn--sm` is valid and correct.
* 3. Every state is styled -- rest, hover, active, focus, disabled. A
* component that styles only its rest state is unfinished.
* 4. Nothing references a colour literal or a primitive token.
*/
@layer fc.components {
.fc-btn {
/*
* inline-flex + gap rather than padding alone: it keeps the label centred
* when an icon is present, and lets the label wrap instead of forcing the
* control to overflow when translated.
*/
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--fc-space-2);
text-align: center;
text-decoration: none;
white-space: nowrap;
/*
* The same height as `.fc-input` and `.fc-select` at every size, so a
* button beside a field lines up without a wrapper fixing it. Comfortably
* above the WCAG 2.2 target size minimum.
*/
min-block-size: var(--fc-size-target-comfortable);
min-inline-size: var(--fc-size-target-min);
padding-block: var(--fc-space-2);
padding-inline: var(--fc-space-4);
border: var(--fc-border-width-control, var(--fc-border-width-thin)) solid transparent;
border-radius: var(--fc-btn-radius, var(--fc-radius-lg));
font-family: inherit;
font-size: var(--fc-font-size-sm);
font-weight: var(--fc-font-weight-medium);
line-height: var(--fc-line-height-snug);
cursor: pointer;
user-select: none;
/* Removes the ~300ms tap delay on touch devices. */
touch-action: manipulation;
/*
* Only the properties that actually change on interaction, and never `all`:
* transitioning everything animates layout properties and produces jank.
*/
transition:
background-color var(--fc-duration-fast) var(--fc-easing-standard),
border-color var(--fc-duration-fast) var(--fc-easing-standard),
color var(--fc-duration-fast) var(--fc-easing-standard),
box-shadow var(--fc-duration-fast) var(--fc-easing-standard),
scale var(--fc-duration-fast) var(--fc-easing-standard);
}
/*
* A press sinks the button slightly. `scale` rather than `transform` so it
* composes with any transform the caller sets, and it is a compositor-only
* property, so it never triggers layout. Disabled and busy buttons do not
* respond, because a control that moves when it cannot act is lying.
*/
.fc-btn:active:not(:disabled, [aria-disabled="true"], [aria-busy="true"]) {
scale: var(--fc-btn-press-scale, 0.97);
}
/* ---- Sizes ----------------------------------------------------------- */
.fc-btn--sm {
min-block-size: 2rem;
--fc-btn-radius: var(--fc-radius-md);
--fc-btn-corner-limit: 1rem;
padding-block: var(--fc-space-1);
padding-inline: var(--fc-space-3);
font-size: var(--fc-font-size-xs);
}
.fc-btn--lg {
min-block-size: 3rem;
padding-block: var(--fc-space-3);
padding-inline: var(--fc-space-6);
font-size: var(--fc-font-size-md);
}
/* Icon-only buttons stay square so the hit target matches the glyph. */
.fc-btn--icon {
padding-inline: var(--fc-space-2);
aspect-ratio: 1;
}
/* Full width is a layout decision, kept separate from visual variants. */
.fc-btn--block {
display: flex;
inline-size: 100%;
}
/* ---- Variants -------------------------------------------------------- */
/*
* Hover rules sit inside `(hover: hover)`. On touch screens a tap leaves the
* last-tapped button in its hover colour until something else is tapped,
* which reads as a stuck control.
*
* The neutral variants tint with `currentColor` at the shared overlay
* opacities instead of swapping to a fixed surface token. A fixed token is
* only right on the surface it was picked against -- `bg-subtle` matches
* `bg-surface` exactly in dark mode, which made the subtle button invisible
* on a card -- while a tint of the label colour stays visible on any surface
* in either scheme.
*/
/* Default / primary. The faint shadow lifts it off the page as the one
action to take. */
.fc-btn--primary,
.fc-btn--ok {
background-color: var(--fc-btn-primary-bg, var(--fc-color-accent-bg));
color: var(--fc-btn-primary-fg, var(--fc-color-text-on-accent));
border-color: var(--fc-btn-primary-border, var(--fc-color-accent-border));
box-shadow: var(--fc-shadow-raised);
}
/* Secondary: an outlined button that sits beside a primary without competing. */
.fc-btn--secondary,
.fc-btn--cancel {
background-color: var(--fc-color-bg-surface);
color: var(--fc-color-text-default);
border-color: var(--fc-color-border-interactive);
}
/* Ghost: no chrome until interacted with. Low affordance -- use sparingly. */
.fc-btn--ghost,
.fc-btn--tertiary {
background-color: transparent;
color: var(--fc-color-text-default);
border-color: transparent;
}
/* Subtle: a tinted fill for the least important of several actions. */
.fc-btn--subtle {
background-color: color-mix(in oklab, currentColor calc(var(--fc-opacity-hover-overlay) * 100%), transparent);
color: var(--fc-color-text-default);
border-color: transparent;
}
/*
* Destructive. Always paired with a confirmation step in the markup: a
* danger-styled button is a request, not a confirmation.
*/
.fc-btn--danger {
background-color: var(--fc-color-danger-bg);
color: var(--fc-color-text-on-danger);
border-color: var(--fc-color-danger-border);
box-shadow: var(--fc-shadow-raised);
}
@media (hover: hover) {
.fc-btn--primary:hover,
.fc-btn--ok:hover {
background-color: var(--fc-btn-primary-bg-hover, var(--fc-color-accent-bg-hover));
}
.fc-btn--secondary:hover,
.fc-btn--cancel:hover {
border-color: color-mix(in oklab, var(--fc-color-border-interactive), var(--fc-color-text-default) 40%);
}
.fc-btn--secondary:hover,
.fc-btn--cancel:hover,
.fc-btn--ghost:hover,
.fc-btn--tertiary:hover {
background-color: color-mix(in oklab, currentColor calc(var(--fc-opacity-hover-overlay) * 100%), transparent);
}
.fc-btn--subtle:hover {
background-color: color-mix(in oklab, currentColor calc(var(--fc-opacity-pressed-overlay) * 100%), transparent);
}
.fc-btn--danger:hover {
background-color: var(--fc-color-danger-bg-hover);
border-color: var(--fc-color-danger-bg-hover);
}
}
/* Pressed. After the hover block, because a pressed button is also hovered
and the two have equal specificity. */
.fc-btn--primary:active,
.fc-btn--ok:active {
background-color: var(--fc-btn-primary-bg-active, var(--fc-color-accent-bg-active));
box-shadow: none;
}
.fc-btn--secondary:active,
.fc-btn--cancel:active,
.fc-btn--ghost:active,
.fc-btn--tertiary:active {
background-color: color-mix(in oklab, currentColor calc(var(--fc-opacity-pressed-overlay) * 100%), transparent);
}
.fc-btn--subtle:active {
background-color: color-mix(in oklab, currentColor calc(var(--fc-opacity-pressed-overlay) * 150%), transparent);
}
.fc-btn--danger:active {
box-shadow: none;
}
/* ---- States ---------------------------------------------------------- */
/*
* The base focus ring paints its offset gap with box-shadow, and this layer
* outranks base, so the raised variants' own shadow would erase the gap.
* Restated here so every variant keeps it.
*/
.fc-btn:focus-visible {
box-shadow: 0 0 0 var(--fc-space-0-5) var(--fc-color-focus-ring-offset);
}
/*
* Disabled. Two details matter:
* - `opacity` alone is not enough, since it drags the label below the
* contrast minimum; dedicated disabled colors preserve label readability.
* - `cursor: not-allowed` comes from the reset layer.
*
* `:disabled` handles the truly disabled control. `aria-disabled` is the
* preferred pattern for a control that must stay focusable so the user can
* perceive why it is unavailable, and is handled just below.
*/
.fc-btn:disabled,
.fc-btn[aria-disabled="true"] {
background-color: var(--fc-color-disabled-bg);
color: var(--fc-color-disabled-fg);
border-color: var(--fc-color-disabled-border);
box-shadow: none;
cursor: not-allowed;
opacity: 1;
}
/* Blocks activation without removing the element from the tab order. */
.fc-btn[aria-disabled="true"] {
pointer-events: none;
}
/*
* Loading. The label stays in the DOM rather than being swapped for a spinner,
* so the accessible name does not change and the control is never left
* unlabelled.
*/
.fc-btn[aria-busy="true"] {
cursor: progress;
}
.fc-btn__spinner {
inline-size: 1em;
block-size: 1em;
border: var(--fc-border-width-thick) solid currentColor;
/* Logical longhand, so the spin direction reads correctly in RTL. */
border-block-start-color: transparent;
border-radius: var(--fc-radius-full);
animation: fc-spin 0.6s linear infinite;
}
@keyframes fc-spin {
to {
transform: rotate(1turn);
}
}
/*
* Reduced motion: a spinner is the one case where stopping the animation
* entirely would remove the only loading affordance, so it slows to a crawl
* instead of freezing. This deliberately overrides the blanket reset.
*/
@media (prefers-reduced-motion: reduce) {
.fc-btn__spinner {
animation-duration: 2s;
}
}
/* ---- Forced colors --------------------------------------------------- */
/*
* In Windows High Contrast the OS discards background and border colours. The
* remaining job is making the edges visible, done with a border because
* box-shadow is also suppressed.
*/
@media (forced-colors: active) {
.fc-btn {
background-color: ButtonFace;
color: ButtonText;
border-color: ButtonBorder;
}
.fc-btn--primary,
.fc-btn--ok {
background-color: Highlight;
color: HighlightText;
border-color: Highlight;
}
}
}
/*!
* future-css -- icon
*
* An inline SVG from the built-in set (`future-css/icons`), drawn in the
* current text colour at the current font size:
*
* <svg class="fc-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
* <path d="..."/>
* </svg>
*
* <button class="fc-btn fc-btn--ghost fc-btn--icon" type="button" aria-label="Copy">
* <svg class="fc-icon" ...>...</svg>
* </button>
*
* The icon has no states of its own. It is never the interactive element --
* the button or link around it is -- so hover, focus, disabled and busy are the
* control's, and the icon follows them through `currentColor`. That is why
* nothing here names a colour token: a disabled button's icon is disabled-grey
* because its label is.
*
* Sized in `em` so an icon matches the text beside it at every type size and
* zoom level, and an icon-only `.fc-btn--sm` gets a smaller glyph without a
* second modifier.
*/
@layer fc.components {
.fc-icon {
display: inline-block;
flex-shrink: 0;
inline-size: var(--fc-icon-size, 1.25em);
block-size: var(--fc-icon-size, 1.25em);
/* Sits on the text's optical centre rather than its baseline when the icon
is inline in running text; flex parents ignore it. */
vertical-align: -0.25em;
fill: none;
stroke: currentColor;
stroke-width: var(--fc-icon-stroke, 2);
stroke-linecap: round;
stroke-linejoin: round;
/*
* Clicks land on the control, not the SVG. Without this, `event.target` is
* the <path> for most of an icon button's area, and delegated handlers
* that compare targets rather than calling `closest()` miss the click.
*/
pointer-events: none;
}
.fc-icon--sm {
--fc-icon-size: 1em;
}
.fc-icon--lg {
--fc-icon-size: 1.5em;
}
/*
* Icons that point along the line of text (back, forward, send, undo) flip
* in RTL so they keep pointing the way the reader travels. `scale` rather
* than `transform`, so a caller's own transform still composes.
*/
.fc-icon--mirror:dir(rtl) {
scale: -1 1;
}
/*
* Motion: none. Forced colours: nothing to restate -- the stroke is
* `currentColor`, which resolves to the forced text or button-text colour of
* the container, so the icon stays visible against the system background.
*/
}
/* Native radios/selects retain their keyboard and no-JS contracts. Theme changes
add no animation, so reduced motion needs no component-specific reset. */
@layer fc.components {
.fc-theme-picker { display: grid; gap: var(--fc-space-4); min-inline-size: 0; color: var(--fc-color-text-default); }
.fc-theme-picker__themes { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); gap: var(--fc-space-2); min-inline-size: 0; margin: 0; padding: 0; border: 0; }
.fc-theme-picker__legend { padding: 0; margin-block-end: var(--fc-space-3); font-weight: var(--fc-font-weight-semibold); }
.fc-theme-picker__option { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: start; gap: var(--fc-space-3); min-block-size: var(--fc-size-target-comfortable); padding: var(--fc-space-3); border: var(--fc-border-width-control, var(--fc-border-width-thin)) solid var(--fc-color-border-interactive); border-radius: var(--fc-radius-lg); background-color: var(--fc-color-bg-surface); background-image: none; color: var(--fc-color-text-default); box-shadow: var(--fc-shadow-raised); font-family: var(--fc-font-family-sans); cursor: pointer; overflow-wrap: anywhere; }
.fc-theme-picker__option:hover { background-color: var(--fc-color-bg-subtle); }
.fc-theme-picker__option:active { background-color: var(--fc-color-bg-muted); }
.fc-theme-picker__option:has(:checked) { background-color: var(--fc-color-selected-bg); color: var(--fc-color-selected-fg); border-color: var(--fc-color-accent-border); }
.fc-theme-picker__option:has(:focus-visible) { outline: var(--fc-border-width-focus) solid var(--fc-color-focus-ring); outline-offset: var(--fc-space-0-5); }
.fc-theme-picker__option:has(:disabled), .fc-theme-picker__option[aria-disabled="true"] { background-color: var(--fc-color-disabled-bg); color: var(--fc-color-disabled-fg); cursor: not-allowed; }
.fc-theme-picker__preview { display: block; margin-block-end: var(--fc-space-3); }
.fc-theme-picker__window { display: block; border: var(--fc-border-width-surface, var(--fc-border-width-thin)) solid var(--fc-color-border-interactive); border-radius: var(--fc-radius-md); background-color: var(--fc-color-bg-canvas); background-image: var(--fc-theme-canvas-image, none); overflow: clip; font-size: var(--fc-font-size-xs); box-shadow: var(--fc-shadow-raised); }
.fc-theme-picker__window-bar { display: block; padding: var(--fc-space-2); background-color: var(--fc-masthead-bg); color: var(--fc-masthead-fg); border-block-end: var(--fc-border-width-thin) solid var(--fc-color-border-interactive); }
.fc-theme-picker__window-body { display: flex; min-inline-size: 0; }
.fc-theme-picker__window-nav { display: grid; align-content: start; gap: var(--fc-space-2); padding: var(--fc-space-2); background-color: var(--fc-color-bg-subtle); color: var(--fc-color-text-default); }
.fc-theme-picker__window-content { display: grid; flex: 1; gap: var(--fc-space-2); min-inline-size: 0; padding: var(--fc-space-2); }
.fc-theme-picker__window-feature { display: block; padding: var(--fc-space-2); border-radius: var(--fc-radius-sm); background-color: var(--fc-theme-feature-bg); background-image: var(--fc-theme-feature-image); text-shadow: var(--fc-theme-feature-text-shadow, none); color: var(--fc-theme-feature-fg); }
.fc-theme-picker__preview-card { display: block; padding: var(--fc-space-2); background-color: var(--fc-card-bg, var(--fc-color-bg-surface)); color: var(--fc-color-text-default); border: var(--fc-border-width-surface, var(--fc-border-width-thin)) var(--fc-card-border-style, solid) var(--fc-card-border, var(--fc-color-border-interactive)); border-radius: var(--fc-radius-md); box-shadow: var(--fc-shadow-raised); }
.fc-theme-picker__preview-action { display: block; padding: var(--fc-space-1) var(--fc-space-2); background-color: var(--fc-btn-primary-bg); color: var(--fc-btn-primary-fg); border: var(--fc-border-width-control, var(--fc-border-width-thin)) solid var(--fc-btn-primary-border); border-radius: var(--fc-radius-md); }
.fc-theme-picker__editor { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr)); gap: var(--fc-space-3); padding: var(--fc-space-4); border: var(--fc-border-width-control, var(--fc-border-width-thin)) solid var(--fc-color-border-interactive); border-radius: var(--fc-radius-lg); }
.fc-theme-picker__name { display: block; font-size: var(--fc-font-size-sm); }
.fc-theme-picker__description { display: block; margin-block-start: var(--fc-space-1); font-size: var(--fc-font-size-sm); line-height: var(--fc-line-height-normal); }
.fc-theme-picker__controls { display: flex; flex-wrap: wrap; gap: var(--fc-space-3); }
.fc-theme-picker__field { display: grid; gap: var(--fc-space-1); flex: 1 1 12rem; min-inline-size: 0; font-size: var(--fc-font-size-sm); }
.fc-theme-picker[data-fc-state="pending"] { border-block-start: var(--fc-border-width-thick) dashed var(--fc-color-border-interactive); }
.fc-theme-picker[data-fc-state="error"] { border-block-start: var(--fc-border-width-thick) solid var(--fc-color-danger-border); }
.fc-theme-picker__status { margin: 0; color: var(--fc-color-text-muted); font-size: var(--fc-font-size-sm); }
@media (forced-colors: active) {
.fc-theme-picker__window { background-image: none; }
.fc-theme-picker__window-feature { background-image: none; text-shadow: none; }
.fc-theme-picker__option { background-color: Canvas; color: CanvasText; border-color: ButtonText; }
.fc-theme-picker__option:has(:checked) { border-color: Highlight; outline: var(--fc-border-width-thick) solid Highlight; }
.fc-theme-picker__option:has(:focus-visible) { outline-color: Highlight; }
.fc-theme-picker__option:has(:disabled), .fc-theme-picker__option[aria-disabled="true"] { color: GrayText; }
}
}
/*!
* future-css -- chat surface
*
* A general-purpose framework component, for applications that present a
* conversational or turn-based interface. It is not an agent-specific toolkit:
* the same components serve a human-to-human support desk, a guided wizard, and
* an agent transcript.
*
* It is, however, where the framework's dual-audience contract (see
* PRINCIPLES.md) is under the most pressure, because chat content arrives
* partially: tokens stream in, tool calls run for minutes, states change while
* someone is reading, and long values are truncated for display.
*
* DUAL-AUDIENCE CONTRACT
* ======================
* Every fact a machine consumer might need is present in the DOM as a
* `data-fc-*` attribute, not only as visible text or a CSS class:
*
* data-fc-kind what this node is (thread, message, part, tool-call)
* data-fc-id stable unique id, mirroring the runtime id
* data-fc-seq monotonic integer; ordering without reading text
* data-fc-role message author (user, assistant, system, tool)
* data-fc-state lifecycle (pending, streaming, running, complete,
* aborted, error, denied)
* data-fc-ts ISO-8601 or epoch-ms timestamp
* data-fc-truncated "true" when visible text has been elided
*
* These attributes are for automation and instrumentation. They are additive:
* a human using a screen reader is served by the ARIA layer and the visible
* text, and the styling below never depends on the attributes being read.
*
* Two rules keep the attributes trustworthy:
*
* 1. Visible text and machine state must never diverge. Both render from one
* source of truth, so a consumer reading `data-fc-state="error"` is never
* misled by a stale success label in the visible header.
*
* 2. Truncation is always declared. When text is clipped for display the full
* value remains available and `data-fc-truncated="true"` is set, so a
* consumer can distinguish "this is the whole text" from "this is a
* summary of more".
*/
@layer fc.components {
/*
* .fc-chat -- the thread container.
*
* `role="log"` and `aria-live` are applied by the consuming markup rather
* than here: a live region on the thread itself would announce every streamed
* token. See the streaming notes in ACCESSIBILITY.md.
*/
.fc-chat {
display: flex;
flex-direction: column;
gap: var(--fc-chat-gap, var(--fc-space-4));
background-color: var(--fc-chat-bg, var(--fc-color-bg-canvas));
color: var(--fc-chat-fg, var(--fc-color-text-default));
block-size: 100%;
min-block-size: 0;
/* Keeps a long thread scrollable while the composer stays pinned. */
overflow-y: auto;
overscroll-behavior-block: contain;
/* Room for the focus ring of the first and last children. */
padding-block: var(--fc-space-4);
padding-inline: var(--fc-space-4);
}
/* Groups consecutive messages from one author. */
.fc-chat__group {
display: flex;
flex-direction: column;
gap: var(--fc-space-2);
}
/*
* .fc-msg -- a single message.
*
* The author is conveyed by `data-fc-role`, not by a positional icon, so an
* agent (and a screen reader) can identify the author without inferring it
* from position or color.
*/
.fc-msg {
display: flex;
flex-direction: column;
gap: var(--fc-space-2);
max-inline-size: var(--fc-chat-message-measure, var(--fc-size-measure, 65ch));
min-inline-size: 0;
}
/*
* Alignment by role. `align-self: flex-end` resolves against the inline axis,
* so a user message sits on the inline-end edge in both LTR and RTL with no
* direction-specific override.
*/
.fc-msg[data-fc-role="user"] {
align-self: flex-end;
align-items: flex-end;
}
.fc-msg[data-fc-role="assistant"],
.fc-msg[data-fc-role="system"] {
align-self: flex-start;
}
/*
* Role accent on the inline-start edge, so the marker moves to the correct
* side automatically when the document direction flips.
*/
.fc-msg[data-fc-role="assistant"] {
border-inline-start: var(--fc-border-width-thick) solid var(--fc-color-border-default);
padding-inline-start: var(--fc-space-3);
}
.fc-msg[data-fc-role="user"] {
background-color: var(--fc-color-bg-subtle);
border-radius: var(--fc-radius-lg);
padding: var(--fc-space-3);
}
/*
* System and tool messages are chrome rather than conversation, so they are
* visually recessive -- but never hidden. An agent debugging a run needs to
* see exactly what the system reported.
*/
.fc-msg[data-fc-role="system"],
.fc-msg[data-fc-role="tool"] {
font-size: var(--fc-font-size-sm);
color: var(--fc-color-text-muted);
}
/* ---- Message parts ---------------------------------------------------- */
/*
* .fc-msg__part -- one renderable unit of a message (text block, code block,
* tool call, artifact). Parts are addressable individually so an agent can
* reason about structure instead of re-parsing prose.
*/
.fc-msg__part {
display: flex;
flex-direction: column;
gap: var(--fc-space-2);
min-inline-size: 0;
}
/* Header line: author label, timestamp, state. */
.fc-msg__meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--fc-space-2);
font-size: var(--fc-font-size-xs);
color: var(--fc-color-text-muted);
}
.fc-msg__author {
font-weight: var(--fc-font-weight-semibold);
color: var(--fc-color-text-default);
}
/*
* Timestamps are machine-readable via data-fc-ts; the visible form is
* abbreviated so a dense thread stays scannable.
*/
.fc-msg__time {
font-variant-numeric: tabular-nums;
}
/*
* .fc-msg__body -- the text content.
*
* `white-space: pre-wrap` is essential for generated content: models emit
* paragraphs, indented lists, and code that must not be reflowed or collapsed.
* `overflow-wrap: anywhere` then lets a long unbroken token (a URL, a base64
* fragment) break instead of forcing the container to overflow.
*/
.fc-msg__body {
white-space: pre-wrap;
overflow-wrap: anywhere;
/* Generated text must never inherit a stray author style. */
font: inherit;
color: var(--fc-color-text-default);
min-inline-size: 0;
}
/*
* Streaming state: a caret is drawn at the end of the text while tokens are
* still arriving, so a reader can tell "still writing" from "finished".
*
* The blink is disabled by the reset layer under prefers-reduced-motion; the
* solid caret remains, so the affordance survives without the animation.
*/
.fc-msg[data-fc-state="streaming"] .fc-msg__body::after {
content: "";
display: inline-block;
inline-size: 0.5em;
block-size: 1em;
margin-inline-start: 0.125em;
background-color: var(--fc-color-accent-text);
vertical-align: text-bottom;
animation: fc-caret 1s steps(2, jump-none) infinite;
}
@keyframes fc-caret {
50% {
opacity: 0;
}
}
/*
* A pending message has no content yet. Reserving space with a skeleton
* prevents layout shift when the first token lands -- a real problem in chat,
* where every message would otherwise shove the viewport.
*/
.fc-msg[data-fc-state="pending"] .fc-msg__body:empty {
min-block-size: 1lh;
/* Reserve roughly two lines so a one-line reply does not shift the page. */
min-inline-size: 12rem;
inline-size: 40%;
background-color: var(--fc-color-bg-subtle);
border-radius: var(--fc-radius-sm);
animation: fc-pulse 1.6s ease-in-out infinite;
}
@keyframes fc-pulse {
50% {
opacity: 0.5;
}
}
/*
* Reduced motion.
*
* The reset layer's blanket rule would drive these animations to 0.01ms while
* leaving them `infinite`, turning a slow blink into a rapid flicker -- worse
* than no animation at all. Both are therefore stopped outright here, and each
* keeps a static affordance so no information is lost: the caret stays solid,
* and the pending skeleton keeps its tint.
*/
@media (prefers-reduced-motion: reduce) {
.fc-msg[data-fc-state="streaming"] .fc-msg__body::after {
animation: none;
}
.fc-msg[data-fc-state="pending"] .fc-msg__body:empty {
animation: none;
}
}
/*
* Truncated content. The clamp is a visual affordance for humans; the
* machine-readable truth is `data-fc-truncated="true"`, with the full value
* kept reachable, so a consumer is never misled into thinking it has the
* whole text.
*/
.fc-msg[data-fc-truncated="true"] .fc-msg__body {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 12;
line-clamp: 12;
overflow: hidden;
}
/* An errored or aborted message stays in place with its state visible. */
/*
* The full border, not only its colour: user and system messages have no
* inline-start edge to recolour, so an error on them would otherwise draw
* nothing at all.
*/
.fc-msg[data-fc-state="error"] {
border-inline-start: var(--fc-border-width-thick) solid var(--fc-color-danger-border);
padding-inline-start: var(--fc-space-3);
}
.fc-msg[data-fc-state="aborted"] {
border-inline-start: var(--fc-border-width-thick) solid var(--fc-color-warning-border);
padding-inline-start: var(--fc-space-3);
}
/* ---- Message actions -------------------------------------------------- */
/*
* Copy, retry, and feedback controls under a message. They recede until the
* message is hovered or anything inside it has focus, so a long thread is not
* a wall of icons -- but they are never removed from the tab order, and on a
* device with no hover they are always shown, since "hover to reveal" would
* make them unreachable by touch.
*/
.fc-msg__actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--fc-space-0-5);
margin-inline-start: calc(var(--fc-space-1-5) * -1);
opacity: 0;
transition: opacity var(--fc-duration-fast) var(--fc-easing-standard);
}
.fc-msg:hover .fc-msg__actions,
.fc-msg:focus-within .fc-msg__actions,
.fc-msg__actions--persistent {
opacity: 1;
}
@media (hover: none) {
.fc-msg__actions {
opacity: 1;
}
}
.fc-msg__action {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--fc-space-1);
min-inline-size: 2rem;
min-block-size: 2rem;
padding-inline: var(--fc-space-1-5);
background-color: transparent;
color: var(--fc-color-text-muted);
border: 0;
border-radius: var(--fc-radius-md);
font: inherit;
font-size: var(--fc-font-size-xs);
cursor: pointer;
transition:
background-color var(--fc-duration-fast) var(--fc-easing-standard),
color var(--fc-duration-fast) var(--fc-easing-standard);
}
.fc-msg__action:hover {
background-color: var(--fc-color-bg-muted);
color: var(--fc-color-text-default);
}
.fc-msg__action:active {
background-color: var(--fc-color-bg-subtle);
}
/* Thumbs up/down and similar toggles: aria-pressed is the state. */
.fc-msg__action[aria-pressed="true"] {
color: var(--fc-color-selected-fg);
background-color: var(--fc-color-selected-bg);
}
.fc-msg__action:disabled,
.fc-msg__action[aria-disabled="true"] {
color: var(--fc-color-disabled-fg);
background-color: transparent;
cursor: not-allowed;
}
/*
* Forced colors.
*
* Three affordances in this component are carried by background color, which the
* OS discards here: the role distinction between user/assistant messages, the
* pending skeleton, and the streaming caret. Each is restated with a system
* color or a border so the thread stays readable as a thread rather than
* collapsing into undifferentiated grey boxes.
*/
@media (forced-colors: active) {
.fc-chat {
background-color: Canvas;
}
.fc-msg {
border: var(--fc-border-width-thin) solid CanvasText;
}
/*
* Role is also carried by data-fc-role and by an author label in the markup,
* so colour here is reinforcement rather than the sole signal -- which is
* what lets it be replaced without losing information.
*/
.fc-msg[data-fc-role="user"] {
border-color: Highlight;
}
.fc-msg[data-fc-role="assistant"] {
border-color: CanvasText;
}
.fc-msg[data-fc-state="pending"] .fc-msg__body:empty {
background-color: GrayText;
/* The skeleton pulse is an animation; without motion it is a solid block,
* which still reserves the space and so still prevents layout shift. */
}
.fc-msg[data-fc-state="streaming"] .fc-msg__body::after {
background-color: Highlight;
}
.fc-msg[data-fc-state="error"],
.fc-msg[data-fc-state="aborted"] {
border-color: LinkText;
}
.fc-msg__action[aria-pressed="true"] {
outline: var(--fc-border-width-thin) solid Highlight;
}
}
}
/*!
* future-css -- tool call / step trace
*
* A general-purpose component for rendering a recorded or in-progress operation:
* an API request, a database query, a file operation, a build step, a device
* command. Agent runtimes are one consumer of it; so is any application that
* needs to show the user what a system is doing on their behalf.
*
* It is the most information-dense component in the system, and the clearest
* test of the dual-audience contract (PRINCIPLES.md):
*
* - Every field is individually addressable via data attributes: name,
* arguments, result, duration, state, and error.
* - `data-fc-state` drives appearance. There is no "success class" that can
* fall out of sync, because the state attribute is the single source of
* truth for both the styles and the DOM contract.
* - Arguments and results may be truncated for display, and truncation is
* declared, so a full value is never silently lost.
* - Duration is rendered as a number rather than a spinner, because "how long
* did that take" must be answerable without extra work.
*
* Colour is never the only signal: each state also carries a text label and a
* glyph, so the state survives greyscale, forced-colors mode, and any future
* monochrome rendering.
*/
@layer fc.components {
/*
* .fc-tool -- the container for a single tool invocation.
*
* Not a <button>: it may be a disclosure region, a static record, or a
* clickable summary. Interactive behaviour belongs to the <summary> or
* <button> inside it, which keeps the container's role honest.
*/
.fc-tool {
display: flex;
flex-direction: column;
gap: var(--fc-space-2);
min-inline-size: 0;
background-color: var(--fc-color-bg-surface);
border: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
border-radius: var(--fc-radius-md);
padding: var(--fc-space-3);
font-family: var(--fc-font-family-mono);
font-size: var(--fc-font-size-xs);
color: var(--fc-color-text-default);
}
/*
* A running tool call gets an inline-start accent bar and a subtle tint, so
* an active operation is findable at a glance in a long trace.
*/
.fc-tool[data-fc-state="running"] {
border-inline-start: var(--fc-border-width-thick) solid var(--fc-color-accent-border);
background-color: var(--fc-color-accent-bg-subtle);
}
.fc-tool[data-fc-state="complete"] {
border-inline-start: var(--fc-border-width-thick) solid var(--fc-color-success-border);
}
.fc-tool[data-fc-state="error"] {
border-inline-start: var(--fc-border-width-thick) solid var(--fc-color-danger-border);
background-color: var(--fc-color-danger-bg-subtle);
}
/*
* `denied` is a distinct state from `error`: a rejected permission is a
* correct, expected outcome and must not read as a failure to the agent.
*/
.fc-tool[data-fc-state="denied"] {
border-inline-start: var(--fc-border-width-thick) solid var(--fc-color-warning-border);
background-color: var(--fc-color-warning-bg-subtle);
}
/*
* `aborted` means the run was cancelled partway. It is visually distinct from
* `denied` so an agent can tell "I stopped this" from "policy stopped this".
*/
.fc-tool[data-fc-state="aborted"] {
border-inline-start: var(--fc-border-width-thick) solid var(--fc-color-border-strong);
}
/* ---- Header row -------------------------------------------------------- */
.fc-tool__header {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--fc-space-2);
min-inline-size: 0;
}
/* The tool name, as the visual anchor of the row. */
.fc-tool__name {
font-weight: var(--fc-font-weight-semibold);
color: var(--fc-color-text-default);
min-inline-size: 0;
}
/*
* The state badge. Carries the state in text as well as colour -- the glyph
* and the label both encode it, so it survives greyscale and forced colors.
*/
.fc-tool__state {
display: inline-flex;
align-items: center;
gap: var(--fc-space-1);
padding-inline: var(--fc-space-2);
padding-block: var(--fc-space-0-5);
border-radius: var(--fc-radius-full);
border: var(--fc-border-width-thin) solid var(--fc-color-border-default);
background-color: var(--fc-color-bg-subtle);
color: var(--fc-color-text-default);
font-family: var(--fc-font-family-sans);
font-size: var(--fc-font-size-2xs);
/* Never letter-spaced: it hurts legibility at this size. */
letter-spacing: normal;
}
.fc-tool[data-fc-state="running"] .fc-tool__state {
border-color: var(--fc-color-accent-border);
color: var(--fc-color-accent-text);
background-color: var(--fc-color-accent-bg-subtle);
}
.fc-tool[data-fc-state="complete"] .fc-tool__state {
border-color: var(--fc-color-success-border);
color: var(--fc-color-success-text);
background-color: var(--fc-color-success-bg-subtle);
}
.fc-tool[data-fc-state="error"] .fc-tool__state {
border-color: var(--fc-color-danger-border);
color: var(--fc-color-danger-text);
background-color: var(--fc-color-danger-bg-subtle);
}
.fc-tool[data-fc-state="denied"] .fc-tool__state {
border-color: var(--fc-color-warning-border);
color: var(--fc-color-warning-text);
background-color: var(--fc-color-warning-bg-subtle);
}
/*
* Duration. Tabular numerals so a column of timings lines up and can be
* compared at a glance; this is exactly the case where proportional digits
* are wrong.
*/
.fc-tool__duration {
margin-inline-start: auto;
font-variant-numeric: tabular-nums;
color: var(--fc-color-text-muted);
}
/* ---- Arguments and results --------------------------------------------- */
/*
* Label for a payload section. Visually small, but these labels are the
* primary way a consumer learns the shape of the call, so they are real text
* rather than a synthesized `content:` string.
*/
.fc-tool__label {
color: var(--fc-color-text-muted);
font-family: var(--fc-font-family-sans);
font-size: var(--fc-font-size-2xs);
}
/*
* .fc-tool__payload -- a preformatted argument or result value.
*
* `pre-wrap` plus `overflow-wrap: anywhere` means a long JSON blob or a
* minified payload wraps rather than forcing the card to scroll sideways,
* which would otherwise make the call unusable on a phone.
*/
.fc-tool__payload {
margin: 0;
padding: var(--fc-space-2);
background-color: var(--fc-color-bg-sunken);
border-radius: var(--fc-radius-sm);
border: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
color: var(--fc-color-text-default);
white-space: pre-wrap;
overflow-wrap: anywhere;
max-block-size: 20rem;
overflow-y: auto;
font-family: var(--fc-font-family-mono);
font-size: var(--fc-font-size-xs);
}
/*
* A long payload is clipped, and the clip is declared. When the caller knows
* the value is partial, `data-fc-truncated="true"` must be present on the
* .fc-tool element; this rule only performs the visual clipping.
*/
.fc-tool__payload[data-fc-truncated="true"] {
max-block-size: 8rem;
/* Fade the clip edge so a cut-off line reads as "continues below". */
mask-image: linear-gradient(to bottom, black 60%, transparent 100%);
}
/* An error message gets the danger treatment but keeps full contrast. */
.fc-tool__error {
padding: var(--fc-space-2);
border-radius: var(--fc-radius-sm);
background-color: var(--fc-color-danger-bg-subtle);
color: var(--fc-color-danger-text);
border: var(--fc-border-width-thin) solid var(--fc-color-danger-border);
white-space: pre-wrap;
overflow-wrap: anywhere;
}
/* ---- Progress ---------------------------------------------------------- */
/*
* A progress track for a call that can report its own progress. The consumer
* sets `style="--fc-tool-progress: 42%"` from real data; the styling below
* only presents it.
*/
.fc-tool__progress {
block-size: var(--fc-border-width-thick);
border-radius: var(--fc-radius-full);
background-color: var(--fc-color-bg-muted);
overflow: hidden;
}
.fc-tool__progress-bar {
block-size: 100%;
inline-size: var(--fc-tool-progress, 0%);
background-color: var(--fc-color-accent-bg);
transition: inline-size var(--fc-duration-normal) var(--fc-easing-standard);
}
/*
* Unknown progress: a sliding indeterminate sweep. Reduced motion disables the
* animation via the reset layer, and the track stays visible -- so the
* component still communicates "working, extent unknown" without movement.
*/
.fc-tool[data-fc-state="running"] .fc-tool__progress-bar {
inline-size: 40%;
animation: fc-indeterminate 1.4s ease-in-out infinite;
}
@keyframes fc-indeterminate {
from {
transform: translateX(-100%);
}
to {
transform: translateX(250%);
}
}
/*
* RTL sweep.
*
* `translateX` is a *physical* transform: +X is always rightward, with no logical
* equivalent, so logical properties cannot make it direction-aware. This is one
* of the small number of places the system genuinely needs an explicit direction
* branch -- see the "What is genuinely direction-sensitive" section in
* INTERNATIONALIZATION.md.
*
* Without this, the indeterminate bar sweeps out of the left edge in an RTL
* interface, which reads as a progress indicator running backwards.
*/
:dir(rtl) .fc-tool[data-fc-state="running"] .fc-tool__progress-bar {
animation-name: fc-indeterminate-rtl;
}
@keyframes fc-indeterminate-rtl {
from {
transform: translateX(100%);
}
to {
transform: translateX(-250%);
}
}
/*
* Reduced motion.
*
* An indeterminate sweep is exactly the kind of large, repeated motion that
* vestibular conditions make unpleasant, so it is removed entirely rather than
* slowed. The track itself stays visible, so the component still communicates
* "working, extent unknown" through a static affordance -- which is why the
* stopping rule below targets the bar and not the track.
*
* A determinate bar (`--fc-tool-progress`) animates its inline-size from a real
* measured value to a new one. That is a small, non-looping movement tied to
* real progress, so it is kept: the reset layer's blanket rule shortens it
* rather than freezing it, and a user who needs it fully static can override.
*/
@media (prefers-reduced-motion: reduce) {
.fc-tool[data-fc-state="running"] .fc-tool__progress-bar {
animation: none;
/* Sit at the leading edge rather than mid-sweep, which would read as a
* progress bar stuck at 40% instead of an indeterminate state. */
inline-size: 100%;
opacity: var(--fc-opacity-hover-overlay);
}
}
/*
* Reduced motion.
*
* The reset layer stops the sweep, which would leave the bar frozen at a static
* 40% -- and a fixed-width bar reads as determinate, i.e. "40% complete". That
* is worse than no indicator at all: it states something false.
*
* So the bar is instead drawn full-width with a striped fill that carries no
* positional meaning. It still says "working, extent unknown" without moving,
* and without implying a percentage.
*/
@media (prefers-reduced-motion: reduce) {
.fc-tool[data-fc-state="running"] .fc-tool__progress-bar {
inline-size: 100%;
animation: none;
/* Repeating-linear-gradient survives reduced motion because nothing
* animates it; it is a static texture, not motion. */
background-image: repeating-linear-gradient(
135deg,
var(--fc-color-accent-bg) 0 0.5rem,
transparent 0.5rem 1rem
);
}
}
/*
* Forced colors.
*
* The OS discards background-color and box-shadow here, so the two affordances
* this component relies on would both vanish: the state tint that distinguishes
* running from error, and the progress track. Both are replaced with system
* colors so the component remains readable rather than becoming an unlabelled
* grey box -- "not a blank rectangle" per the accessibility contract.
*/
@media (forced-colors: active) {
.fc-tool {
border-color: CanvasText;
}
.fc-tool__progress-track {
background-color: Canvas;
border: var(--fc-border-width-thin) solid CanvasText;
/* A border survives forced-colors where a background difference does not. */
}
.fc-tool__progress-bar {
background-color: Highlight;
}
/* State is conveyed by the text label as well as colour, so the system
* colors only need to keep the components distinguishable, not decorative. */
.fc-tool[data-fc-state="error"] {
border-color: LinkText;
}
}
}
/*!
* future-css -- rich text editor
*
* A formatting toolbar over a contenteditable region, plus the document styles
* the editor produces.
*
* Design constraints that shaped this:
*
* - **The toolbar is real buttons in a real toolbar.** `<div role="toolbar">`
* with click handlers re-implements what `<button>` and `role` already give.
* Every item is focusable and announces its pressed state.
* - **State is carried by `aria-pressed`, not a class.** The button's
* appearance and its announced state then cannot disagree, which is the
* usual failure when two class names are kept in sync by hand.
* - **The editor degrades to a plain textarea.** Without JavaScript the
* editing region is still a textarea: content is visible, editable, and
* keyboard-accessible.
* - **Document styles are separate from editor chrome.** `.fc-prose` styles
* what a user produces; `.fc-editor*` styles the editing surface. They work
* independently -- a document rendered from the model uses `.fc-prose` with
* no editor at all.
*/
@layer fc.components {
/* ---- Surface ----------------------------------------------------------- */
.fc-editor {
display: flex;
flex-direction: column;
min-inline-size: 0;
background-color: var(--fc-color-bg-surface);
color: var(--fc-color-text-default);
border: var(--fc-border-width-thin) solid var(--fc-color-border-default);
border-radius: var(--fc-radius-md);
}
/*
* Focus-within lifts the whole surface, signalling "you are editing here"
* without competing with the caret's own focus ring.
*/
.fc-editor:focus-within {
border-color: var(--fc-color-accent-border);
box-shadow: 0 0 0 var(--fc-space-0-5) var(--fc-color-focus-ring-offset);
}
/* The toolbar is `fc-toolbar` (toolbar.css), the general component; the
editor's sits inside its border. */
/* ---- Editing region ---------------------------------------------------- */
/*
* `min-block-size` rather than a fixed height: a fixed-height editor traps the
* user in a small box and forces a nested scroll region, which is a well-known
* keyboard trap.
*/
.fc-editor__content {
padding: var(--fc-space-3);
min-block-size: 8lh;
/* Grows with content up to this point, then scrolls internally. */
max-block-size: 60vh;
overflow-y: auto;
/* Room for the focus ring inside the scroll container. */
padding-inline: var(--fc-space-4);
color: var(--fc-color-text-default);
font-family: inherit;
font-size: var(--fc-font-size-md);
line-height: var(--fc-line-height-normal);
}
/*
* Placeholder styling. `data-placeholder` on the region rather than a
* `content:` string, so the real hint text lives in the markup.
*/
.fc-editor__content:empty::before {
content: attr(data-placeholder);
color: var(--fc-color-text-subtle);
/* A placeholder is a hint, not a label, so it must not intercept clicks. */
pointer-events: none;
}
/*
* A read-only editor is a preview. It must not look editable, because the
* absence of a caret would otherwise be the only signal.
*/
.fc-editor__content[readonly],
.fc-editor__content[aria-readonly="true"] {
background-color: var(--fc-color-bg-subtle);
}
/* ---- Document styles --------------------------------------------------- */
/*
* .fc-prose -- styles for editor content.
*
* Usable standalone for rendering a document the model produced, with no editor
* present. That is why it shares the file with the editor rather than being
* duplicated: one definition of "what bold looks like" for both the editing and
* the reading case.
*/
.fc-prose {
color: var(--fc-color-text-default);
line-height: var(--fc-line-height-normal);
}
/* Spacing between blocks comes from the children, not wrapper margins. */
.fc-prose > * + * {
margin-block-start: var(--fc-space-3);
}
.fc-prose :is(h1, h2, h3, h4, h5, h6) {
font-weight: var(--fc-font-weight-semibold);
line-height: var(--fc-line-height-tight);
margin-block: var(--fc-space-4) var(--fc-space-2);
text-wrap: balance;
}
.fc-prose h1 { font-size: var(--fc-font-size-3xl); }
.fc-prose h2 { font-size: var(--fc-font-size-2xl); }
.fc-prose h3 { font-size: var(--fc-font-size-xl); }
/*
* Inline marks: the *rendering* of a mark. The editor applies them to the
* document; this only decides how they look.
*/
.fc-prose :is(strong, b) {
font-weight: var(--fc-font-weight-bold);
}
.fc-prose :is(em, i) {
font-style: italic;
}
/*
* Underline gets its own offset so it stays legible over a link's own
* underline rather than merging into a single line.
*/
.fc-prose u,
.fc-prose ins {
text-decoration: underline;
text-underline-offset: 0.2em;
}
.fc-prose s,
.fc-prose del {
text-decoration: line-through;
}
/* Inline code, distinguished from a code *block* by size and padding. */
.fc-prose :is(code):not(pre code) {
font-family: var(--fc-font-family-mono);
font-size: 0.9375em;
padding-inline: 0.25em;
padding-block: 0.125em;
background-color: var(--fc-color-bg-subtle);
border-radius: var(--fc-radius-sm);
}
/*
* Code blocks.
*
* `overflow-x: auto` rather than wrapping: wrapping code destroys the
* indentation that carries meaning in most languages. A horizontal scrollbar
* inside a container is a real cost, but a mangled program is worse.
*/
.fc-prose pre {
font-family: var(--fc-font-family-mono);
font-size: var(--fc-font-size-sm);
line-height: var(--fc-line-height-normal);
padding: var(--fc-space-3);
overflow-x: auto;
background-color: var(--fc-color-bg-sunken);
border: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
border-radius: var(--fc-radius-sm);
/* 2 is the near-universal convention, and mismatched indentation in a
listing is disorienting. */
tab-size: 2;
}
.fc-prose blockquote {
margin-inline: 0;
padding-inline-start: var(--fc-space-3);
border-inline-start: var(--fc-border-width-thick) solid var(--fc-color-border-default);
color: var(--fc-color-text-muted);
}
/*
* The reset zeroes list padding via :where(); restore it inside prose so a
* nested list still reads as nested.
*/
.fc-prose ul,
.fc-prose ol {
padding-inline-start: var(--fc-space-5);
}
.fc-prose li + li {
margin-block-start: var(--fc-space-1);
}
.fc-prose :is(a) {
color: var(--fc-color-accent-text);
text-decoration: underline;
text-underline-offset: 0.2em;
}
.fc-prose hr {
border: 0;
border-block-start: var(--fc-border-width-thin) solid var(--fc-color-border-default);
margin-block: var(--fc-space-4);
}
/* ---- State -------------------------------------------------------------- */
/*
* A failed save must not discard what the user wrote, so the content stays in
* place and only the border changes.
*/
.fc-editor[data-fc-state="error"] {
border-color: var(--fc-color-danger-border);
}
.fc-editor[data-fc-state="saving"] .fc-editor__content {
/* Kept subtle: dimming far enough to notice would take the text below the
contrast minimum, and the state is announced separately anyway. */
opacity: 0.9;
}
/*
* Forced colors.
*
* The toolbar's own forced-colors block lives in toolbar.css.
*/
@media (forced-colors: active) {
.fc-prose pre {
background-color: Canvas;
border-color: CanvasText;
}
}
}
/*!
* future-css -- context menu
*
* A popup menu for right-click and for application-triggered actions.
*
* **Positioning is JavaScript's job, by design.** The Rust/WASM layer owns the
* item model and keyboard traversal, but where the menu opens and whether it
* flips to stay on screen depend on layout the browser alone knows: scroll
* position, ancestor clipping, the visual viewport on mobile. Computing that in
* WASM would mean maintaining a second, always-stale copy of the browser's
* geometry. See docs/WASM.md.
*
* That split is why this file contains no offsets beyond a fixed anchor, and why
* the markup uses the Popover API where available.
*/
@layer fc.components {
/*
* `position: fixed` because a context menu is anchored to the viewport, not to
* any scrolling ancestor; an absolutely-positioned menu inside a scroll
* container scrolls away from its trigger.
*/
.fc-menu {
position: fixed;
z-index: var(--fc-z-dropdown);
display: flex;
flex-direction: column;
/* A long menu scrolls rather than growing past the viewport. The positioning
script narrows this to the space actually available. */
max-block-size: min(60vh, 24rem);
overflow-y: auto;
overscroll-behavior-block: contain;
min-inline-size: 12rem;
padding-block: var(--fc-space-1);
padding-inline: var(--fc-space-1);
background-color: var(--fc-color-bg-overlay);
color: var(--fc-color-text-default);
border: var(--fc-border-width-thin) solid var(--fc-color-border-default);
border-radius: var(--fc-radius-md);
/* The elevation separates the menu from the page beneath it; a border alone
is not enough on a light background. */
box-shadow: var(--fc-shadow-overlay);
font-size: var(--fc-font-size-sm);
line-height: var(--fc-line-height-snug);
}
/*
* Open and closed.
*
* The Popover API handles this via `[popover]` and `:popover-open`. Where it is
* unsupported the fallback is a `data-fc-open` attribute the script toggles --
* so a closed menu is never a flash of unpositioned content.
*/
.fc-menu:not(:popover-open):not([data-fc-open="true"]) {
display: none;
}
/* ---- Items -------------------------------------------------------------- */
/*
* Menu items are `<button>`s, not `<div>`s. A non-button item would need
* `role="menuitem"`, `tabindex`, and Enter/Space handling by hand -- and would
* still miss form participation and correct forced-colors rendering.
*/
.fc-menu__item {
display: flex;
align-items: center;
gap: var(--fc-space-2);
inline-size: 100%;
/* Compact by convention, but the hit area stays at the WCAG 24px minimum;
the visual padding is what shrinks, not the target. */
min-block-size: var(--fc-size-target-min);
padding-block: var(--fc-space-1-5);
padding-inline: var(--fc-space-2);
background-color: transparent;
color: var(--fc-color-text-default);
border: 0;
border-radius: var(--fc-radius-sm);
font-family: inherit;
font-size: inherit;
text-align: start;
cursor: pointer;
transition: background-color var(--fc-duration-fast) var(--fc-easing-standard);
}
.fc-menu__item:hover:not(:disabled) {
background-color: var(--fc-color-accent-bg-subtle);
color: var(--fc-color-accent-text);
}
/*
* Disabled items are skipped by arrow navigation (that rule lives in the Rust
* model), so they never receive focus. Dimmed text plus a not-allowed cursor is
* the whole treatment -- a disabled item must not look interactive.
*/
.fc-menu__item:disabled,
.fc-menu__item[aria-disabled="true"] {
color: var(--fc-color-disabled-fg);
cursor: not-allowed;
}
/* Keyboard focus, using the shared ring from the base layer. */
.fc-menu__item:focus-visible {
outline: var(--fc-border-width-focus) solid var(--fc-color-focus-ring);
/* Pulled inward so the ring does not sit outside the menu's own edge and get
clipped by its overflow. */
outline-offset: calc(-1 * var(--fc-border-width-focus));
}
/* ---- Item parts --------------------------------------------------------- */
/* The label takes the remaining space so shortcuts align to the inline end. */
.fc-menu__label {
flex: 1;
min-inline-size: 0;
}
/*
* Shortcut text, e.g. "Cmd+S". Marked `aria-hidden` in the markup: it is
* useful to a sighted user and redundant to a screen-reader user, who is
* already inside a menu and would otherwise hear the key sequence twice.
*/
.fc-menu__shortcut {
color: var(--fc-color-text-muted);
font-size: var(--fc-font-size-xs);
/* Tabular so a column of shortcuts lines up rather than jittering. */
font-variant-numeric: tabular-nums;
margin-inline-start: var(--fc-space-3);
}
/*
* The checkmark column for checkbox items. Reserved width so labels stay
* aligned whether or not anything is checked -- without it, checking one item
* would shift every label in the menu.
*/
.fc-menu__check {
inline-size: 1em;
flex-shrink: 0;
text-align: center;
visibility: hidden;
}
.fc-menu__item[aria-checked="true"] .fc-menu__check {
visibility: visible;
}
/*
* Submenu affordance. The chevron points in the *reading* direction, so it
* needs an explicit flip -- a transform has no logical equivalent. One of the
* few places the system requires a direction branch.
*/
.fc-menu__submenu-arrow {
margin-inline-start: var(--fc-space-3);
flex-shrink: 0;
opacity: 0.7;
}
:dir(rtl) .fc-menu__submenu-arrow {
transform: scaleX(-1);
}
/* ---- Groups ------------------------------------------------------------- */
/*
* A separator.
*
* `role="separator"` on a real element carries the grouping cue that tells a
* screen-reader user why unrelated items suddenly follow. An `aria-hidden`
* divider would be skipped, losing that cue.
*/
.fc-menu__separator {
block-size: 0;
margin-block: var(--fc-space-1);
border: 0;
border-block-start: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
}
/* A labelled group, e.g. "Text style". */
.fc-menu__group-label {
padding-block: var(--fc-space-1);
padding-inline: var(--fc-space-2);
color: var(--fc-color-text-muted);
font-size: var(--fc-font-size-2xs);
font-weight: var(--fc-font-weight-semibold);
/* Not letter-spaced: the string is already short and tracking hurts
legibility at this size. */
letter-spacing: normal;
}
/* ---- State -------------------------------------------------------------- */
/*
* Destructive items, marked by a leading rule as well as colour so the intent
* survives greyscale and forced colors. A delete that is only red is a delete a
* colour-blind user cannot identify.
*/
.fc-menu__item--danger {
color: var(--fc-color-danger-text);
border-inline-start: var(--fc-border-width-thick) solid var(--fc-color-danger-border);
/* Inset by the rule width so the label lines up with the non-danger items. */
padding-inline-start: calc(var(--fc-space-2) - var(--fc-border-width-thick));
}
.fc-menu__item--danger:hover:not(:disabled) {
background-color: var(--fc-color-danger-bg-subtle);
color: var(--fc-color-danger-text);
}
@media (forced-colors: active) {
.fc-menu {
background-color: Canvas;
border-color: CanvasText;
/* box-shadow is suppressed in forced colors, so the edge is carried by the
border alone. */
}
.fc-menu__item:hover:not(:disabled) {
background-color: Highlight;
color: HighlightText;
}
.fc-menu__item--danger {
color: LinkText;
border-inline-start-color: LinkText;
}
.fc-menu__separator {
border-block-start-color: CanvasText;
}
}
}
/*!
* future-css -- data grid
*
* A spreadsheet-like surface: a scrollable body, sticky headers, and cells that
* follow the shared table semantics from the base layer.
*
* Scope, stated plainly: this is the *UI element* -- chrome, alignment, sticky
* headers, selection, focus. It is not a spreadsheet application. No formulas, no
* collaborative editing, no attempt to be an Office clone inside a component
* library. The cell and range model a real implementation needs already exists
* in `crates/fc-grid`; this file is its presentation.
*
* The accessibility problem here is real: a grid with `aria-rowcount="10000"`
* renders only ~40 rows. A screen reader must be told the true dimensions even
* though the DOM holds a window, or the content appears to end at row 40. That
* ARIA is not optional decoration.
*/
@layer fc.components {
.fc-grid {
display: grid;
/* `minmax(0, 1fr)` rather than `1fr`, so a wide cell shrinks instead of
forcing horizontal overflow on the container. */
grid-auto-columns: minmax(0, 1fr);
grid-auto-rows: min-block-size;
/* No inline-size: the consumer's template decides, and a default would fight
it. */
overflow: auto;
/* Stops a scroll chaining to the page, which is disorienting inside a
full-viewport app shell. */
overscroll-behavior: contain;
overscroll-behavior-inline: contain;
background-color: var(--fc-color-bg-surface);
color: var(--fc-color-text-default);
border: var(--fc-border-width-thin) solid var(--fc-color-border-default);
border-radius: var(--fc-radius-md);
font-size: var(--fc-font-size-sm);
line-height: var(--fc-line-height-snug);
}
/* ---- Headers ------------------------------------------------------------ */
/*
* Column and row headers are visually chrome but semantically name the data,
* which is why they carry a real scope rather than being presentation-only.
*/
.fc-grid__col-header,
.fc-grid__row-header {
position: sticky;
z-index: var(--fc-z-raised);
display: flex;
align-items: center;
padding-inline: var(--fc-space-2);
padding-block: var(--fc-space-1-5);
background-color: var(--fc-color-bg-subtle);
color: var(--fc-color-text-muted);
font-weight: var(--fc-font-weight-semibold);
/* A header that wraps to two lines misaligns the row beneath it. */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* The column header row sticks to the block-start edge while scrolling. */
.fc-grid__col-header {
inset-block-start: 0;
}
/* The row header column sticks to the inline-start edge. */
.fc-grid__row-header {
inset-inline-start: 0;
justify-content: flex-end;
/* Digits align on the decimal. */
font-variant-numeric: tabular-nums;
}
/*
* The corner cell, where both sticky edges meet. It needs a higher z-index than
* either single-axis header, or the column header scrolls over it while the row
* header vanishes beneath the column.
*/
.fc-grid__corner {
position: sticky;
inset-block-start: 0;
inset-inline-start: 0;
z-index: var(--fc-z-sticky);
background-color: var(--fc-color-bg-muted);
}
/* ---- Cells -------------------------------------------------------------- */
/*
* Rows are transparent to the grid.
*
* `.fc-grid` declares `grid-auto-columns` so that the *cells* are the grid
* items, which is what makes a spreadsheet-like surface possible: every cell
* in column N lines up with every other cell in column N, no matter which row
* it is in.
*
* That only works if a row contributes no box of its own. A row is still a
* `<div role="row">`, which defaults to `display: block` -- so it becomes a
* single grid item, each row lands in one implicit column, and the cells
* inside it lay out as ordinary block boxes. The result is a grid that looks
* roughly right but whose columns do not line up across rows.
*
* `display: contents` removes the row's box while keeping it in the
* accessibility tree, which is exactly the behaviour needed: the row is still
* announced as a row grouping its cells, but contributes no layout.
*
* This is worth stating explicitly rather than leaving implicit, because the
* failure is invisible in a screenshot of a single row and obvious only when
* you look down a column.
*
* Scoped to the grid's own rows because other components use `role="row"`
* too: `.fc-git__row` is a flex row at the same specificity, so an unscoped
* rule won or lost purely on stylesheet order. Loaded per component, the git
* rows lost their box, and `focus()` on a box-less element silently fails.
* Child selectors rather than a descendant one: rows may sit inside a
* `role="rowgroup"` that the consumer also makes `display: contents`, but a
* git surface placed in a cell has rows of its own that must keep their box.
*/
.fc-grid > [role="row"],
.fc-grid > [role="rowgroup"] > [role="row"] {
display: contents;
}
.fc-grid__cell {
display: flex;
align-items: center;
padding-inline: var(--fc-space-2);
padding-block: var(--fc-space-1);
/* Clips rather than overflowing: a long value must not widen the column. */
min-inline-size: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
border-block-end: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
border-inline-end: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
}
/*
* The editing cell.
*
* `min-block-size` comes from the grid track rather than a fixed height, so a
* wrapped value grows the row instead of being clipped mid-character.
*/
.fc-grid__cell--editing {
/* Replaces the border that would otherwise show through the input. */
box-shadow: inset 0 0 0 var(--fc-border-width-thick) var(--fc-color-accent-border);
overflow: visible;
white-space: normal;
}
/* The input inside an editing cell. Sized to the cell, not the other way. */
.fc-grid__input {
inline-size: 100%;
border: 0;
padding: 0;
margin: 0;
background-color: transparent;
color: inherit;
font: inherit;
/*
* No `outline: none` here.
*
* The base layer already gives inputs a focus ring through :where(), which is
* zero-specificity -- but this rule sits in the components layer, so an
* `outline: none` would outrank it and leave the cell with NO visible focus
* indicator at all. Suppressing the ring is only safe when something
* provably replaces it, and here the ring is already correct.
*/
min-inline-size: 0;
}
/* ---- Selection ---------------------------------------------------------- */
/*
* A tinted fill plus an inset outline. The outline matters: at low alpha a fill
* alone is nearly invisible against a busy sheet, and selection is the single
* most important thing to perceive while editing.
*/
.fc-grid__cell[aria-selected="true"] {
background-color: var(--fc-color-selected-bg);
color: var(--fc-color-selected-fg);
box-shadow: inset 0 0 0 var(--fc-border-width-thick) var(--fc-color-accent-border);
}
/*
* The active cell within a selection.
*
* Distinct from the rest of the selection because it is where typing goes. Without
* the distinction, a user cannot tell which cell they are about to overwrite.
*/
.fc-grid__cell[data-fc-active="true"] {
background-color: var(--fc-color-bg-surface);
color: var(--fc-color-text-default);
box-shadow: inset 0 0 0 var(--fc-border-width-focus) var(--fc-color-accent-bg);
}
/* ---- Alignment ---------------------------------------------------------- */
/*
* Numeric alignment: `end` rather than `right`, so it mirrors in RTL, with
* tabular figures. A column of numbers that does not line up on the decimal
* cannot be scanned, which is most of the value of looking at one.
*/
.fc-grid__cell--numeric,
.fc-grid__col-header--numeric {
justify-content: flex-end;
text-align: end;
font-variant-numeric: tabular-nums;
}
/* ---- State -------------------------------------------------------------- */
/*
* Dirty cells, marked per the dual-audience contract.
*
* A corner tick rather than a fill, so it composes with selection and validation
* instead of competing with them for the same visual channel.
*/
.fc-grid__cell[data-fc-dirty="true"] {
position: relative;
}
.fc-grid__cell[data-fc-dirty="true"]::after {
content: "";
position: absolute;
inset-block-start: 0;
inset-inline-end: 0;
/* A triangle from two borders, in the top-inline corner. */
inline-size: 0;
block-size: 0;
border-block-start: var(--fc-space-1-5) solid var(--fc-color-accent-border);
border-inline-end: var(--fc-space-1-5) solid transparent;
}
/* Validation state, paired with `aria-invalid` in the markup. */
.fc-grid__cell[aria-invalid="true"] {
box-shadow: inset 0 0 0 var(--fc-border-width-thick) var(--fc-color-danger-border);
}
/* Read-only cells are visibly inert. */
.fc-grid__cell[aria-readonly="true"] {
background-color: var(--fc-color-bg-subtle);
color: var(--fc-color-text-muted);
}
@media (forced-colors: active) {
.fc-grid {
background-color: Canvas;
border-color: CanvasText;
}
.fc-grid__cell {
border-block-end-color: CanvasText;
border-inline-end-color: CanvasText;
}
.fc-grid__col-header,
.fc-grid__row-header,
.fc-grid__corner {
background-color: Canvas;
color: CanvasText;
}
.fc-grid__cell[aria-selected="true"] {
/* Selection must survive, since a fill is discarded here. */
box-shadow: inset 0 0 0 var(--fc-border-width-focus) Highlight;
}
}
}
/*!
* future-css -- embedded browser chrome
*
* The *chrome* around an embedded browsing context: a tab strip, an address bar,
* navigation controls, and the frame hosting the content.
*
* **Scope, stated plainly.** The browsing itself is an `<iframe>`. This is not a
* browser engine, and building one inside a component library is not a sensible
* use of effort -- the platform already provides the browsing context, its
* process isolation, and its network stack. What a design system can usefully
* provide is the accessible, themeable, direction-correct UI around it, plus the
* security affordances that make embedding safe to offer.
*/
@layer fc.components {
/* ---- Frame -------------------------------------------------------------- */
/*
* `min-block-size: 0` on the frame is what actually lets it shrink; without it
* a flex child refuses to shrink below its content and the layout overflows.
*/
.fc-webview {
display: flex;
flex-direction: column;
min-inline-size: 0;
/* A definite height is required for a flex column with a growing child; the
consumer sets it. */
block-size: 100%;
overflow: hidden;
background-color: var(--fc-color-bg-surface);
color: var(--fc-color-text-default);
border: var(--fc-border-width-thin) solid var(--fc-color-border-default);
border-radius: var(--fc-radius-md);
}
.fc-webview__frame {
flex: 1;
/* The single most important line in this file. */
min-block-size: 0;
inline-size: 100%;
/* The iframe has its own default border, which would double up here. */
border: 0;
background-color: var(--fc-color-bg-canvas);
}
/* ---- Tab strip ---------------------------------------------------------- */
.fc-webview__tabs {
display: flex;
align-items: stretch;
gap: var(--fc-space-1);
padding: var(--fc-space-1);
background-color: var(--fc-color-bg-subtle);
border-block-end: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
overflow-x: auto;
/* So a dragged tab settles rather than stopping arbitrarily. */
scroll-snap-type: inline proximity;
}
/*
* A `<button role="tab">`, per the ARIA tabs pattern. `aria-selected` drives the
* active styling so the visible tab and the announced tab cannot disagree.
*/
.fc-webview__tab {
display: inline-flex;
align-items: center;
gap: var(--fc-space-2);
min-inline-size: 0;
max-inline-size: 14rem;
padding-inline: var(--fc-space-2);
padding-block: var(--fc-space-1-5);
scroll-snap-align: start;
background-color: transparent;
color: var(--fc-color-text-muted);
border: var(--fc-border-width-thin) solid transparent;
border-radius: var(--fc-radius-sm);
font-family: inherit;
font-size: var(--fc-font-size-sm);
cursor: pointer;
}
.fc-webview__tab:hover {
background-color: var(--fc-color-bg-muted);
color: var(--fc-color-text-default);
}
.fc-webview__tab[aria-selected="true"] {
background-color: var(--fc-color-bg-surface);
color: var(--fc-color-text-default);
border-color: var(--fc-color-border-default);
font-weight: var(--fc-font-weight-semibold);
}
.fc-webview__tab-label {
min-inline-size: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* ---- Toolbar ------------------------------------------------------------ */
.fc-webview__toolbar {
display: flex;
align-items: center;
gap: var(--fc-space-1);
padding: var(--fc-space-1) var(--fc-space-2);
background-color: var(--fc-color-bg-surface);
border-block-end: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
min-inline-size: 0;
}
.fc-webview__nav {
display: inline-flex;
align-items: center;
justify-content: center;
inline-size: var(--fc-size-target-min);
block-size: var(--fc-size-target-min);
flex-shrink: 0;
background-color: transparent;
color: var(--fc-color-text-default);
border: var(--fc-border-width-thin) solid transparent;
border-radius: var(--fc-radius-sm);
cursor: pointer;
}
.fc-webview__nav:hover:not(:disabled) {
background-color: var(--fc-color-bg-muted);
}
/*
* There is no history to go back to at the start of a session, and the button
* must be genuinely disabled rather than inert-looking: one that looks live
* invites the user to keep pressing it.
*/
.fc-webview__nav:disabled {
color: var(--fc-color-disabled-fg);
cursor: not-allowed;
}
/*
* Direction-sensitive glyphs.
*
* A back arrow is one of the few genuinely physical affordances in a UI: it
* points the way the browser will travel, which is *backwards* in reading order.
* So it flips with direction, while a reload arrow -- which is rotational -- must
* not.
*/
:dir(rtl) .fc-webview__nav--back {
transform: scaleX(-1);
}
/*
* The address bar. Grows to fill and shrinks rather than forcing the toolbar to
* overflow -- the address is usually the longest string in the bar.
*/
.fc-webview__address {
flex: 1;
min-inline-size: 0;
display: flex;
align-items: center;
gap: var(--fc-space-2);
padding-inline: var(--fc-space-2);
block-size: 2rem;
background-color: var(--fc-color-bg-subtle);
border: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
border-radius: var(--fc-radius-full);
}
.fc-webview__address-input {
flex: 1;
min-inline-size: 0;
background: transparent;
border: 0;
color: var(--fc-color-text-default);
/* Monospace: a URL is easier to read and verify when the characters are
aligned, and a homoglyph in a hostname is a real phishing vector. */
font-family: var(--fc-font-family-mono);
font-size: var(--fc-font-size-xs);
/*
* No `outline: none`. The address field is a real input, and the base layer's
* focus ring is exactly right here; removing it (from a later layer, which
* outranks base) would leave the toolbar's most important control with no
* visible keyboard focus.
*/
}
/*
* The secure/not-secure indicator.
*
* A glyph plus text, not colour alone. The indicator exists to convey trust to
* someone who cannot rely on the colour, and a green padlock conveys nothing to a
* screen-reader user.
*/
.fc-webview__secure {
display: inline-flex;
align-items: center;
gap: var(--fc-space-1);
flex-shrink: 0;
font-size: var(--fc-font-size-2xs);
color: var(--fc-color-text-muted);
}
/*
* An insecure connection uses the warning family and changes the glyph, so it is
* distinguishable by shape and by text rather than by hue.
*/
.fc-webview__secure[data-fc-state="insecure"] {
color: var(--fc-color-warning-text);
}
/* ---- States ------------------------------------------------------------- */
/*
* Loading and error states overlay the frame.
*
* A sibling of the iframe, not a child: an element positioned over the frame
* would not cover a cross-origin document, and content inside the frame cannot
* be reliably overlaid from the parent.
*/
.fc-webview__overlay {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--fc-space-2);
padding: var(--fc-space-4);
text-align: center;
background-color: var(--fc-color-bg-canvas);
color: var(--fc-color-text-muted);
}
.fc-webview__overlay[hidden] {
display: none;
}
.fc-webview__error {
color: var(--fc-color-danger-text);
background-color: var(--fc-color-danger-bg-subtle);
border: var(--fc-border-width-thin) solid var(--fc-color-danger-border);
border-radius: var(--fc-radius-md);
padding: var(--fc-space-3);
max-inline-size: 28rem;
}
@media (forced-colors: active) {
.fc-webview {
background-color: Canvas;
border-color: CanvasText;
}
.fc-webview__tabs,
.fc-webview__toolbar {
background-color: Canvas;
border-block-end-color: CanvasText;
}
.fc-webview__tab[aria-selected="true"] {
background-color: Canvas;
color: CanvasText;
border-color: CanvasText;
}
.fc-webview__address {
background-color: Canvas;
border-color: CanvasText;
}
}
}
/*!
* future-css -- terminal
*
* A terminal emulator surface: the output grid, the input line, and the chrome
* around them.
*
* **Scope, stated plainly.** This is the presentation layer. Parsing escape
* sequences, managing a PTY, scrollback buffers, and multiplexing belong to a
* terminal engine (xterm.js and equivalents do this well and should be used).
* What a design system owns is everything visible: grid geometry, the ANSI
* palette, cursor states, selection, and the accessibility contract.
*
* # Why a terminal has its own palette
*
* Two reasons, both structural rather than stylistic:
*
* 1. **A terminal has its own background**, which is dark in *both* color
* schemes. An application that follows the OS into a light theme must not
* produce a light terminal -- every terminal theme ever written keeps it
* dark, and a light one is a genuine usability regression.
* 2. **ANSI slots are hue-fixed by protocol.** `\e[31m` is red. A build log's
* red error means nothing rendered blue, so these are the only tokens in the
* system named by hue rather than by purpose.
*
* Both are enforced: `src/tokens/semantic/terminal.ts` declares 19 contrast
* pairings checked at build time in both schemes. The first hand-written version
* of that palette failed 16 of them.
*
* # Accessibility
*
* Terminals are the hardest surface to make accessible, and most of the work is
* not visual:
*
* - **The output region is a `log`**, so additions are announced politely
* rather than interrupting. Deliberately not `assertive`: a fast-running
* command would make a screen reader unusable.
* - **The input line is a real textbox**, not a `div`, so caret and IME
* composition work. Without this, an IME-composed character -- common in CJK
* and other composing scripts -- silently fails.
* - **Color is never the only signal.** A failed command carries its exit
* status as text, so a red line is not the only thing marking it.
*/
@layer fc.components {
.fc-terminal {
display: flex;
flex-direction: column;
min-inline-size: 0;
/* A terminal needs a definite height; the consumer sets it. */
block-size: 100%;
background-color: var(--fc-term-bg);
color: var(--fc-term-fg);
border: var(--fc-border-width-thin) solid var(--fc-color-border-default);
border-radius: var(--fc-radius-md);
/* The terminal background is its own contrast environment, so it is never
themed -- not even by high-contrast or reduced-transparency preferences. */
}
/*
* The output grid.
*
* `min-block-size: 0` so it scrolls while the input line stays pinned -- the
* same flex-shrink requirement as the webview frame, for the same reason.
*/
.fc-terminal__output {
flex: 1;
min-block-size: 0;
overflow-y: auto;
overscroll-behavior-block: contain;
padding: var(--fc-space-2) var(--fc-space-3);
/*
* Monospace is a functional requirement, not a style choice: a terminal
* whose glyphs are proportional columns is unreadable, because column
* alignment is how output is structured.
*/
font-family: var(--fc-font-family-mono);
font-size: var(--fc-font-size-sm);
/* Terminal leading is conventionally ~1.2; more wastes vertical space, less
collides descenders. */
line-height: var(--fc-line-height-snug);
/* Programming ligatures turn `!=` into one glyph and break column counting. */
font-variant-ligatures: none;
/*
* Deliberately NOT `white-space: pre` here.
*
* That looks correct and is not. With `pre` on the container, every newline
* and indent in the markup *between* line elements renders as a blank output
* line -- so a 10-line document occupied 598px instead of 180px, and the
* output scrolled when it had no reason to. Pre belongs on the line, where
* the whitespace is meaningful, and nowhere else.
*/
tab-size: 4;
}
/*
* A line of output.
*
* Block, not inline: an inline element collapses trailing spaces, which carry
* meaning in aligned output.
*/
.fc-terminal__line {
display: block;
/*
* `pre` lives here rather than on the container, so leading spaces and
* column alignment are preserved within a line while the whitespace between
* lines in the markup collapses to nothing.
*/
white-space: pre;
min-block-size: 1lh;
}
/* ---- ANSI colors ------------------------------------------------------- */
/*
* One class per SGR slot. There is deliberately no `.fc-terminal__text--danger`
* here, and that absence is the point: a terminal's colors are chosen by the
* *program's output*, not by the application's intent. The class mirrors the
* escape sequence because that is what the content actually says.
*/
.fc-term-ansi-black { color: var(--fc-term-ansi-black); }
.fc-term-ansi-red { color: var(--fc-term-ansi-red); }
.fc-term-ansi-green { color: var(--fc-term-ansi-green); }
.fc-term-ansi-yellow { color: var(--fc-term-ansi-yellow); }
.fc-term-ansi-blue { color: var(--fc-term-ansi-blue); }
.fc-term-ansi-magenta { color: var(--fc-term-ansi-magenta); }
.fc-term-ansi-cyan { color: var(--fc-term-ansi-cyan); }
.fc-term-ansi-white { color: var(--fc-term-ansi-white); }
.fc-term-ansi-bright-black { color: var(--fc-term-ansi-bright-black); }
.fc-term-ansi-bright-red { color: var(--fc-term-ansi-bright-red); }
.fc-term-ansi-bright-green { color: var(--fc-term-ansi-bright-green); }
.fc-term-ansi-bright-yellow { color: var(--fc-term-ansi-bright-yellow); }
.fc-term-ansi-bright-blue { color: var(--fc-term-ansi-bright-blue); }
.fc-term-ansi-bright-magenta { color: var(--fc-term-ansi-bright-magenta); }
.fc-term-ansi-bright-cyan { color: var(--fc-term-ansi-bright-cyan); }
.fc-term-ansi-bright-white { color: var(--fc-term-ansi-bright-white); }
/* ---- Text attributes --------------------------------------------------- */
/*
* Bold, dim, italic, underline, inverse.
*
* Dim is a real ANSI attribute and the one that most often breaks the contrast
* contract -- which is why `--fc-term-fg-dim` is a defined, verified token at
* 4.5:1 rather than a plain opacity on the default foreground. An opacity would
* compound with whatever color the line already carries.
*/
.fc-terminal__bold { font-weight: var(--fc-font-weight-bold); }
.fc-terminal__dim { color: var(--fc-term-fg-dim); }
.fc-terminal__italic { font-style: italic; }
.fc-terminal__underline { text-decoration: underline; }
.fc-terminal__strike { text-decoration: line-through; }
/*
* Inverse swaps foreground and background for the span.
*
* Both colors are explicit: relying on `color: inherit` with a background
* would resolve against the wrong side inside a nested inverse.
*/
.fc-terminal__inverse {
color: var(--fc-term-bg);
background-color: var(--fc-term-fg);
}
/* ---- Input line -------------------------------------------------------- */
/* Flex so the prompt cannot wrap away from the caret. */
.fc-terminal__input-row {
display: flex;
align-items: baseline;
gap: var(--fc-space-2);
padding: var(--fc-space-2) var(--fc-space-3);
border-block-start: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
min-inline-size: 0;
}
/* `flex-shrink: 0` so a long prompt truncates rather than crushing the input. */
.fc-terminal__prompt {
flex-shrink: 0;
color: var(--fc-term-ansi-green);
white-space: pre;
user-select: none;
}
/*
* A real `<input>`, so caret movement, selection, clipboard, undo, and IME
* composition all work natively. Terminals that fake this with a contenteditable
* div break IME input for every CJK user.
*/
.fc-terminal__input {
flex: 1;
min-inline-size: 0;
background: transparent;
border: 0;
padding: 0;
color: var(--fc-term-fg);
/* Inherits monospace from the parent so the caret column lines up with the
output above it. */
font: inherit;
}
/* ---- Cursor ------------------------------------------------------------ */
/*
* The block cursor.
*
* A real element rather than a pseudo-element on the input, because a cursor
* must be selectable and must move with the caret as the user types -- neither
* of which a pseudo-element can do.
*/
.fc-terminal__cursor {
display: inline-block;
inline-size: 0.6em;
block-size: 1em;
background-color: var(--fc-term-cursor);
color: var(--fc-term-bg);
/* Sits on the text baseline without adding line height. */
vertical-align: text-bottom;
}
/*
* The blinking variant.
*
* The blink is the one animation here and it carries meaning -- it is how the
* user knows the shell is waiting. Under reduced motion it becomes a static
* block rather than nothing, keeping the signal without the motion.
*/
.fc-terminal__cursor--blink {
animation: fc-cursor-blink 1.06s step-end infinite;
}
@keyframes fc-cursor-blink {
50% {
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.fc-terminal__cursor--blink {
animation: none;
}
}
/* ---- Status bar -------------------------------------------------------- */
/*
* Describes the terminal rather than the application, so it follows the terminal
* background rather than the app chrome.
*/
.fc-terminal__status {
display: flex;
align-items: center;
gap: var(--fc-space-3);
padding-block: var(--fc-space-1);
padding-inline: var(--fc-space-3);
background-color: var(--fc-term-bg-raised);
color: var(--fc-term-fg-dim);
border-block-start: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
font-family: var(--fc-font-family-mono);
font-size: var(--fc-font-size-2xs);
}
/* Pushes the last item to the inline end -- an exit code, typically. */
.fc-terminal__status-end {
margin-inline-start: auto;
}
/*
* Exit status, colored AND prefixed with a word.
*
* A red "127" alone is a color-only signal; "exit 127" survives greyscale and
* forced colors.
*/
.fc-terminal__exit {
font-variant-numeric: tabular-nums;
}
.fc-terminal__exit[data-fc-state="error"] {
color: var(--fc-term-ansi-red);
}
.fc-terminal__exit[data-fc-state="complete"] {
color: var(--fc-term-ansi-green);
}
/* ---- Selection --------------------------------------------------------- */
/*
* Scoped to the output rather than global: selecting terminal output deserves
* its own treatment and should not inherit the app's selection color.
*/
.fc-terminal__output::selection {
background-color: var(--fc-term-ansi-blue);
color: var(--fc-term-bg);
}
/* ---- State ------------------------------------------------------------- */
/* A dimmed input row rather than a color change: the output may be frozen or
erroring, and a user needs to know the session is over, not merely idle. */
.fc-terminal[data-fc-state="disconnected"] .fc-terminal__input-row {
opacity: 0.6;
}
@media (forced-colors: active) {
.fc-terminal {
/* The terminal background is forced to Canvas, so the text must be forced
to CanvasText or the output becomes unreadable. Here honoring forced
colors is a matter of legibility, not preference. */
background-color: Canvas;
color: CanvasText;
border-color: CanvasText;
}
/* Success and failure collapse to one color: in forced-colors mode there is
no hue to distinguish them, which is why the visible label carries the
state as text as well. */
.fc-term-ansi-red,
.fc-term-ansi-bright-red,
.fc-term-ansi-green,
.fc-term-ansi-bright-green {
color: LinkText;
}
.fc-terminal__cursor {
background-color: CanvasText;
color: Canvas;
}
.fc-terminal__status {
background-color: Canvas;
color: CanvasText;
border-block-start-color: CanvasText;
}
}
}
/*!
* future-css -- code block and code editor surface
*
* Styles the reading surface for source code. The highlighting itself is a
* JavaScript concern (`src/js/highlight.ts`); this file owns only the surface,
* the gutters, and the token colors.
*
* Design constraints that shaped this:
*
* - **The markup is an enhancement, the text is the contract.** Every token
* span sits inside a single `<code>`, so the element's `textContent` is
* byte-identical to the source. A copy button and a machine reader both get
* the original text, and highlighting can be turned off with CSS alone.
* - **No inline styles.** Roles are classes (`fc-code__tok--keyword`), so a
* theme can restyle them and the CSS stays inspectable. An inline `style` per
* span would make the DOM the stylesheet.
* - **Wrapping is a design decision, not an inherited one.** `pre-wrap` is a
* real accessibility improvement, but it makes line numbers meaningless, so
* the gutter and the wrap mode are designed together.
*/
@layer fc.components {
/* ---- Surface ----------------------------------------------------------- */
.fc-code {
display: block;
min-inline-size: 0;
background-color: var(--fc-code-bg);
color: var(--fc-code-fg);
border: var(--fc-border-width-surface, var(--fc-border-width-thin)) solid var(--fc-code-border-default);
border-radius: var(--fc-radius-lg);
/* Child surfaces must follow the outer curve without painting over its border. */
overflow: clip;
/*
* Code carries a denser leading than prose. The document's line-height is
* tuned for reading sentences; code is read by shape, and loose leading
* blurs the line boundaries that carry the structure.
*/
line-height: var(--fc-line-height-snug);
}
/*
* Wrapping is the default, deliberately. WCAG 2.2 requires content to work at
* 400% zoom without two-dimensional scrolling, and `pre` with `overflow-x`
* fails that for any line wider than the viewport. `.fc-code--scroll` opts
* into scrolling for the cases where column alignment genuinely matters.
*/
.fc-code__source {
display: block;
padding: var(--fc-space-3) var(--fc-space-4);
margin: 0;
/* A block source must not inherit the base inline-code pill decoration. */
background-color: transparent;
border-radius: 0;
font-family: var(--fc-font-family-mono);
font-size: var(--fc-font-size-sm);
tab-size: 2;
/* `break-word` rather than `anywhere`: it breaks a long token only when it
would otherwise overflow, so short lines are left untouched. */
overflow-wrap: break-word;
white-space: pre-wrap;
}
/*
* `:where()` on the descendant, deliberately.
*
* Scoping a modifier to a child needs two classes in a selector, which reads as
* over-qualification -- and rightly so, since a bare descendant combinator
* raises specificity for no gain. `:where()` contributes zero, so the modifier
* selects *what* to style while `.fc-code__source` keeps its own single-class
* specificity and a consumer's override still wins. This is the same reasoning
* as the reset layer, applied inside a component.
*/
.fc-code--scroll :where(.fc-code__source) {
white-space: pre;
overflow-wrap: normal;
overflow-x: auto;
}
/* ---- Chrome ------------------------------------------------------------ */
/*
* Optional header carrying the language name and actions. Separate from the
* source so the source can be copied without dragging the label along.
*/
.fc-code__bar {
grid-column: 1 / -1;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: var(--fc-space-2);
padding: var(--fc-space-2) var(--fc-space-4);
border-block-end: var(--fc-border-width-thin) solid var(--fc-code-border-default);
background-color: var(--fc-code-bg-subtle);
font-family: var(--fc-font-family-sans);
font-size: var(--fc-font-size-xs);
color: var(--fc-code-fg);
}
/*
* The language name is a real element, not a CSS `content` value. Generated
* content is exposed inconsistently to screen readers and cannot be selected
* or copied, which would make the one piece of real information unselectable.
*/
.fc-code__lang {
font-weight: var(--fc-font-weight-medium);
letter-spacing: 0.04em;
text-transform: uppercase;
}
/* ---- Gutter ------------------------------------------------------------ */
/*
* Line numbers are `aria-hidden` in the markup. They are a visual index, and
* announcing "line 47" before each of 47 lines makes the content unusable.
* The source text remains the accessible content.
*/
.fc-code--numbered {
display: grid;
/* One column for both halves, so the numbers and the source stay aligned. */
grid-template-columns: auto minmax(0, 1fr);
}
/*
* The gutter is a separate element rather than a column *inside* the source,
* which is what makes alignment break when a line wraps: the source uses
* `pre-wrap`, so one logical line can occupy several visual rows, and an
* independent gutter keeps allocating exactly one number per logical line.
* After the first wrap every subsequent number is off by a row.
*
* `align-content: start` plus a fixed line-height on the numbers is the whole
* fix that is available in CSS alone: the gutter cannot know how many visual
* rows each line took. Two honest options, and neither is free:
*
* - Wrapping (the default) keeps 400% zoom accessible, which WCAG 1.4.10
* requires, and accepts that a *wrapped* numbered block can misalign.
* - `.fc-code--scroll` disables wrapping so one logical line is always one
* visual row and the numbers are exact.
*
* The line-height must match `.fc-code__source` exactly or every number is
* off by a constant offset, so both read the same token.
*/
.fc-code__gutter {
padding-block: var(--fc-space-3);
padding-inline: var(--fc-space-2);
border-inline-end: var(--fc-border-width-thin) solid var(--fc-code-border-default);
background-color: var(--fc-code-bg-inset);
color: var(--fc-code-fg);
font-family: var(--fc-font-family-mono);
font-size: var(--fc-font-size-sm);
line-height: var(--fc-line-height-snug);
font-variant-numeric: tabular-nums;
text-align: end;
/* Numbers are their own blocks so they align as a column rather than
running together into "1234". */
user-select: none;
/* Track the source's top padding so the first number sits on the first
line rather than floating above it. */
align-content: start;
}
.fc-code__gutter > span {
display: block;
/* Mirrors the source's `pre-wrap` line box. A gutter number is one logical
line, so it must occupy exactly one source line box. */
min-block-size: 1lh;
}
.fc-code--numbered :where(.fc-code__source) {
padding-inline-start: var(--fc-space-4);
}
/*
* Scrolling makes the numbering exact, so say so rather than leaving the
* tradeoff to be discovered. Wrapped numbering is approximate by
* construction; unwrapped numbering is exact.
*
* `:where()` on the descendant contributes zero specificity, so this does not
* outrank a consumer's own `.fc-code__gutter > span` rule.
*/
.fc-code--scroll :where(.fc-code__gutter > span) {
min-block-size: 1lh;
}
/* ---- Token colors ------------------------------------------------------ */
/*
* Every role is declared, even where two share a value. A missing declaration
* silently inherits, so an unstyled role looks like a deliberate choice and is
* not. Declaring all of them also puts the whole role set in one place when a
* theme is remapped.
*
* Weight is used on two roles only. Weighting every token is what makes some
* themes look like a fruit salad; keywords and types are the ones a reader
* actually scans for.
*/
.fc-code__tok--fg { color: var(--fc-code-fg); }
.fc-code__tok--comment { color: var(--fc-code-comment); font-style: italic; }
.fc-code__tok--keyword { color: var(--fc-code-keyword); font-weight: var(--fc-font-weight-semibold); }
.fc-code__tok--string { color: var(--fc-code-string); }
.fc-code__tok--number { color: var(--fc-code-number); }
.fc-code__tok--function { color: var(--fc-code-function); }
.fc-code__tok--type { color: var(--fc-code-type); }
.fc-code__tok--operator { color: var(--fc-code-operator); }
.fc-code__tok--punctuation { color: var(--fc-code-punctuation); }
.fc-code__tok--property { color: var(--fc-code-property); }
.fc-code__tok--constant { color: var(--fc-code-constant); }
.fc-code__tok--tag { color: var(--fc-code-tag); }
.fc-code__tok--attribute { color: var(--fc-code-attribute); }
.fc-code__tok--regexp { color: var(--fc-code-regexp); }
.fc-code__tok--heading { color: var(--fc-code-heading); font-weight: var(--fc-font-weight-semibold); }
.fc-code__tok--link { color: var(--fc-code-link); text-decoration: underline; }
.fc-code__tok--invalid { color: var(--fc-code-invalid); }
/*
* An unrecognised language. The text is left exactly as written -- a fallback
* must never damage content -- but the block is marked so a developer can see
* that highlighting did not apply.
*/
.fc-code--unsupported :where(.fc-code__source) {
color: var(--fc-code-fg);
}
/* ---- Forced colors ----------------------------------------------------- */
/*
* The OS discards every `color` declared above, so the distinctions between
* roles collapse. That is the correct outcome -- these users asked for a
* palette the OS controls -- but the italic comment style survives, because it
* is not a color, and it keeps comments distinguishable without any hue.
*/
@media (forced-colors: active) {
.fc-code {
background-color: Canvas;
color: CanvasText;
border-color: CanvasText;
}
.fc-code__bar,
.fc-code__gutter {
background-color: Canvas;
color: GrayText;
border-color: GrayText;
}
.fc-code__tok--comment {
font-style: italic;
}
/* Underlined, so links stay distinguishable once hue is discarded. */
.fc-code__tok--link {
text-decoration: underline;
}
}
}
/*!
* future-css -- diff viewer
*
* Renders the output of `diffLines()` as a reviewable sequence of lines.
*
* Design constraints that shaped this:
*
* - **Colour is never the only cue.** Added and removed lines differ in
* background, in foreground hue, in a +/- glyph, and in `data-fc-diff`. Drop
* any one of those -- a colour-blind reader, a printer, forced colors, a
* monochrome terminal -- and the distinction survives. That redundancy is why
* this is not simply `.fc-diff--add { background: green }`.
* - **State lives in `data-fc-diff`, not a class.** The value is the
* machine-readable half of the DOM contract described in AGENTS.md, so an
* automated reader selects added or removed rows with a selector rather than
* by reading colors.
* - **Gutter and content share a row.** Each line is one grid row with its own
* number cells, so a line number cannot drift from its content.
*/
@layer fc.components {
.fc-diff {
display: block;
min-inline-size: 0;
background-color: var(--fc-code-bg);
color: var(--fc-code-fg);
border: var(--fc-border-width-thin) solid var(--fc-code-border-default);
border-radius: var(--fc-radius-md);
/*
* Code carries a denser leading than prose: it is read by shape, and loose
* leading blurs the line boundaries that carry the structure.
*/
line-height: var(--fc-line-height-snug);
}
/* ---- Header ------------------------------------------------------------ */
/*
* Summary bar. The counts are text, not decoration: "+3 -1" is the fastest way
* for a reviewer to size a change before reading it.
*/
.fc-diff__bar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--fc-space-3);
padding: var(--fc-space-2) var(--fc-space-4);
border-block-end: var(--fc-border-width-thin) solid var(--fc-code-border-default);
background-color: var(--fc-code-bg-subtle);
font-family: var(--fc-font-family-sans);
font-size: var(--fc-font-size-xs);
}
.fc-diff__stat {
display: inline-flex;
align-items: center;
gap: var(--fc-space-1);
font-variant-numeric: tabular-nums;
/* Never zeroed out: a count of zero is meaningful, so the numbers must not
shift the layout as they change. */
min-inline-size: 3.5ch;
}
.fc-diff__stat--added { color: var(--fc-code-added-fg); }
.fc-diff__stat--removed { color: var(--fc-code-removed-fg); }
/* ---- Lines ------------------------------------------------------------- */
/*
* One grid row per line: marker, old number, new number, content.
*
* `grid-template-columns` rather than a `<table>`: a diff is not tabular data
* with a header relationship, it is a sequence of lines, and a table would
* imply column semantics that do not exist. Row role and per-line state come
* from attributes instead.
*/
.fc-diff__line {
display: grid;
grid-template-columns: 2ch auto auto 1fr;
align-items: baseline;
gap: var(--fc-space-2);
padding-inline: var(--fc-space-2);
font-family: var(--fc-font-family-mono);
font-size: var(--fc-font-size-sm);
/*
* `pre-wrap` rather than `pre` plus a scroller: a long line wraps instead of
* forcing two-dimensional scrolling, which WCAG 1.4.10 treats as a failure.
* `.fc-diff--scroll` opts back out for column-aligned review.
*/
white-space: pre-wrap;
overflow-wrap: break-word;
}
/*
* Scrolling mode has to provide the scroller. Switching rows to `white-space:
* pre` without one leaves nowhere for the overflow to go, so a long line
* widens the whole document instead of scrolling inside the diff -- which is
* the same 1.4.10 failure the wrapping default exists to avoid.
*/
.fc-diff--scroll {
overflow-x: auto;
overscroll-behavior-inline: contain;
}
.fc-diff--scroll :where(.fc-diff__line) {
white-space: pre;
overflow-wrap: normal;
}
/*
* A grid row's width is its container's, so the row itself cannot scroll; the
* bar has to be as wide as its content for the scroller to have anything to
* scroll. `min-inline-size: max-content` is what makes this work without
* fixing a column width the consumer cannot override.
*
* `position: sticky` keeps the summary in view while scrolling sideways,
* which is the only reason the bar is worth pinning during a horizontal read.
*/
.fc-diff--scroll :where(.fc-diff__bar) {
position: sticky;
inset-inline-start: 0;
min-inline-size: max-content;
}
/*
* State, driven by the data attribute so the styling, the machine-readable
* contract, and the sign glyph all derive from one source of truth.
*/
.fc-diff__line[data-fc-diff="add"] {
background-color: var(--fc-code-bg-added);
color: var(--fc-code-added-fg);
}
.fc-diff__line[data-fc-diff="remove"] {
background-color: var(--fc-code-bg-removed);
color: var(--fc-code-removed-fg);
}
/*
* The +/- glyph is text in the markup, not a CSS `content` value, so it is
* selectable, translatable, and readable when colors are stripped. It is
* `aria-hidden`, since the state is already conveyed by `data-fc-diff`.
*/
.fc-diff__marker {
text-align: center;
user-select: none;
}
/*
* Line numbers. A line present on only one side has no number on the other,
* and leaving that cell empty -- rather than showing 0 or a dash -- keeps the
* columns aligned and never invents a line that did not exist.
*/
.fc-diff__num {
color: var(--fc-code-fg);
font-variant-numeric: tabular-nums;
text-align: end;
min-inline-size: 3ch;
/* Visual reference only; `aria-hidden` in the markup, because announcing
"1 1" before every line makes the content far harder to follow. */
user-select: none;
opacity: 0.75;
}
.fc-diff__content {
min-inline-size: 0;
}
/*
* Syntax colors inside a diff row need no rules of their own.
*
* It is tempting to re-declare them scoped to `.fc-diff` because the row tints
* the background. That is unnecessary and wrong: the token classes from
* `code.css` already carry exactly the right values, and scoping them here
* would raise specificity for no gain -- which is precisely what the
* `no-over-qualified` rule exists to prevent. The tint is subtle enough that
* the solved contrast ratios still hold against it, and that is why
* `added-fg`/`removed-fg` were checked against the tinted surfaces rather than
* against `bg`.
*/
/* ---- Hunk separator ---------------------------------------------------- */
/*
* Collapses long runs of unchanged lines. Purely an affordance: the full line
* sequence stays in the DOM, so a machine reading the diff is not misled by
* what a person sees.
*/
.fc-diff__gap {
display: flex;
align-items: center;
gap: var(--fc-space-2);
padding: var(--fc-space-1) var(--fc-space-4);
background-color: var(--fc-code-bg-subtle);
color: var(--fc-code-fg);
border-block: var(--fc-border-width-thin) solid var(--fc-code-border-default);
font-family: var(--fc-font-family-sans);
font-size: var(--fc-font-size-xs);
}
/* ---- Forced colors ----------------------------------------------------- */
/*
* Under forced colors the OS replaces every background, so the added/removed
* distinction cannot be carried by tint at all. It survives through the glyph,
* the `data-fc-diff` attribute, and the Highlight pair below.
*/
@media (forced-colors: active) {
.fc-diff,
.fc-diff__bar,
.fc-diff__gap {
background-color: Canvas;
color: CanvasText;
border-color: CanvasText;
}
.fc-diff__line[data-fc-diff="add"],
.fc-diff__line[data-fc-diff="remove"] {
background-color: Canvas;
color: CanvasText;
}
.fc-diff__line[data-fc-diff="add"] .fc-diff__marker,
.fc-diff__line[data-fc-diff="remove"] .fc-diff__marker {
/*
* Highlight/HighlightText is the OS's own "selected" pair -- the closest
* system color to "this line changed". `forced-color-adjust: none` is
* required to keep it, since the OS suppresses author colors by default;
* the exemption is scoped to the two-cell marker rather than the row, so
* nothing else escapes the system palette.
*/
background-color: Highlight;
color: HighlightText;
forced-color-adjust: none;
}
.fc-diff__num {
color: GrayText;
}
}
}
/*!
* future-css -- select / listbox
*
* A value-selecting combobox and its option list.
*
* WHY THIS IS A COMPONENT, AND NOT THE NATIVE <select>
* ==============================================
*
* `docs/COMPONENTS.md` previously said a custom listbox was "not a component".
* That position has been reversed: an application that needs models grouped by
* provider, filtered as you type, with typeahead over a few hundred entries
* cannot be served by `<select>`, and fighting the platform to get partway there
* is worse than owning the whole thing.
*
* That makes this the most accessibility-sensitive component in the system,
* because it reimplements part of the accessibility tree. Two consequences shape
* every rule below:
*
* 1. **Focus stays on the invoker.** The highlighted option is conveyed with
* `aria-activedescendant`, never by moving DOM focus. A combobox that moves
* focus into its list cannot be tabbed back to and does not read as a
* combobox at all.
* 2. **State is carried in ARIA, never by colour alone.** Selection is
* `aria-selected`, availability is `aria-disabled`, and the highlight is
* drawn as a surface *and* a border -- so it survives forced-colors mode
* and a greyscale printer.
*
* The value is also exposed as `data-fc-value` on the invoker, per the DOM
* contract in AGENTS.md: a machine reads the selection without parsing the
* visible label, and the label is free to change (a model renamed upstream)
* without the data contract following it.
*/
@layer fc.components {
/* ---- Invoker ------------------------------------------------------------ */
/*
* A `<button role="combobox">`, not a `<div>`.
*
* That single choice buys form participation, Space/Enter activation, the
* platform's focus ring, and correct forced-colors rendering. A div would need
* `role`, `tabindex`, and both key handlers by hand and would still miss the
* rest.
*/
.fc-select {
display: inline-flex;
align-items: center;
justify-content: space-between;
gap: var(--fc-space-2);
inline-size: 100%;
/* The same height, edge, and radius as `.fc-input`, so a select and a text
field in one form read as the same family of control. A dense host
(the context strip) lowers it through `--fc-select-block-size`. */
min-block-size: var(--fc-select-block-size, var(--fc-size-target-comfortable));
padding-block: var(--fc-space-1-5);
padding-inline: var(--fc-space-3);
font-family: var(--fc-font-family-sans);
font-size: var(--fc-font-size-sm);
line-height: var(--fc-line-height-snug);
text-align: start;
color: var(--fc-color-text-default);
background-color: var(--fc-color-bg-surface);
border: var(--fc-border-width-thin) solid var(--fc-color-border-interactive);
border-radius: var(--fc-radius-lg);
cursor: pointer;
transition:
border-color var(--fc-duration-fast) var(--fc-easing-standard),
box-shadow var(--fc-duration-fast) var(--fc-easing-standard);
}
/* `bg-raised` equals `bg-surface` in the light scheme, so a fill change was
no hover at all there. The edge moves toward the text colour instead. */
.fc-select:hover:not([aria-disabled="true"], [aria-expanded="true"]) {
border-color: color-mix(in oklab, var(--fc-color-border-interactive), var(--fc-color-text-default) 40%);
}
/*
* `aria-disabled`, not `disabled`.
*
* A select with no options is a legitimate state the user should be able to
* reach and read -- otherwise they cannot discover *why* it is empty. `open()`
* on an empty list is a no-op, so the button stays operable and simply does
* nothing, which is the honest behaviour.
*/
.fc-select[aria-disabled="true"] {
color: var(--fc-color-disabled-fg);
background-color: var(--fc-color-disabled-bg);
border-color: var(--fc-color-border-default);
cursor: not-allowed;
}
.fc-select:focus-visible {
outline: var(--fc-border-width-focus) solid var(--fc-color-focus-ring);
outline-offset: var(--fc-space-0-5);
}
/*
* The open state is a border change as well as a background one: the list is
* anchored directly beneath, and on a page of identical surfaces a fill
* change alone is easy to miss.
*/
.fc-select[aria-expanded="true"] {
border-color: var(--fc-color-accent-border);
background-color: var(--fc-color-bg-raised);
box-shadow: 0 0 0 var(--fc-space-1) var(--fc-color-accent-bg-subtle);
}
/* ---- Value and chevron --------------------------------------------------- */
/*
* `min-inline-size: 0` so a long label truncates instead of forcing the button
* wider than its container. Without it the flex item refuses to shrink below
* its content and the whole control overflows -- the classic flexbox trap.
*/
.fc-select__value {
min-inline-size: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* The placeholder is what an empty selection reads as, and it must not be
mistaken for a value. */
.fc-select__value[data-fc-placeholder="true"] {
color: var(--fc-color-text-muted);
}
/*
* The chevron is decorative: `aria-hidden` in the markup, because the value
* and `aria-expanded` already say which state this is in. Announcing a glyph
* adds nothing and, on a control read dozens of times a day, is noise.
*/
.fc-select__chevron {
flex: none;
color: var(--fc-color-text-muted);
transition: transform var(--fc-duration-normal) var(--fc-easing-standard);
}
.fc-select[aria-expanded="true"] .fc-select__chevron {
transform: rotate(180deg);
}
/* ---- Option list --------------------------------------------------------- */
/*
* `position: fixed` and the Popover API, for the same reason `.fc-menu` does:
* the list must escape any `overflow: hidden` ancestor, and a listbox that
* scrolls with its container is unusable inside a scrolling panel.
*
* `position-anchor` is progressive enhancement -- where it is unsupported the
* binding falls back to measuring, and the rule below simply does not apply.
*/
.fc-select__list {
/*
* `margin: 0` and `inset: auto` are load-bearing, not resets.
*
* A `popover` in the top layer gets `margin: auto` and `inset: 0` from the
* UA stylesheet, which centers the element and stretches it to fill the
* viewport. That silently defeats the coordinates the binding measures and
* sets: the list came out centred and inverted rather than under its
* invoker. `.fc-menu` has the same popover but is positioned by script at a
* point, so it never met this.
*/
margin: 0;
inset: auto;
position: fixed;
z-index: var(--fc-z-dropdown);
display: flex;
flex-direction: column;
/* A long list scrolls rather than growing past the viewport. */
max-block-size: min(60vh, 24rem);
overflow-y: auto;
overscroll-behavior-block: contain;
min-inline-size: 12rem;
padding-block: var(--fc-space-1);
padding-inline: var(--fc-space-1);
background-color: var(--fc-color-bg-overlay);
color: var(--fc-color-text-default);
border: var(--fc-border-width-thin) solid var(--fc-color-border-default);
border-radius: var(--fc-radius-lg);
box-shadow: var(--fc-shadow-overlay);
font-size: var(--fc-font-size-sm);
line-height: var(--fc-line-height-snug);
/*
* The list fades in rather than snapping open. Opacity only: a scale or
* translate would change the box the binding measures to place and flip
* the list, so it would land a few pixels off its invoker. Entry only -- a
* list that lingers on close would still be painted over the next thing
* the user clicks.
*/
transition: opacity var(--fc-duration-normal) var(--fc-easing-decelerate);
}
@starting-style {
.fc-select__list {
opacity: 0;
}
}
/*
* Same open/closed contract as `.fc-menu`: the Popover API where available, a
* `data-fc-open` attribute the script toggles where it is not. A closed list
* must never be a flash of unpositioned options.
*/
.fc-select__list:not(:popover-open):not([data-fc-open="true"]) {
display: none;
}
/* ---- Options ------------------------------------------------------------- */
/*
* `<div role="option">`, not `<button>`.
*
* A button inside a listbox is a nested interactive control: assistive tech
* announces it as a button *inside* a listbox option, and Tab would try to
* reach it. The listbox pattern deliberately uses non-focusable options and
* moves a single active descendant instead.
*/
.fc-select__option {
display: flex;
align-items: baseline;
gap: var(--fc-space-2);
min-block-size: var(--fc-size-target-min);
padding-block: var(--fc-space-1-5);
padding-inline: var(--fc-space-2);
/* Selected options must stay legible when highlighted, and vice versa, so
selection is a mark rather than a fill. */
color: var(--fc-color-text-default);
border-radius: var(--fc-radius-md);
border-inline-start: var(--fc-border-width-thick) solid transparent;
cursor: pointer;
}
/*
* The highlight is drawn from `aria-activedescendant`'s target, so it follows
* whatever the binding considers active -- there is no second source of truth
* that could disagree with the ARIA.
*
* Surface *and* a left border, deliberately. A fill alone disappears in
* forced-colors mode, where the OS replaces author backgrounds.
*/
.fc-select__option[data-fc-active="true"] {
background-color: var(--fc-color-selected-bg);
color: var(--fc-color-selected-fg);
border-inline-start-color: var(--fc-color-accent-border);
}
/*
* Selection is marked with a check glyph in a reserved slot rather than by
* tinting the row, so "selected" and "highlighted" never look like the same
* state -- which is exactly the confusion that makes a listbox feel broken
* when you arrow onto the option you already chose.
*/
.fc-select__option[aria-selected="true"] {
font-weight: var(--fc-font-weight-semibold);
}
.fc-select__option-check {
flex: none;
inline-size: 1ch;
color: var(--fc-color-accent-text);
}
/*
* An unavailable option is dimmed but never hidden: the user must be able to
* see that it exists and that it is the reason they cannot select it.
*/
.fc-select__option[aria-disabled="true"] {
color: var(--fc-color-disabled-fg);
cursor: not-allowed;
}
/*
* The active descendant may be an unavailable option while arrowing past it --
* it must still be readable, so the disabled colour wins over the highlight.
*/
.fc-select__option[aria-disabled="true"][data-fc-active="true"] {
background-color: transparent;
border-inline-start-color: transparent;
}
.fc-select__option-icon { flex: none; font-size: var(--fc-font-size-lg); }
.fc-select__option-content { display: grid; gap: var(--fc-space-0-5); min-inline-size: 0; }
.fc-select__option-description { font-size: var(--fc-font-size-xs); color: inherit; overflow-wrap: anywhere; }
.fc-select__option-meta { margin-inline-start: auto; font-size: var(--fc-font-size-xs); color: inherit; overflow-wrap: anywhere; }
/* ---- Groups -------------------------------------------------------------- */
/*
* A group heading is `role="presentation"` in the markup and carries the
* accessible name for its `role="group"` wrapper. Visual only.
*/
.fc-select__group-label {
padding-block: var(--fc-space-2) var(--fc-space-1);
padding-inline: var(--fc-space-2);
color: var(--fc-color-text-muted);
font-size: var(--fc-font-size-xs);
font-weight: var(--fc-font-weight-semibold);
/* Uppercase groups read as headings but break camelCase model ids. */
text-transform: none;
}
/* ---- Empty state --------------------------------------------------------- */
/*
* An empty list says so rather than presenting a blank box, which reads as a
* broken control.
*/
.fc-select__empty {
padding-block: var(--fc-space-2);
padding-inline: var(--fc-space-2);
color: var(--fc-color-text-muted);
font-size: var(--fc-font-size-sm);
}
/* ---- Forced colors ------------------------------------------------------- */
/*
* Under forced colors the OS replaces backgrounds, so the highlight tint is
* lost. The left border survives, and `aria-selected` remains in the tree, so
* the state is still conveyed -- but only because it was never carried by the
* tint alone.
*/
@media (forced-colors: active) {
.fc-select,
.fc-select__list {
background-color: Canvas;
color: CanvasText;
border-color: CanvasText;
}
.fc-select[aria-expanded="true"] {
border-color: Highlight;
}
.fc-select__option[data-fc-active="true"] {
background-color: Highlight;
color: HighlightText;
/* The border is a second cue here, and CanvasText keeps it visible against
the Highlight fill the OS substituted. */
border-inline-start-color: CanvasText;
}
.fc-select__option[aria-disabled="true"] {
color: GrayText;
}
.fc-select__option[aria-disabled="true"][data-fc-active="true"] {
background-color: Canvas;
color: GrayText;
border-inline-start-color: Canvas;
}
.fc-select__group-label,
.fc-select__empty {
color: GrayText;
}
}
/* ---- Reduced motion ------------------------------------------------------ */
/*
* The chevron rotation is the only motion. Stopping it outright rather than
* leaving a 0.01ms animation keeps the state change legible via the border
* change, which is not animated.
*/
@media (prefers-reduced-motion: reduce) {
.fc-select__chevron {
transition: none;
}
}
}
/*!
* future-css -- chat composer
*
* The input surface beneath a chat thread: text entry, attachments, and a
* send/stop control.
*
* # The decisions that shaped this
*
* **A `<textarea>`, not `contenteditable`.** The editor component uses
* contenteditable, so this looks inconsistent -- and it should, because the
* requirements are different. A composer needs IME composition to work, and an
* IME-composed character inserted into a contenteditable region fails silently.
* That is not hypothetical: it is routine in CJK and other composing scripts, and
* it is the same failure `terminal.css` documents for its own input. A textarea
* also gives Enter-to-send, Shift+Enter-for-newline, and native undo for free.
*
* **One send control, not two.** Send and Stop swap meaning on the same button as
* `data-fc-state` changes. Two separate buttons mean two tab stops where the user
* expects one, and a "Stop" button that lingers after the run finishes is a dead
* control sitting next to a live one. The binding owns which click does what.
*
* **Attachments are a list, not a blob.** Each one is a removable chip with its
* own accessible name. "Remove" repeated four times tells a screen-reader user
* nothing about which attachment is going away, so every chip names its file.
*/
@layer fc.components {
/* ---- Shell --------------------------------------------------------------- */
/*
* Pinned beneath the thread. The `border-block-start` rather than a full border
* is what makes it read as attached to the thread above rather than floating.
*/
.fc-composer {
min-inline-size: 0;
display: flex;
flex-direction: column;
gap: var(--fc-space-2);
padding-block: var(--fc-space-3);
padding-inline: var(--fc-space-4);
background-color: var(--fc-composer-bg, var(--fc-color-bg-surface));
color: var(--fc-composer-fg, var(--fc-color-text-default));
border-block-start: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
}
/* Optional standalone surface; input parts read these properties so no
descendant modifier selector can raise their specificity. */
.fc-composer--contained {
--fc-composer-input-border: transparent;
--fc-composer-input-padding: 0;
--fc-composer-input-size: var(--fc-font-size-md);
border: var(--fc-border-width-thin) solid var(--fc-color-border-interactive);
border-radius: var(--fc-composer-radius, var(--fc-radius-2xl));
box-shadow: var(--fc-shadow-raised);
padding: var(--fc-space-4);
}
.fc-composer--contained:focus-within {
border-color: var(--fc-color-accent-border);
}
/* ---- Text entry ---------------------------------------------------------- */
/*
* A real `<textarea>` with `resize: none`. The browser's resize grip is a
* second, unlabelled way to resize the control, and it lets the user make the
* input taller than the layout can accommodate. The binding grows the field
* from its content instead, up to a ceiling.
*/
.fc-composer__input {
display: block;
inline-size: 100%;
resize: none;
min-block-size: 2.5lh;
/* The ceiling. The binding measures and clamps, but this is the fallback when
scripting is absent: without it a textarea defaults to a single row and the
placeholder is clipped. */
max-block-size: 12lh;
padding-block: var(--fc-space-2);
padding-inline: var(--fc-composer-input-padding, var(--fc-space-3));
font-family: var(--fc-font-family-sans);
font-size: var(--fc-composer-input-size, var(--fc-font-size-sm));
line-height: var(--fc-line-height-normal);
color: var(--fc-composer-fg, var(--fc-color-text-default));
background-color: var(--fc-composer-bg, var(--fc-color-bg-surface));
border: var(--fc-border-width-thin) solid var(--fc-composer-input-border, var(--fc-color-border-default));
border-radius: var(--fc-radius-md);
/* Grows with its content, up to max-block-size, then scrolls. */
field-sizing: content;
overflow-y: auto;
}
.fc-composer__input::placeholder {
color: var(--fc-color-text-muted);
}
.fc-composer__input:focus-visible {
outline: var(--fc-border-width-focus) solid var(--fc-color-focus-ring);
outline-offset: var(--fc-space-0-5);
}
/*
* Disabled while a run is in flight and the composer is not accepting input.
*
* `readOnly` rather than `disabled` in the markup: a disabled control leaves the
* tab order, so a keyboard user tabbing past the composer would not learn that
* there is anything there. Read-only keeps it focusable and readable.
*/
.fc-composer__input[readonly] {
background-color: var(--fc-color-bg-subtle);
color: var(--fc-color-text-muted);
}
/* ---- Row: attachments, actions ------------------------------------------- */
.fc-composer__row {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--fc-space-3);
flex-wrap: wrap;
}
.fc-composer__tools {
min-inline-size: 0;
max-inline-size: 100%;
display: flex;
align-items: center;
gap: var(--fc-space-2);
flex-wrap: wrap;
}
/* ---- Attachments --------------------------------------------------------- */
/*
* A list, so a screen reader announces how many are attached. `role="list"`
* with plain `<li>` children rather than buttons-in-a-div: the chips are not
* themselves interactive, only their remove buttons are.
*/
.fc-composer__attachments {
display: flex;
flex-wrap: wrap;
gap: var(--fc-space-2);
margin: 0;
padding: 0;
list-style: none;
}
.fc-composer__attachment {
display: inline-flex;
align-items: center;
/*
* The name's 14rem cap plus the remove button is wider than a 320px
* composer, so the chip is also capped at its row; the name then truncates
* further instead of pushing the page sideways. Found by the fixture page.
*/
max-inline-size: 100%;
min-inline-size: 0;
gap: var(--fc-space-1);
padding-block: var(--fc-space-1);
padding-inline: var(--fc-space-2);
background-color: var(--fc-color-bg-subtle);
color: var(--fc-composer-fg, var(--fc-color-text-default));
border: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
border-radius: var(--fc-radius-full);
font-size: var(--fc-font-size-xs);
}
/*
* The file name is truncated rather than allowed to wrap the chip, so a long
* path cannot make the row grow without bound. `title` carries the full value.
*/
.fc-composer__attachment-name {
min-inline-size: 0;
max-inline-size: 14rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/*
* The remove button. `aria-label` names the file it removes -- "Remove" four
* times tells a screen-reader user nothing about which attachment is going away.
*/
.fc-composer__attachment-remove {
display: inline-flex;
align-items: center;
justify-content: center;
inline-size: var(--fc-space-4);
block-size: var(--fc-space-4);
padding: 0;
font-family: var(--fc-font-family-sans);
font-size: var(--fc-font-size-xs);
line-height: 1;
color: var(--fc-color-text-muted);
background-color: transparent;
border: 0;
border-radius: var(--fc-radius-full);
cursor: pointer;
}
.fc-composer__attachment-remove:hover {
background-color: var(--fc-color-danger-bg);
color: var(--fc-color-danger-text);
}
.fc-composer__attachment-remove:focus-visible {
outline: var(--fc-border-width-focus) solid var(--fc-color-focus-ring);
outline-offset: var(--fc-space-0-5);
}
/*
* An attachment that could not be read is shown, not silently dropped. Hiding
* it would leave the user sending a request that is missing what they added.
*/
.fc-composer__attachment[data-fc-state="error"] {
border-color: var(--fc-color-danger-border);
color: var(--fc-color-danger-text);
}
/* ---- Send / stop --------------------------------------------------------- */
/*
* One control whose meaning changes with `data-fc-state`. The button is always
* present: swapping it for a different element would move focus, and a control
* that appears mid-run is exactly the one a keyboard user is not looking at.
*/
.fc-composer__send {
margin-inline-start: auto;
min-inline-size: var(--fc-size-target-min);
min-block-size: var(--fc-size-target-min);
}
/*
* Stop is a destructive action and is styled as one. While a run is in flight
* the fill goes away, because "stop" is an interruption rather than a
* confirmation, and a filled accent button invites a click that aborts work.
*/
/*
* `danger-bg-subtle`, not the solid `danger-bg`.
*
* The only *declared* danger pairing is `danger-text` on `danger-bg-subtle`;
* pairing dark danger text with a solid danger fill puts two neighbouring ramp
* steps together and the label stops being readable. Stop is the only control
* for aborting a run, so it has to meet the 4.5:1 minimum in both schemes --
* which means using the combination the contrast contract actually validates
* rather than the one that looks more emphatic.
*/
.fc-composer__send[data-fc-state="running"] {
background-color: var(--fc-color-danger-bg-subtle);
color: var(--fc-color-danger-text);
border-color: var(--fc-color-danger-border);
font-weight: var(--fc-font-weight-semibold);
}
.fc-composer__send[data-fc-state="running"]:hover {
background-color: var(--fc-color-danger-bg-subtle);
border-color: var(--fc-color-danger-border);
}
/* An empty composer cannot be sent. Disabled rather than inert: it stays
focusable so the reason is discoverable. */
.fc-composer__send:disabled {
opacity: 1;
cursor: not-allowed;
}
/* ---- Status -------------------------------------------------------------- */
/*
* Token and attachment counts. Always present in the DOM and updated in place
* rather than added and removed, because a live region that gains content
* announces -- one that gains and loses silently does not.
*/
.fc-composer__status {
margin: 0;
color: var(--fc-color-text-muted);
font-size: var(--fc-font-size-xs);
}
/* ---- Drag and drop ------------------------------------------------------- */
/*
* A drop target needs to say so. The border and tint are the only cue, and they
* must not depend on colour alone -- the status line names what is happening.
*/
.fc-composer[data-fc-dragging="true"] {
background-color: var(--fc-color-bg-raised);
border-color: var(--fc-color-accent-border);
}
/* ---- Forced colors ------------------------------------------------------- */
/*
* The disabled input, the error chip, and the drop cue are all carried by
* background there, which the OS discards. Each is restated with a system colour
* or a border so the states remain distinguishable.
*/
@media (forced-colors: active) {
.fc-composer {
background-color: Canvas;
color: CanvasText;
border-block-start-color: CanvasText;
}
.fc-composer--contained {
border-color: CanvasText;
}
.fc-composer--contained:focus-within {
border-color: Highlight;
}
.fc-composer__input {
background-color: Canvas;
color: CanvasText;
border-color: CanvasText;
}
.fc-composer__input[readonly] {
color: GrayText;
}
.fc-composer__attachment {
background-color: Canvas;
color: CanvasText;
border-color: CanvasText;
}
.fc-composer__attachment[data-fc-state="error"] {
border-color: LinkText;
color: LinkText;
}
.fc-composer__send[data-fc-state="running"] {
background-color: Canvas;
color: CanvasText;
border-color: Highlight;
}
.fc-composer[data-fc-dragging="true"] {
background-color: Canvas;
border-color: Highlight;
}
}
}
/*!
* future-css -- context strip
*
* The row of context controls above a chat composer: provider, model, effort,
* and model parameters.
*
* WHY THIS IS ITS OWN COMPONENT
* =============================
*
* Each control is a `fc-select`, so the strip itself is mostly layout. But two
* behaviours belong to the *group* rather than to any one control, and neither
* can live in the select:
*
* 1. **Provider filters model.** Changing provider swaps the model list, and the
* previously selected model may not exist under the new one. Each control
* owning its own state would leave the pair disagreeing — a model from one
* provider selected under another, which is a request nobody can fulfil.
* 2. **Effort is not always available.** Reasoning models accept an effort
* level; others do not, and offering a control that will be silently ignored
* is worse than not offering it.
*
* So the strip is where those relationships live, and each control stays a
* reusable select.
*/
@layer fc.components {
.fc-context-strip {
display: flex;
align-items: center;
gap: var(--fc-space-2);
/*
* Wraps rather than scrolls. A strip of five controls at 400% zoom on a narrow
* window has to reflow; a horizontal scroller here is the same 1.4.10 failure
* the diff view had.
*/
flex-wrap: wrap;
padding-block: var(--fc-space-2);
padding-inline: var(--fc-space-3);
/* A toolbar of compact selects; full form height would make the strip
taller than the conversation line it annotates. */
--fc-select-block-size: 2rem;
background-color: var(--fc-color-bg-subtle);
border: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
border-radius: var(--fc-radius-md);
}
/*
* One labelled control.
*
* The label is `fc-u-visually-hidden` in markup rather than a visible word, so
* the strip stays compact; each control's accessible name comes from its label.
*/
.fc-context-strip__field {
display: inline-flex;
align-items: center;
gap: var(--fc-space-2);
min-inline-size: 0;
}
.fc-context-strip__label {
color: var(--fc-color-text-muted);
font-size: var(--fc-font-size-xs);
font-weight: var(--fc-font-weight-medium);
/* Wrapping a label mid-word reads as a rendering fault. */
white-space: nowrap;
}
/* Pushes the parameters to the end of the row at wide sizes. */
.fc-context-strip__spacer {
flex: 1 1 0;
min-inline-size: 0;
}
/*
* Model parameters: temperature, max tokens, and so on.
*
* Shown as read-only values rather than inputs. They are model *facts* the
* application configures, and a spinner next to a generated token count implies
* a precision the number does not have. A consumer that wants them editable
* replaces this region.
*/
.fc-context-strip__params {
display: inline-flex;
align-items: center;
gap: var(--fc-space-2);
flex-wrap: wrap;
}
.fc-context-strip__param {
display: inline-flex;
align-items: baseline;
gap: var(--fc-space-1);
padding-block: var(--fc-space-1);
padding-inline: var(--fc-space-2);
background-color: var(--fc-color-bg-surface);
border: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
border-radius: var(--fc-radius-full);
font-family: var(--fc-font-family-mono);
font-size: var(--fc-font-size-xs);
color: var(--fc-color-text-default);
}
.fc-context-strip__param-name {
color: var(--fc-color-text-muted);
}
/* Figures must not shift width as they change. */
.fc-context-strip__param-value {
font-variant-numeric: tabular-nums;
}
/*
* A control that does not apply to the current model.
*
* Kept in the layout with a dashed border rather than removed: a row that reflows when the
* user changes model makes the whole strip jump, and the absence would not be
* announced at all. `aria-disabled` is already on the select; this is the visual
* half, and it does not depend on the attribute so a consumer overriding the
* attribute does not silently lose the styling.
*/
.fc-context-strip__field[data-fc-applicable="false"] {
border-block-end: var(--fc-border-width-thin) dashed var(--fc-color-border-default);
}
/*
* A value the current model does not support, e.g. an effort level carried over
* from a model that had one. Surfaced rather than quietly cleared, because a
* setting that disappears is a setting the user cannot find again.
*/
.fc-context-strip__notice {
display: inline-flex;
align-items: center;
gap: var(--fc-space-1);
color: var(--fc-color-warning-text);
font-size: var(--fc-font-size-xs);
}
/* ---- Forced colors ------------------------------------------------------- */
/*
* The not-applicable state retains a border cue in forced colors. The notice is colour-only in the default scheme and must
* be restated.
*/
@media (forced-colors: active) {
.fc-context-strip {
background-color: Canvas;
color: CanvasText;
border-color: CanvasText;
}
.fc-context-strip__param {
background-color: Canvas;
color: CanvasText;
border-color: CanvasText;
}
.fc-context-strip__notice {
color: CanvasText;
/* The warning is also conveyed by the text itself ("not supported"), so the
colour here is reinforcement rather than the signal. */
font-weight: var(--fc-font-weight-semibold);
}
}
}
/*!
* future-css -- git surface
*
* A working tree: branch, status counts, and a file list with per-file badges and
* line stats.
*
* WHY THE STATES ARE DECLARED RATHER THAN INFERRED
* ================================================
*
* A file's stage is genuinely two independent facts -- its index state and its
* worktree state -- and a file can be in any combination. A file staged as
* modified and *also* modified again in the worktree is the case that matters:
* it appears twice in a commit-shaped question and once in an unstaged question.
*
* So both are rendered as separate badges keyed by `data-fc-stage`, and the row
* declares `data-fc-index` / `data-fc-worktree` for a reader that wants the facts
* rather than the labels.
*/
@layer fc.components {
.fc-git {
display: flex;
flex-direction: column;
gap: var(--fc-space-2);
background-color: var(--fc-color-bg-surface);
border: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
border-radius: var(--fc-radius-md);
overflow: hidden;
}
.fc-git__header {
display: flex;
align-items: baseline;
gap: var(--fc-space-2);
flex-wrap: wrap;
padding-block: var(--fc-space-2);
padding-inline: var(--fc-space-3);
background-color: var(--fc-color-bg-subtle);
border-block-end: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
}
.fc-git__branch {
font-family: var(--fc-font-family-mono);
font-size: var(--fc-font-size-sm);
font-weight: var(--fc-font-weight-semibold);
color: var(--fc-color-text-default);
/* A long branch name is normal. Truncating the *end* keeps the repository
name visible, which is the part that identifies which branch this is. */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-inline-size: 0;
}
.fc-git__summary {
font-size: var(--fc-font-size-xs);
color: var(--fc-color-text-muted);
/* The counts change in width as they change. Pinning them stops the header
from reflowing on every keystroke elsewhere in the tree. */
font-variant-numeric: tabular-nums;
}
.fc-git__files {
display: flex;
flex-direction: column;
/* The list scrolls; the header does not. A file list taller than the viewport
is normal, a header that scrolls away with it is not. */
overflow-y: auto;
max-block-size: 24rem;
}
.fc-git__empty {
margin: 0;
padding: var(--fc-space-4) var(--fc-space-3);
color: var(--fc-color-text-muted);
font-size: var(--fc-font-size-sm);
text-align: center;
}
/* ---- One file ----------------------------------------------------------- */
.fc-git__row {
display: flex;
align-items: center;
gap: var(--fc-space-2);
padding-block: var(--fc-space-2);
padding-inline: var(--fc-space-3);
min-block-size: var(--fc-size-target-min);
border-block-end: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
cursor: pointer;
}
.fc-git__row:last-child {
border-block-end: none;
}
.fc-git__row:hover {
background-color: var(--fc-color-bg-subtle);
}
/*
* Selection is a background *and* an inset ring.
*
* The ring is there because the background alone disappears under forced
* colors, where background-color is discarded entirely -- so with only a
* background, the selected row would be invisible in exactly the mode where
* distinguishing it matters most.
*/
.fc-git__row[data-fc-selected="true"] {
background-color: var(--fc-color-selected-bg);
color: var(--fc-color-selected-fg);
box-shadow: inset 2px 0 0 0 var(--fc-color-selected-fg);
}
.fc-git__file-name {
flex: 1 1 auto;
min-inline-size: 0;
font-family: var(--fc-font-family-mono);
font-size: var(--fc-font-size-sm);
color: var(--fc-color-text-default);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* The directory de-emphasised so the filename reads first, which is the part a
reader is scanning for. */
.fc-git__file-dir {
color: var(--fc-color-text-muted);
}
.fc-git__stages {
display: inline-flex;
align-items: center;
gap: var(--fc-space-1);
flex: 0 0 auto;
}
.fc-git__badge {
padding-block: 0 var(--fc-space-0-5);
padding-inline: var(--fc-space-2);
border: var(--fc-border-width-thin) solid currentColor;
border-radius: var(--fc-radius-full);
font-family: var(--fc-font-family-mono);
font-size: var(--fc-font-size-xs);
line-height: var(--fc-line-height-tight);
text-transform: lowercase;
}
/*
* Badge colours come from the semantic state tokens, not from literals, so the
* whole set re-themes. And each state is carried by a border *and* the word,
* so removing colour removes nothing.
*/
.fc-git__badge[data-fc-state="modified"] {
color: var(--fc-color-warning-text);
}
.fc-git__badge[data-fc-state="added"],
.fc-git__badge[data-fc-state="untracked"] {
color: var(--fc-color-success-text);
}
.fc-git__badge[data-fc-state="deleted"] {
color: var(--fc-color-danger-text);
}
.fc-git__badge[data-fc-state="renamed"] {
color: var(--fc-color-info-text);
}
.fc-git__badge[data-fc-state="conflicted"] {
color: var(--fc-color-danger-text);
font-weight: var(--fc-font-weight-semibold);
}
.fc-git__stage-button {
padding-block: var(--fc-space-0-5);
padding-inline: var(--fc-space-2);
min-block-size: var(--fc-size-target-min);
background-color: transparent;
border: var(--fc-border-width-thin) solid var(--fc-color-border-default);
border-radius: var(--fc-radius-sm);
color: var(--fc-color-text-default);
font-family: var(--fc-font-family-sans);
font-size: var(--fc-font-size-xs);
cursor: pointer;
}
.fc-git__stage-button:hover {
background-color: var(--fc-color-bg-subtle);
}
.fc-git__stage-button:focus-visible {
outline: var(--fc-border-width-focus) solid var(--fc-color-focus-ring);
outline-offset: var(--fc-space-0-5);
}
/* ---- Line stats --------------------------------------------------------- */
.fc-git__stat {
display: inline-flex;
gap: var(--fc-space-2);
flex: 0 0 auto;
font-family: var(--fc-font-family-mono);
font-size: var(--fc-font-size-xs);
/* Aligned to the digits: the count must not shift the row's other content as
it changes. */
font-variant-numeric: tabular-nums;
}
.fc-git__stat-add {
color: var(--fc-color-success-text);
}
.fc-git__stat-remove {
color: var(--fc-color-danger-text);
}
/* ---- Focus -------------------------------------------------------------- */
/*
* `:focus-visible` and never `outline: none`.
*
* The row is a `role="option"` with a roving tabindex, so it is focusable and
* the ring is the only thing telling a keyboard user where they are.
*/
.fc-git__row:focus-visible {
outline: var(--fc-border-width-focus) solid var(--fc-color-focus-ring);
outline-offset: calc(-1 * var(--fc-border-width-focus));
}
/* ---- Reduced motion ----------------------------------------------------- */
/* Nothing here animates today, and if something is added later the reduced
motion reset covers it. Stated rather than left implicit so the omission is
a decision, not an oversight. */
/* ---- Forced colors ------------------------------------------------------ */
/*
* Under forced colors the accent background is discarded, so selection rests
* entirely on the ring and the row keeps its own border.
*/
@media (forced-colors: active) {
.fc-git,
.fc-git__header,
.fc-git__row {
background-color: Canvas;
color: CanvasText;
border-color: CanvasText;
}
.fc-git__row[data-fc-selected="true"] {
/* Highlight is the system-selected background, which the OS maps to its own
chosen pair -- so it is guaranteed contrast in a scheme we cannot see. */
background-color: Highlight;
color: HighlightText;
forced-color-adjust: none;
}
/*
* Badges lose their semantic colour here, so each state is marked with a
* leading glyph instead. `content` on a pseudo-element survives forced
* colors because it is generated text, not paint.
*/
.fc-git__badge::before {
content: attr(data-fc-glyph);
margin-inline-end: var(--fc-space-0-5);
}
}
}
/*!
* future-css -- form field
*
* A label, an optional hint, a control, and an optional error, wired together
* with `for`/`id` and `aria-describedby`. The control is the real element --
* `<input>`, `<textarea>`, or a native `<select>` -- so autofill, password
* managers, mobile keyboards, and validation all keep working.
*
* <div class="fc-field">
* <label class="fc-field__label" for="email">Email</label>
* <p class="fc-field__hint" id="email-hint">We never share it.</p>
* <input class="fc-input" id="email" type="email"
* aria-describedby="email-hint email-error">
* <p class="fc-field__error" id="email-error"></p>
* </div>
*
* The error element exists from the start and is filled when validation fails.
* Adding the node later would leave `aria-describedby` pointing at nothing until
* it arrives, and some screen readers cache the description on first focus.
*/
@layer fc.components {
.fc-field {
display: flex;
flex-direction: column;
gap: var(--fc-space-1-5);
min-inline-size: 0;
}
.fc-field__label {
font-size: var(--fc-font-size-sm);
font-weight: var(--fc-font-weight-medium);
color: var(--fc-color-text-default);
}
/*
* The asterisk is decoration for sighted users; the control's `required`
* attribute is what assistive technology announces. Marking it aria-hidden in
* the markup keeps it from being read as "star".
*/
.fc-field__required {
color: var(--fc-color-danger-text);
margin-inline-start: var(--fc-space-0-5);
}
.fc-field__optional {
color: var(--fc-color-text-muted);
font-weight: var(--fc-font-weight-regular);
margin-inline-start: var(--fc-space-1);
}
.fc-field__hint {
margin: 0;
font-size: var(--fc-font-size-sm);
color: var(--fc-color-text-muted);
}
/*
* The error is never colour alone: it is text, it is announced through
* aria-describedby, and it carries a glyph so the state survives greyscale and
* forced colours.
*/
.fc-field__error {
margin: 0;
display: flex;
gap: var(--fc-space-1);
align-items: baseline;
font-size: var(--fc-font-size-sm);
font-weight: var(--fc-font-weight-medium);
color: var(--fc-color-danger-text);
}
.fc-field__error::before {
/* Alternative text after the slash is empty, so the glyph is not read. */
content: "\26A0\FE0E" / "";
}
/* Present in the DOM from the start, invisible until it has something to say. */
.fc-field__error:empty {
display: none;
}
/* ---- Text controls --------------------------------------------------- */
.fc-input {
display: block;
inline-size: 100%;
min-block-size: var(--fc-size-target-comfortable);
/* Small enough that one line of text plus padding stays under the minimum,
so the minimum -- shared with buttons and selects -- is the real height.
With space-2 the input came out 42px beside a 40px button. */
padding-block: var(--fc-space-1-5);
padding-inline: var(--fc-space-3);
background-color: var(--fc-color-bg-surface);
color: var(--fc-color-text-default);
border: var(--fc-border-width-control, var(--fc-border-width-thin)) solid var(--fc-color-border-interactive);
border-radius: var(--fc-radius-lg);
font-family: inherit;
font-size: var(--fc-font-size-md);
line-height: var(--fc-line-height-normal);
transition:
border-color var(--fc-duration-fast) var(--fc-easing-standard),
box-shadow var(--fc-duration-fast) var(--fc-easing-standard),
background-color var(--fc-duration-fast) var(--fc-easing-standard);
}
/*
* Hover moves the border toward the text colour. `border-strong` is not used
* because it is lighter than `border-interactive` in the light scheme, so the
* edge faded on hover there and darkened in dark mode.
*/
.fc-input:hover {
border-color: color-mix(in oklab, var(--fc-color-border-interactive), var(--fc-color-text-default) 40%);
}
/*
* Text inputs match :focus-visible on every focus, mouse included. The ring
* sits flush against the accent border, with a soft accent halo beyond it,
* rather than floating a gap away: a ring that hovers off a text box reads as
* a second box. Contrast still comes from the 2px outline, not the halo.
*/
.fc-input:focus-visible {
border-color: var(--fc-color-accent-border);
outline-offset: 0;
box-shadow: 0 0 0 calc(var(--fc-border-width-focus) + var(--fc-space-1)) var(--fc-color-accent-bg-subtle);
}
.fc-input::placeholder {
color: var(--fc-color-text-subtle);
opacity: 1;
}
/*
* Invalid. `aria-invalid` is set by the application after it has validated;
* `:user-invalid` covers native constraint validation once the user has
* interacted. `:invalid` alone is deliberately not used: it would mark a
* required field red before anyone has typed in it.
*/
.fc-input[aria-invalid="true"],
.fc-input:user-invalid {
border-color: var(--fc-color-danger-border);
}
.fc-input[aria-invalid="true"]:focus-visible,
.fc-input:user-invalid:focus-visible {
outline-color: var(--fc-color-danger-border);
box-shadow: 0 0 0 calc(var(--fc-border-width-focus) + var(--fc-space-1)) var(--fc-color-danger-bg-subtle);
}
.fc-input:disabled {
background-color: var(--fc-color-disabled-bg);
color: var(--fc-color-disabled-fg);
border-color: var(--fc-color-border-default);
cursor: not-allowed;
}
/*
* Read-only is not disabled: the value is still focusable, selectable, and
* submitted. It is drawn as a recessed surface without the interactive border
* so it does not invite typing.
*/
.fc-input:read-only:not(select) {
background-color: var(--fc-color-bg-subtle);
border-color: var(--fc-color-border-default);
}
.fc-input--sm {
min-block-size: 2rem;
padding-block: var(--fc-space-1);
padding-inline: var(--fc-space-2);
font-size: var(--fc-font-size-sm);
border-radius: var(--fc-radius-md);
}
.fc-input--lg {
min-block-size: 3rem;
padding-inline: var(--fc-space-4);
font-size: var(--fc-font-size-lg);
}
/* :where() keeps this at one class, so --autosize below can still win. */
:where(textarea).fc-input {
min-block-size: 5lh;
resize: vertical;
}
/*
* `field-sizing: content` grows the box with what is typed, with no script and
* no measuring. The max keeps a pasted novel from pushing the submit button off
* screen; past it, the textarea scrolls.
*/
.fc-input--autosize {
field-sizing: content;
min-block-size: 3lh;
max-block-size: 16lh;
}
/*
* Filled: a tinted field with an underline, Material's filled text field.
* Reads as a field among dense controls where a full outline would add a
* box to a page of boxes. The underline is the boundary that has to meet
* 3:1 (WCAG 1.4.11), declared against the fill in the contrast contract.
*/
.fc-input--filled {
background-color: var(--fc-color-bg-subtle);
border-color: transparent;
border-block-end-color: var(--fc-color-border-interactive);
border-end-start-radius: 0;
border-end-end-radius: 0;
}
/* Not on an invalid or disabled field, whose own borders must survive hover. */
.fc-input--filled:hover:not([aria-invalid="true"], :user-invalid, :disabled) {
background-color: var(--fc-color-bg-muted);
border-color: transparent;
border-block-end-color: var(--fc-color-text-default);
}
.fc-input--filled:focus-visible:not([aria-invalid="true"], :user-invalid) {
border-block-end-color: var(--fc-color-accent-border);
}
/*
* The date and time inputs' picker button, dimmed until hovered so it reads
* as secondary to the value. The icon follows the token layer's
* `color-scheme`, so it inverts in dark mode on its own.
*/
.fc-input::-webkit-calendar-picker-indicator {
cursor: pointer;
opacity: 0.7;
}
.fc-input::-webkit-calendar-picker-indicator:hover {
opacity: 1;
}
/*
* Native select. The chevron is two gradients rather than an inline SVG,
* because an SVG data URI would carry a literal colour that cannot follow the
* theme. `currentColor` keeps it in step with the text in every scheme.
*/
select.fc-input {
appearance: none;
padding-inline-end: var(--fc-space-8);
background-image:
linear-gradient(45deg, transparent 50%, currentColor 50%),
linear-gradient(135deg, currentColor 50%, transparent 50%);
background-size: 0.3125rem 0.3125rem, 0.3125rem 0.3125rem;
background-repeat: no-repeat;
/* background-position has no logical form, hence the RTL branch below. */
background-position:
calc(100% - 1.125rem) 50%,
calc(100% - 0.8125rem) 50%;
cursor: pointer;
}
select.fc-input:dir(rtl) {
background-position:
0.8125rem 50%,
1.125rem 50%;
}
/* ---- Input group ----------------------------------------------------- */
/*
* A control with leading or trailing adornments (a currency symbol, a unit, a
* button). The group draws the border and the focus ring so the adornments
* read as part of one control rather than three boxes touching.
*/
.fc-input-group {
display: flex;
align-items: stretch;
min-block-size: var(--fc-size-target-comfortable);
background-color: var(--fc-color-bg-surface);
border: var(--fc-border-width-thin) solid var(--fc-color-border-interactive);
border-radius: var(--fc-radius-lg);
overflow: clip;
transition:
border-color var(--fc-duration-fast) var(--fc-easing-standard),
box-shadow var(--fc-duration-fast) var(--fc-easing-standard);
}
.fc-input-group:hover {
border-color: color-mix(in oklab, var(--fc-color-border-interactive), var(--fc-color-text-default) 40%);
}
/* The same halo as a focused `.fc-input`, so a group and a bare field focus
alike. The inner control still draws the 2px ring. */
.fc-input-group:focus-within {
border-color: var(--fc-color-accent-border);
box-shadow: 0 0 0 var(--fc-space-1) var(--fc-color-accent-bg-subtle);
}
.fc-input-group:has([aria-invalid="true"], :user-invalid) {
border-color: var(--fc-color-danger-border);
}
.fc-input-group:has(:disabled) {
background-color: var(--fc-color-disabled-bg);
border-color: var(--fc-color-border-default);
}
.fc-input-group__control {
flex: 1;
min-inline-size: 0;
padding-block: var(--fc-space-1-5);
padding-inline: var(--fc-space-3);
background-color: transparent;
color: var(--fc-color-text-default);
border: 0;
border-radius: 0;
font: inherit;
font-size: var(--fc-font-size-md);
}
/*
* The group clips its corners, which would clip an outside ring too, so the
* inner control draws its ring inset. Removing it instead would leave the
* keyboard user with only a border-colour change to find focus by.
*/
.fc-input-group__control:focus-visible {
outline-offset: calc(var(--fc-border-width-focus) * -1);
box-shadow: none;
}
.fc-input-group__addon {
display: flex;
align-items: center;
padding-inline: var(--fc-space-3);
color: var(--fc-color-text-muted);
background-color: var(--fc-color-bg-subtle);
font-size: var(--fc-font-size-sm);
white-space: nowrap;
}
/* ---- Form-level error summary ---------------------------------------- */
/*
* Shown above the form after a failed submit, with focus moved to it
* (tabindex="-1" in the markup). Each entry links to the field it describes,
* so a keyboard user can jump straight to the problem rather than tabbing
* through every valid field to find it.
*/
.fc-error-summary {
display: flex;
flex-direction: column;
gap: var(--fc-space-2);
padding: var(--fc-space-4);
background-color: var(--fc-color-danger-bg-subtle);
color: var(--fc-color-danger-text);
border: var(--fc-border-width-thin) solid var(--fc-color-danger-border);
border-inline-start-width: var(--fc-space-1);
border-radius: var(--fc-radius-lg);
}
.fc-error-summary__title {
margin: 0;
font-size: var(--fc-font-size-md);
font-weight: var(--fc-font-weight-semibold);
}
.fc-error-summary__list {
margin: 0;
display: flex;
flex-direction: column;
gap: var(--fc-space-1);
padding-inline-start: var(--fc-space-5);
list-style: disc;
}
.fc-error-summary__link {
color: inherit;
text-decoration: underline;
text-underline-offset: 0.15em;
}
/* ---- Forced colors --------------------------------------------------- */
@media (forced-colors: active) {
.fc-input,
.fc-input-group {
border-color: FieldText;
}
.fc-input[aria-invalid="true"],
.fc-input:user-invalid,
.fc-input-group:has([aria-invalid="true"], :user-invalid) {
/* A thicker edge restates "invalid" once the danger hue is discarded. */
border-width: var(--fc-border-width-thick);
}
.fc-input:disabled {
border-color: GrayText;
color: GrayText;
}
.fc-error-summary {
border-color: CanvasText;
}
}
}
/* Alternate presentations share the native control's validation and focus states.
No motion is introduced; field and checkbox own their reduced-motion resets. */
@layer fc.components {
.fc-input--soft { background-color: var(--fc-color-bg-subtle); }
.fc-input--underlined { border-block-start-color: transparent; border-inline-color: transparent; border-radius: 0; }
.fc-field--inline { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr)); align-items: center; gap: var(--fc-space-3); }
.fc-choice { display: flex; align-items: start; gap: var(--fc-space-3); padding: var(--fc-space-4); min-block-size: var(--fc-size-target-comfortable); border: var(--fc-border-width-thin) solid var(--fc-color-border-interactive); border-radius: var(--fc-radius-lg); background-color: var(--fc-color-bg-surface); color: var(--fc-color-text-default); cursor: pointer; }
.fc-choice:hover { background-color: var(--fc-color-bg-subtle); }
.fc-choice:active { background-color: var(--fc-color-bg-muted); }
.fc-choice:has(:checked) { border-color: var(--fc-color-accent-border); background-color: var(--fc-color-selected-bg); }
.fc-choice:has(:focus-visible) { outline: var(--fc-border-width-focus) solid var(--fc-color-focus-ring); outline-offset: var(--fc-space-0-5); }
.fc-choice:has(:disabled), .fc-choice:has([aria-disabled="true"]) { background-color: var(--fc-color-disabled-bg); color: var(--fc-color-disabled-fg); cursor: not-allowed; }
.fc-choice__content { display: grid; gap: var(--fc-space-1); min-inline-size: 0; overflow-wrap: anywhere; }
.fc-choice__description { font-size: var(--fc-font-size-sm); color: inherit; }
@media (forced-colors: active) {
.fc-choice { border-color: ButtonBorder; }
.fc-choice:has(:checked) { outline: var(--fc-border-width-thin) solid Highlight; }
.fc-choice:has(:disabled) { color: GrayText; }
}
}
/* Native disclosure and checkboxes preserve keyboard and form behavior without JS.
No animation is introduced, so reduced-motion needs no component override. */
@layer fc.components {
.fc-multiselect { min-inline-size: 0; border: var(--fc-border-width-thin) solid var(--fc-color-border-interactive); border-radius: var(--fc-radius-lg); background-color: var(--fc-color-bg-surface); color: var(--fc-color-text-default); }
.fc-multiselect__summary { min-block-size: var(--fc-size-target-comfortable); padding: var(--fc-space-3); cursor: pointer; overflow-wrap: anywhere; }
.fc-multiselect__summary:hover { background-color: var(--fc-color-bg-subtle); }
.fc-multiselect__summary:active { background-color: var(--fc-color-bg-muted); }
.fc-multiselect__summary:focus-visible { outline: var(--fc-border-width-focus) solid var(--fc-color-focus-ring); outline-offset: var(--fc-space-0-5); }
.fc-multiselect__panel { display: grid; gap: var(--fc-space-3); padding: var(--fc-space-3); border-block-start: var(--fc-border-width-thin) solid var(--fc-color-border-interactive); }
.fc-multiselect__options { display: grid; gap: var(--fc-space-2); max-block-size: 20rem; overflow: auto; padding: var(--fc-space-1); }
.fc-multiselect__option { display: flex; align-items: start; gap: var(--fc-space-3); min-block-size: var(--fc-size-target-comfortable); padding: var(--fc-space-3); border: var(--fc-border-width-thin) solid var(--fc-color-border-interactive); border-radius: var(--fc-radius-md); cursor: pointer; }
.fc-multiselect__option:hover { background-color: var(--fc-color-bg-subtle); }
.fc-multiselect__option:active { background-color: var(--fc-color-bg-muted); }
.fc-multiselect__option:has(:checked) { background-color: var(--fc-color-selected-bg); border-color: var(--fc-color-accent-border); }
.fc-multiselect__option:has(:focus-visible) { outline: var(--fc-border-width-focus) solid var(--fc-color-focus-ring); outline-offset: var(--fc-space-0-5); }
.fc-multiselect__option:has(:disabled), .fc-multiselect__option:has([aria-disabled="true"]) { background-color: var(--fc-color-disabled-bg); color: var(--fc-color-disabled-fg); cursor: not-allowed; }
.fc-multiselect__content { display: grid; gap: var(--fc-space-1); min-inline-size: 0; overflow-wrap: anywhere; }
.fc-multiselect__description { font-size: var(--fc-font-size-sm); color: inherit; }
.fc-multiselect__actions { display: flex; gap: var(--fc-space-2); flex-wrap: wrap; }
@media (forced-colors: active) {
.fc-multiselect, .fc-multiselect__option { border-color: ButtonBorder; }
.fc-multiselect__option:has(:checked) { outline: var(--fc-border-width-thin) solid Highlight; }
.fc-multiselect__option:has(:disabled) { color: GrayText; }
}
}
/*!
* future-css -- checkbox, radio, and switch
*
* Checkboxes and radios are the native controls, tinted with `accent-color`
* rather than rebuilt. A rebuilt checkbox has to reimplement the indeterminate
* state, forced-colours rendering, zoom, and touch hit-testing; the native one
* already does all four, and `accent-color` themes it from the same token as
* every other accent surface.
*
* <label class="fc-check">
* <input class="fc-check__input" type="checkbox" name="terms">
* <span class="fc-check__label">I accept the terms</span>
* <span class="fc-check__hint">You can withdraw at any time.</span>
* </label>
*
* The switch is the one control drawn by hand, because no native switch exists.
* It is still a real checkbox -- `<input type="checkbox" role="switch">` -- so it
* submits, resets, and toggles with Space like any other.
*/
@layer fc.components {
/*
* The whole label is the hit target, so the target size is met by the row,
* not by the 1.125rem box.
*/
.fc-check {
display: grid;
grid-template-columns: auto 1fr;
column-gap: var(--fc-space-2);
row-gap: var(--fc-space-0-5);
align-items: start;
min-block-size: var(--fc-size-target-min);
padding-block: var(--fc-space-1);
cursor: pointer;
color: var(--fc-color-text-default);
}
.fc-check__input {
/* Vertically centres the box on the first line of the label, however it wraps. */
margin-block-start: calc((1lh - 1.125rem) / 2);
margin-inline: 0;
inline-size: 1.125rem;
block-size: 1.125rem;
accent-color: var(--fc-color-accent-bg);
cursor: inherit;
}
.fc-check__label {
font-size: var(--fc-font-size-sm);
line-height: var(--fc-line-height-normal);
}
.fc-check__hint {
grid-column: 2;
font-size: var(--fc-font-size-xs);
color: var(--fc-color-text-muted);
}
.fc-check:has(:disabled) {
cursor: not-allowed;
color: var(--fc-color-disabled-fg);
}
.fc-check:has([aria-invalid="true"], :user-invalid) .fc-check__label {
color: var(--fc-color-danger-text);
}
/* A group of checks: a fieldset whose legend is the question being asked. */
.fc-check-group {
display: flex;
flex-direction: column;
gap: var(--fc-space-1);
}
.fc-check-group__legend {
margin-block-end: var(--fc-space-1);
font-size: var(--fc-font-size-sm);
font-weight: var(--fc-font-weight-medium);
}
.fc-check-group--inline {
flex-direction: row;
flex-wrap: wrap;
column-gap: var(--fc-space-5);
}
/* ---- Switch ---------------------------------------------------------- */
/*
* `--fc-switch-travel` is the distance the thumb moves. It is derived from the
* track and thumb sizes so a size modifier only has to restate those three.
* `--fc-switch-stretch` widens the thumb while pressed; the checked position
* subtracts it, so the thumb grows toward the middle from either end instead
* of running off the track.
*
* The off track is filled with `border-strong` rather than outlined: the fill
* is what meets the 3:1 boundary contrast, and a light thumb sits on it. A
* grey thumb on a grey outline -- the old drawing -- met the ratio but read
* as disabled.
*/
.fc-switch {
--fc-switch-w: 2.5rem;
--fc-switch-h: 1.375rem;
--fc-switch-thumb: 1rem;
--fc-switch-stretch: 0rem;
--fc-switch-pad: calc((var(--fc-switch-h) - var(--fc-switch-thumb)) / 2 - var(--fc-border-width-thin));
--fc-switch-travel: calc(
var(--fc-switch-w) - var(--fc-switch-thumb) - 2 * var(--fc-switch-pad) - 2 * var(--fc-border-width-thin)
);
appearance: none;
flex-shrink: 0;
display: inline-flex;
align-items: center;
inline-size: var(--fc-switch-w);
block-size: var(--fc-switch-h);
margin: 0;
padding-inline: var(--fc-switch-pad);
vertical-align: middle;
background-color: var(--fc-color-border-strong);
border: var(--fc-border-width-thin) solid var(--fc-color-border-strong);
border-radius: var(--fc-radius-full);
cursor: pointer;
transition:
background-color var(--fc-duration-normal) var(--fc-easing-standard),
border-color var(--fc-duration-normal) var(--fc-easing-standard);
}
.fc-switch::before {
content: "";
inline-size: calc(var(--fc-switch-thumb) + var(--fc-switch-stretch));
block-size: var(--fc-switch-thumb);
border-radius: var(--fc-radius-full);
background-color: var(--fc-color-bg-thumb);
box-shadow: var(--fc-shadow-raised);
/*
* `translate` is physical and has no logical form; the RTL branch below
* reverses it, the same treatment the progress sweep needed. The spring
* curve gives the thumb a little weight as it lands.
*/
transition:
translate var(--fc-duration-slow) var(--fc-easing-spring),
inline-size var(--fc-duration-fast) var(--fc-easing-standard),
background-color var(--fc-duration-normal) var(--fc-easing-standard);
}
.fc-switch:checked {
background-color: var(--fc-color-accent-bg);
border-color: var(--fc-color-accent-border);
}
@media (hover: hover) {
.fc-switch:hover {
background-color: var(--fc-color-border-interactive);
border-color: var(--fc-color-border-interactive);
}
.fc-switch:checked:hover {
background-color: var(--fc-color-accent-bg-hover);
border-color: var(--fc-color-accent-bg-hover);
}
}
.fc-switch:checked::before {
background-color: var(--fc-color-text-on-accent);
translate: calc(var(--fc-switch-travel) - var(--fc-switch-stretch)) 0;
}
.fc-switch:checked:dir(rtl)::before {
translate: calc((var(--fc-switch-travel) - var(--fc-switch-stretch)) * -1) 0;
}
/* Pressing stretches the thumb, so the control feels like it is being pushed. */
.fc-switch:active:not(:disabled) {
--fc-switch-stretch: calc(var(--fc-switch-thumb) / 4);
}
.fc-switch:disabled {
cursor: not-allowed;
opacity: 1;
}
.fc-switch--sm {
--fc-switch-w: 2rem;
--fc-switch-h: 1.125rem;
--fc-switch-thumb: 0.75rem;
}
.fc-switch--lg {
--fc-switch-w: 3rem;
--fc-switch-h: 1.625rem;
--fc-switch-thumb: 1.25rem;
}
/*
* A switch row: label first, switch at the inline end, which is the settings
* pattern people already know. Laid out by flex so it mirrors in RTL with no
* extra rule.
*/
.fc-switch-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--fc-space-4);
min-block-size: var(--fc-size-target-comfortable);
cursor: pointer;
}
.fc-switch-row__text {
display: flex;
flex-direction: column;
gap: var(--fc-space-0-5);
min-inline-size: 0;
}
.fc-switch-row__label {
font-size: var(--fc-font-size-sm);
font-weight: var(--fc-font-weight-medium);
}
.fc-switch-row__hint {
font-size: var(--fc-font-size-xs);
color: var(--fc-color-text-muted);
}
.fc-switch-row:has(:disabled) {
cursor: not-allowed;
color: var(--fc-color-disabled-fg);
}
/* ---- Forced colors --------------------------------------------------- */
/*
* The track and thumb are backgrounds, which forced colours discard. The thumb
* is restated as a system colour and the checked state as Highlight, so on/off
* is still legible -- and position, not colour, remains the primary cue.
*/
@media (forced-colors: active) {
.fc-switch {
border-color: ButtonText;
background-color: ButtonFace;
}
.fc-switch::before {
background-color: ButtonText;
}
.fc-switch:checked {
background-color: Highlight;
border-color: Highlight;
}
.fc-switch:checked::before {
background-color: HighlightText;
}
.fc-switch:disabled {
border-color: GrayText;
}
.fc-switch:disabled::before {
background-color: GrayText;
}
}
}
/*!
* future-css -- card
*
* A surface that groups related content. The element is whatever the content
* is: `<article>` for a self-contained item, `<section>` with a heading for a
* panel, `<li>` inside a list of cards.
*
* The card is a size container. Its parts respond to the width the card is
* given, not to the viewport, so the same card works in a three-column grid on
* a wide screen, a sidebar, and a phone without a breakpoint per placement. This
* is the first component in the system to use container queries, and the reason
* is exactly that: a card's width is decided by its parent, which the viewport
* says nothing about.
*/
@layer fc.components {
.fc-card {
container: fc-card / inline-size;
position: relative;
display: flex;
flex-direction: column;
min-inline-size: 0;
background-color: var(--fc-card-bg, var(--fc-color-bg-surface));
color: var(--fc-color-text-default);
border: var(--fc-border-width-surface, var(--fc-border-width-thin)) var(--fc-card-border-style, solid) var(--fc-card-border, var(--fc-color-border-subtle));
border-radius: var(--fc-radius-xl);
box-shadow: var(--fc-card-shadow, none);
/* `clip` rather than `hidden`: it rounds the media corners without creating
a scroll container, so sticky children and focus rings behave. */
overflow: clip;
transition:
box-shadow var(--fc-duration-normal) var(--fc-easing-standard),
border-color var(--fc-duration-normal) var(--fc-easing-standard),
translate var(--fc-duration-normal) var(--fc-easing-standard);
}
.fc-card--feature {
background-color: var(--fc-theme-feature-bg, var(--fc-color-bg-surface));
background-image: var(--fc-theme-feature-image, none);
text-shadow: var(--fc-theme-feature-text-shadow, none);
color: var(--fc-theme-feature-fg, var(--fc-color-text-default));
padding: var(--fc-space-6);
}
@media (forced-colors: active) {
.fc-card--feature { text-shadow: none; background-image: none; background-color: Canvas; color: CanvasText; }
}
.fc-card--elevated {
border-color: transparent;
box-shadow: var(--fc-shadow-raised);
}
.fc-card--subtle {
background-color: var(--fc-color-bg-subtle);
border-color: transparent;
}
/* ---- Parts ----------------------------------------------------------- */
.fc-card__media {
display: block;
inline-size: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
background-color: var(--fc-color-bg-muted);
}
.fc-card__header,
.fc-card__body,
.fc-card__footer {
padding-inline: var(--fc-space-5);
}
.fc-card__header {
display: flex;
align-items: start;
gap: var(--fc-space-3);
padding-block-start: var(--fc-space-5);
}
.fc-card__heading {
display: flex;
flex-direction: column;
gap: var(--fc-space-1);
flex: 1;
min-inline-size: 0;
}
.fc-card__title {
margin: 0;
font-size: var(--fc-font-size-lg);
font-weight: var(--fc-font-weight-semibold);
line-height: var(--fc-line-height-snug);
}
.fc-card__subtitle {
margin: 0;
font-size: var(--fc-font-size-sm);
color: var(--fc-color-text-muted);
}
.fc-card__body {
display: flex;
flex-direction: column;
gap: var(--fc-space-4);
flex: 1;
padding-block: var(--fc-space-5);
font-size: var(--fc-font-size-sm);
color: var(--fc-color-text-muted);
}
/* Base prose rhythm would double the gap the body already sets. */
.fc-card__body > * {
margin-block: 0;
}
.fc-card__footer {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--fc-space-2);
padding-block: var(--fc-space-3) var(--fc-space-5);
}
/* A footer separated by a rule, for cards whose actions are a distinct zone. */
.fc-card__footer--divided {
border-block-start: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
padding-block-start: var(--fc-space-4);
}
/* ---- Container-driven layout ----------------------------------------- */
/*
* Optional wrapper that puts the media beside the content once the card is
* wide enough to afford it. Below that, it stacks. The threshold is the
* card's own width, so a "horizontal" card inside a narrow sidebar correctly
* stays vertical.
*/
.fc-card__layout {
display: grid;
grid-template-columns: 1fr;
}
@container fc-card (inline-size >= 34rem) {
.fc-card__layout {
grid-template-columns: minmax(12rem, 2fr) 3fr;
}
.fc-card__layout > .fc-card__media {
block-size: 100%;
aspect-ratio: auto;
}
}
/* Tight cards: less padding, so the content keeps its measure. */
@container fc-card (inline-size < 18rem) {
.fc-card__header,
.fc-card__body,
.fc-card__footer {
padding-inline: var(--fc-space-3);
}
.fc-card__header {
padding-block-start: var(--fc-space-3);
}
.fc-card__title {
font-size: var(--fc-font-size-md);
}
}
/* ---- Interactive card ------------------------------------------------ */
/*
* The whole card is clickable, but only one element is the link: the title.
* Its ::after stretches over the card, so the hit area is large while the
* accessible name is just the title -- not the entire card's text read as one
* run-on link. Other controls inside the card sit above the overlay.
*/
.fc-card__link {
color: inherit;
text-decoration: none;
}
.fc-card__link::after {
content: "";
position: absolute;
inset: 0;
z-index: var(--fc-z-base);
}
/*
* Focus stays on the title's own ring rather than moving to the card: the
* ring then marks exactly what has focus, and what Enter will follow.
*/
.fc-card:has(.fc-card__link:focus-visible) {
border-color: var(--fc-color-accent-border);
}
.fc-card:has(.fc-card__link):hover {
border-color: var(--fc-color-border-default);
box-shadow: var(--fc-shadow-overlay);
}
.fc-card:has(.fc-card__link):active {
translate: 0 var(--fc-space-px);
box-shadow: var(--fc-shadow-raised);
}
.fc-card__footer :is(a, button) {
position: relative;
z-index: var(--fc-z-raised);
}
/*
* Selected, for cards used as large radio or checkbox options. Driven by the
* state of a real input inside the card, so the visual and the form value
* cannot disagree. A selected option also matches :checked, so the selector
* must name the input: an ordinary card containing a select is not selected.
*/
.fc-card:has(input:checked) {
border-color: var(--fc-color-accent-border);
background-color: var(--fc-color-accent-bg-subtle);
}
.fc-card:has(:disabled) {
opacity: 1;
cursor: not-allowed;
}
/* ---- Forced colors --------------------------------------------------- */
@media (forced-colors: active) {
.fc-card {
border-color: CanvasText;
}
.fc-card:has(input:checked) {
border-color: Highlight;
border-width: var(--fc-border-width-thick);
}
}
}
/*!
* future-css -- badge and tag
*
* A badge is a static label: a status, a count, a category. It is not
* interactive and is not focusable. A tag is the interactive cousin: it can be
* toggled (a filter chip, `aria-pressed`) or removed (an input token with its
* own remove button).
*
* Tone is never the only signal. A badge that says "Failed" in red also says
* "Failed"; the colour is reinforcement for sighted users, not the message.
*/
@layer fc.components {
.fc-badge {
display: inline-flex;
align-items: center;
gap: var(--fc-space-1);
max-inline-size: 100%;
padding-block: var(--fc-space-0-5);
padding-inline: var(--fc-space-2);
background-color: var(--fc-color-bg-muted);
color: var(--fc-color-text-muted);
border: var(--fc-border-width-thin) solid transparent;
border-radius: var(--fc-radius-full);
font-size: var(--fc-font-size-xs);
font-weight: var(--fc-font-weight-medium);
line-height: var(--fc-line-height-snug);
white-space: nowrap;
vertical-align: middle;
}
.fc-badge--accent {
background-color: var(--fc-color-accent-bg-subtle);
color: var(--fc-color-accent-text);
}
.fc-badge--success {
background-color: var(--fc-color-success-bg-subtle);
color: var(--fc-color-success-text);
}
.fc-badge--warning {
background-color: var(--fc-color-warning-bg-subtle);
color: var(--fc-color-warning-text);
}
.fc-badge--danger {
background-color: var(--fc-color-danger-bg-subtle);
color: var(--fc-color-danger-text);
}
.fc-badge--info {
background-color: var(--fc-color-info-bg-subtle);
color: var(--fc-color-info-text);
}
/* The one solid tone with a checked foreground pair (text-on-accent). */
.fc-badge--solid {
background-color: var(--fc-color-accent-bg);
color: var(--fc-color-text-on-accent);
}
/* Outlined: for dense tables, where a filled pill per row is visual noise. */
.fc-badge--outline {
background-color: transparent;
border-color: currentColor;
}
.fc-badge--square {
border-radius: var(--fc-radius-sm);
}
/* Counts: tabular figures so a changing number does not jiggle its neighbours. */
.fc-badge--count {
justify-content: center;
min-inline-size: 1.25rem;
padding-inline: var(--fc-space-1-5);
font-variant-numeric: tabular-nums;
}
/*
* A status dot. Decorative: the badge text carries the meaning, so the dot is
* aria-hidden in the markup.
*/
.fc-badge__dot {
flex-shrink: 0;
inline-size: 0.375rem;
block-size: 0.375rem;
border-radius: var(--fc-radius-full);
background-color: currentColor;
}
.fc-badge__label {
overflow: hidden;
text-overflow: ellipsis;
}
/* ---- Keyboard key ---------------------------------------------------- */
/*
* Keyboard shortcuts, as in "Press <kbd>Enter</kbd> to send". The raised edge
* is a border, not a shadow, so it survives forced colours.
*/
.fc-kbd {
display: inline-flex;
align-items: center;
justify-content: center;
min-inline-size: 1.5em;
padding-inline: 0.35em;
background-color: var(--fc-color-bg-surface);
color: var(--fc-color-text-muted);
border: var(--fc-border-width-thin) solid var(--fc-color-border-default);
border-block-end-width: var(--fc-border-width-thick);
border-radius: var(--fc-radius-sm);
font-family: var(--fc-font-family-mono);
font-size: 0.8em;
line-height: 1.6;
}
/* ---- Tag ------------------------------------------------------------- */
.fc-tag {
display: inline-flex;
align-items: center;
gap: var(--fc-space-1);
min-block-size: 2rem;
max-inline-size: 100%;
padding-inline: var(--fc-space-3);
background-color: var(--fc-color-bg-surface);
color: var(--fc-color-text-default);
border: var(--fc-border-width-thin) solid var(--fc-color-border-default);
border-radius: var(--fc-radius-full);
font-family: inherit;
font-size: var(--fc-font-size-sm);
line-height: var(--fc-line-height-snug);
white-space: nowrap;
transition:
background-color var(--fc-duration-fast) var(--fc-easing-standard),
border-color var(--fc-duration-fast) var(--fc-easing-standard),
color var(--fc-duration-fast) var(--fc-easing-standard);
}
/* Only a tag that is itself a button reacts to the pointer. */
button.fc-tag {
cursor: pointer;
}
button.fc-tag:hover {
background-color: var(--fc-color-bg-muted);
border-color: var(--fc-color-border-strong);
}
/*
* A toggled filter chip. aria-pressed is the state, and the only one: there is
* no `is-selected` class to fall out of step with what a screen reader hears.
*/
.fc-tag[aria-pressed="true"] {
background-color: var(--fc-color-selected-bg);
color: var(--fc-color-selected-fg);
border-color: var(--fc-color-accent-border);
}
.fc-tag[aria-pressed="true"]::before {
content: "\2713" / "";
font-weight: var(--fc-font-weight-bold);
}
.fc-tag:disabled,
.fc-tag[aria-disabled="true"] {
color: var(--fc-color-disabled-fg);
border-color: var(--fc-color-disabled-border);
cursor: not-allowed;
}
.fc-tag__label {
overflow: hidden;
text-overflow: ellipsis;
}
/*
* The remove control is its own button with its own name ("Remove Rust"), not
* a click handler on the tag: a tag that removes itself when clicked anywhere
* is impossible to inspect and easy to trigger by accident.
*/
.fc-tag__remove {
display: inline-flex;
align-items: center;
justify-content: center;
inline-size: var(--fc-size-target-min);
block-size: var(--fc-size-target-min);
margin-inline-end: calc(var(--fc-space-2) * -1);
padding: 0;
background-color: transparent;
color: var(--fc-color-text-muted);
border: 0;
border-radius: var(--fc-radius-full);
font-size: var(--fc-font-size-md);
line-height: 1;
cursor: pointer;
}
.fc-tag__remove:hover {
background-color: var(--fc-color-bg-muted);
color: var(--fc-color-text-default);
}
.fc-tag__remove:active {
background-color: var(--fc-color-border-subtle);
}
/* ---- Forced colors --------------------------------------------------- */
@media (forced-colors: active) {
.fc-badge {
border-color: CanvasText;
}
.fc-tag[aria-pressed="true"] {
border-color: Highlight;
border-width: var(--fc-border-width-thick);
}
}
}
/*!
* future-css -- alert / callout
*
* An inline message about the page or a part of it. The status family (info,
* success, warning, danger) shares one anatomy:
*
* <div class="fc-alert fc-alert--warning" role="status">
* <span class="fc-alert__icon" aria-hidden="true">!</span>
* <div class="fc-alert__content">
* <p class="fc-alert__title">Quota nearly used</p>
* <p class="fc-alert__body">You have 3 requests left today.</p>
* </div>
* </div>
*
* The role is the consumer's decision and is not implied by the class: a
* callout present at page load needs no role at all, a message inserted after an
* action wants `role="status"`, and only something that interrupts -- a failed
* save -- wants `role="alert"`. Defaulting to `alert` would make every callout
* on a page interrupt the screen reader on load.
*/
@layer fc.components {
.fc-alert {
/*
* The tone is carried in two custom properties, so each variant is two
* declarations and the layout below is written once.
*/
--fc-alert-fg: var(--fc-color-text-default);
--fc-alert-bg: var(--fc-color-bg-subtle);
--fc-alert-edge: var(--fc-color-border-strong);
display: grid;
grid-template-columns: auto 1fr auto;
align-items: start;
gap: var(--fc-space-3);
padding: var(--fc-space-4);
background-color: var(--fc-alert-bg);
color: var(--fc-alert-fg);
border: var(--fc-border-width-thin) solid color-mix(in oklch, var(--fc-alert-edge) 35%, transparent);
border-inline-start: var(--fc-space-1) solid var(--fc-alert-edge);
border-radius: var(--fc-radius-lg);
}
.fc-alert--info {
--fc-alert-fg: var(--fc-color-info-text);
--fc-alert-bg: var(--fc-color-info-bg-subtle);
--fc-alert-edge: var(--fc-color-info-border);
}
.fc-alert--success {
--fc-alert-fg: var(--fc-color-success-text);
--fc-alert-bg: var(--fc-color-success-bg-subtle);
--fc-alert-edge: var(--fc-color-success-border);
}
.fc-alert--warning {
--fc-alert-fg: var(--fc-color-warning-text);
--fc-alert-bg: var(--fc-color-warning-bg-subtle);
--fc-alert-edge: var(--fc-color-warning-border);
}
.fc-alert--danger {
--fc-alert-fg: var(--fc-color-danger-text);
--fc-alert-bg: var(--fc-color-danger-bg-subtle);
--fc-alert-edge: var(--fc-color-danger-border);
}
/* No icon: the content takes the first column. */
.fc-alert:not(:has(.fc-alert__icon)) {
grid-template-columns: 1fr auto;
}
.fc-alert__icon {
display: inline-flex;
align-items: center;
justify-content: center;
inline-size: 1.25rem;
block-size: 1lh;
font-weight: var(--fc-font-weight-bold);
}
.fc-alert__content {
display: flex;
flex-direction: column;
gap: var(--fc-space-1);
min-inline-size: 0;
}
.fc-alert__title {
margin: 0;
font-size: var(--fc-font-size-sm);
font-weight: var(--fc-font-weight-semibold);
}
.fc-alert__body {
margin: 0;
font-size: var(--fc-font-size-sm);
}
.fc-alert__actions {
display: flex;
flex-wrap: wrap;
gap: var(--fc-space-2);
margin-block-start: var(--fc-space-2);
}
.fc-alert__dismiss {
display: inline-flex;
align-items: center;
justify-content: center;
inline-size: var(--fc-size-target-min);
block-size: var(--fc-size-target-min);
margin-block-start: calc(var(--fc-space-0-5) * -1);
padding: 0;
background-color: transparent;
color: inherit;
border: 0;
border-radius: var(--fc-radius-md);
cursor: pointer;
}
.fc-alert__dismiss:hover {
background-color: color-mix(in oklch, currentColor 10%, transparent);
}
/* A banner spans its container edge to edge, so it drops the rounded box. */
.fc-alert--banner {
border-radius: 0;
border-inline-width: 0;
border-block-start-width: 0;
border-block-end: var(--fc-border-width-thin) solid var(--fc-alert-edge);
}
/* ---- Forced colors --------------------------------------------------- */
/*
* The tint is discarded, so the thick edge is restated as a system colour.
* The title text and icon carry the tone from here on.
*/
@media (forced-colors: active) {
.fc-alert {
border-color: CanvasText;
}
}
}
/*!
* future-css -- dialog and sheet
*
* Built on the native `<dialog>` element, opened with `showModal()`. That one
* call provides what hand-rolled modals spend hundreds of lines approximating
* and still get wrong: the top layer (no z-index war), an inert background (no
* focus trap to write), Escape to close, focus moved in on open and returned on
* close, and a `::backdrop`.
*
* <button type="button" commandfor="confirm" command="show-modal">Delete</button>
* <dialog class="fc-dialog" id="confirm" aria-labelledby="confirm-title" closedby="any">
* <header class="fc-dialog__header">
* <h2 class="fc-dialog__title" id="confirm-title">Delete this chat?</h2>
* </header>
* <div class="fc-dialog__body">…</div>
* <footer class="fc-dialog__footer">
* <button commandfor="confirm" command="close">Cancel</button>
* …
* </footer>
* </dialog>
*
* `commandfor`/`command` open and close it without a line of script where they
* are supported; elsewhere, `dialog.showModal()` and `dialog.close()` are the
* whole API. `closedby="any"` adds light dismiss on a backdrop click.
*
* Motion uses `@starting-style` and discrete transitions of `display` and
* `overlay`, so the dialog animates in AND out with no JavaScript. Browsers
* that lack either simply show and hide it instantly, which is the correct
* fallback.
*/
@layer fc.components {
.fc-dialog {
--fc-dialog-size: 32rem;
flex-direction: column;
inline-size: min(100% - 2 * var(--fc-space-4), var(--fc-dialog-size));
max-inline-size: none;
max-block-size: min(100dvh - 2 * var(--fc-space-4), 48rem);
padding: 0;
background-color: var(--fc-color-bg-raised);
color: var(--fc-color-text-default);
border: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
border-radius: var(--fc-radius-2xl);
box-shadow: var(--fc-shadow-modal);
overflow: hidden;
opacity: 0;
scale: 0.96;
transition:
opacity var(--fc-duration-normal) var(--fc-easing-decelerate),
scale var(--fc-duration-normal) var(--fc-easing-decelerate),
translate var(--fc-duration-slow) var(--fc-easing-decelerate),
overlay var(--fc-duration-normal) allow-discrete,
display var(--fc-duration-normal) allow-discrete;
}
/*
* `display` is only set while open. Setting it unconditionally would override
* the UA's `dialog:not([open]) { display: none }` and show every closed
* dialog on the page.
*/
.fc-dialog[open] {
display: flex;
opacity: 1;
scale: 1;
}
@starting-style {
.fc-dialog[open] {
opacity: 0;
scale: 0.96;
}
}
/*
* The backdrop dims the page in both schemes. light-dark() picks a dark wash
* in each -- bg-inverse is near-black in light mode and bg-sunken is black in
* dark mode -- so the backdrop never turns into a white fog over a dark page.
*/
.fc-dialog::backdrop {
background-color: color-mix(
in oklch,
light-dark(var(--fc-color-bg-inverse), var(--fc-color-bg-sunken)) 50%,
transparent
);
backdrop-filter: blur(var(--fc-space-0-5));
opacity: 0;
transition:
opacity var(--fc-duration-normal) var(--fc-easing-standard),
overlay var(--fc-duration-normal) allow-discrete,
display var(--fc-duration-normal) allow-discrete;
}
.fc-dialog[open]::backdrop {
opacity: 1;
}
@starting-style {
.fc-dialog[open]::backdrop {
opacity: 0;
}
}
/* ---- Parts ----------------------------------------------------------- */
.fc-dialog__header {
display: flex;
align-items: start;
gap: var(--fc-space-3);
padding-block: var(--fc-space-5) var(--fc-space-2);
padding-inline: var(--fc-space-6);
}
.fc-dialog__title {
margin: 0;
flex: 1;
min-inline-size: 0;
font-size: var(--fc-font-size-lg);
font-weight: var(--fc-font-weight-semibold);
line-height: var(--fc-line-height-snug);
}
.fc-dialog__description {
margin: 0;
margin-block-start: var(--fc-space-1);
font-size: var(--fc-font-size-sm);
color: var(--fc-color-text-muted);
}
.fc-dialog__close {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
inline-size: 2rem;
block-size: 2rem;
margin-block-start: calc(var(--fc-space-1) * -1);
margin-inline-end: calc(var(--fc-space-2) * -1);
padding: 0;
background-color: transparent;
color: var(--fc-color-text-muted);
border: 0;
border-radius: var(--fc-radius-md);
font-size: var(--fc-font-size-lg);
cursor: pointer;
}
.fc-dialog__close:hover {
background-color: var(--fc-color-bg-muted);
color: var(--fc-color-text-default);
}
/*
* Only the body scrolls, so the title and the actions stay visible however
* long the content is. `overscroll-behavior` keeps a scroll that reaches the
* end from chaining to the page behind.
*/
.fc-dialog__body {
flex: 1;
min-block-size: 0;
overflow-y: auto;
overscroll-behavior: contain;
padding-block: var(--fc-space-2) var(--fc-space-4);
padding-inline: var(--fc-space-6);
font-size: var(--fc-font-size-sm);
color: var(--fc-color-text-muted);
}
.fc-dialog__body > :last-child {
margin-block-end: 0;
}
.fc-dialog__footer {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: var(--fc-space-2);
padding-block: var(--fc-space-4);
padding-inline: var(--fc-space-6);
background-color: var(--fc-color-bg-subtle);
border-block-start: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
}
/* ---- Sizes ----------------------------------------------------------- */
.fc-dialog--sm {
--fc-dialog-size: 24rem;
}
.fc-dialog--lg {
--fc-dialog-size: 48rem;
}
.fc-dialog--fullscreen {
--fc-dialog-size: 100%;
max-block-size: 100dvh;
block-size: 100dvh;
inline-size: 100%;
border-radius: 0;
}
/* ---- Sheet ----------------------------------------------------------- */
/*
* A side sheet docked to the inline-end edge: settings, a run inspector, the
* sources behind an answer. Same element, same focus behaviour, different
* geometry. `margin-inline-start: auto` docks it to the correct side in RTL
* with no extra rule; the slide direction is a physical translate and does
* need one.
*/
.fc-dialog--sheet {
--fc-dialog-size: 28rem;
--fc-sheet-from: 100%;
block-size: 100dvh;
max-block-size: 100dvh;
margin-block: 0;
margin-inline: auto 0;
border-radius: 0;
border-block-width: 0;
border-inline-end-width: 0;
translate: var(--fc-sheet-from) 0;
opacity: 1;
scale: 1;
}
.fc-dialog--sheet:dir(rtl) {
--fc-sheet-from: -100%;
}
.fc-dialog--sheet[open] {
translate: 0 0;
}
@starting-style {
.fc-dialog--sheet[open] {
translate: var(--fc-sheet-from) 0;
opacity: 1;
scale: 1;
}
}
/*
* A bottom sheet, docked to the block-end edge at every width: secondary
* actions, a share sheet, details for a selection. Opened with
* `showModal()` it is modal; with `show()` it is a standard sheet that
* leaves the page usable, which is why it is fixed rather than relying on
* the top layer to position it.
*/
.fc-dialog--bottom {
--fc-dialog-size: 40rem;
position: fixed;
inset-block: auto 0;
inset-inline: 0;
/* Edge to edge on a phone, a centred 40rem panel on wider screens. */
inline-size: min(100%, var(--fc-dialog-size));
margin-block: auto 0;
margin-inline: auto;
max-block-size: min(90dvh, 48rem);
padding-block-end: env(safe-area-inset-bottom);
border-end-start-radius: 0;
border-end-end-radius: 0;
border-block-end-width: 0;
translate: 0 100%;
opacity: 1;
scale: 1;
}
.fc-dialog--bottom[open] {
translate: 0 0;
}
@starting-style {
.fc-dialog--bottom[open] {
translate: 0 100%;
opacity: 1;
scale: 1;
}
}
/*
* The drag handle is decorative: it says "this is a sheet" but nothing here
* makes it draggable, and a sheet that could only be closed by dragging
* would shut out keyboard and switch users. Every sheet keeps a close
* button or `closedby="any"`.
*/
.fc-dialog__handle {
flex-shrink: 0;
inline-size: 2rem;
block-size: 0.25rem;
margin-block: var(--fc-space-2) 0;
margin-inline: auto;
background-color: var(--fc-color-border-strong);
border-radius: var(--fc-radius-full);
}
/* On a phone the sheet is the whole screen; a 28rem panel would not fit. */
@media (width < 36rem) {
.fc-dialog--sheet {
inline-size: 100%;
}
/* A centred dialog becomes a bottom sheet, within thumb reach. */
.fc-dialog:not(.fc-dialog--sheet, .fc-dialog--fullscreen, .fc-dialog--bottom) {
inline-size: 100%;
margin-block-end: 0;
border-end-start-radius: 0;
border-end-end-radius: 0;
}
}
/* ---- Forced colors --------------------------------------------------- */
/*
* The shadow and the backdrop wash are both suppressed, which would leave a
* borderless box floating on an identical canvas. The border restates the
* edge.
*/
@media (forced-colors: active) {
.fc-dialog {
border: var(--fc-border-width-thick) solid CanvasText;
}
.fc-dialog__footer {
border-block-start-color: CanvasText;
}
.fc-dialog__handle {
background-color: CanvasText;
}
}
/* A solid backdrop preserves separation when the user rejects translucency. */
@media (prefers-reduced-transparency: reduce) {
.fc-dialog::backdrop {
background-color: var(--fc-color-bg-inverse);
backdrop-filter: none;
}
}
}
/*!
* future-css -- popover and tooltip
*
* POPOVER. Non-modal floating content on the native popover API:
*
* <button popovertarget="filters">Filters</button>
* <div class="fc-popover" id="filters" popover>…</div>
*
* The platform provides the top layer, light dismiss, Escape, and returning
* focus to the invoker. Where CSS anchor positioning is supported, the popover
* sits against its invoker and flips when it runs out of room; elsewhere it
* falls back to the UA's centred placement, which is usable rather than broken.
*
* TOOLTIP. A short, supplementary label for a control:
*
* <span class="fc-tooltip">
* <button aria-describedby="tip-copy">Copy</button>
* <span class="fc-tooltip__bubble" role="tooltip" id="tip-copy">Copy to clipboard</span>
* </span>
*
* Shown on hover AND keyboard focus, never hover alone. Never the only place
* information lives: the bubble repeats or clarifies a name, so a user who
* never sees it loses nothing. The `tooltip` binding adds the one behaviour CSS
* cannot: Escape dismisses it without moving focus (WCAG 1.4.13).
*/
@layer fc.components {
.fc-popover {
inline-size: max-content;
max-inline-size: min(22rem, 100vi - 2 * var(--fc-space-4));
padding: var(--fc-space-4);
margin: var(--fc-space-1);
background-color: var(--fc-color-bg-raised);
color: var(--fc-color-text-default);
border: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
border-radius: var(--fc-radius-xl);
box-shadow: var(--fc-shadow-overlay);
font-size: var(--fc-font-size-sm);
/*
* Anchored below the invoker, aligned to its inline-start edge, flipping to
* the other side of either axis when there is no room. All logical, so the
* alignment mirrors in RTL.
*/
position-area: block-end span-inline-end;
position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
opacity: 0;
translate: 0 calc(var(--fc-space-1) * -1);
transition:
opacity var(--fc-duration-fast) var(--fc-easing-decelerate),
translate var(--fc-duration-fast) var(--fc-easing-decelerate),
overlay var(--fc-duration-fast) allow-discrete,
display var(--fc-duration-fast) allow-discrete;
}
.fc-popover:popover-open {
opacity: 1;
translate: 0 0;
}
@starting-style {
.fc-popover:popover-open {
opacity: 0;
translate: 0 calc(var(--fc-space-1) * -1);
}
}
.fc-popover__title {
margin: 0;
margin-block-end: var(--fc-space-2);
font-size: var(--fc-font-size-sm);
font-weight: var(--fc-font-weight-semibold);
}
.fc-popover__footer {
display: flex;
justify-content: flex-end;
gap: var(--fc-space-2);
margin-block-start: var(--fc-space-3);
}
/* ---- Tooltip --------------------------------------------------------- */
.fc-tooltip {
position: relative;
display: inline-flex;
}
.fc-tooltip__bubble {
position: absolute;
inset-block-end: calc(100% + var(--fc-space-1-5));
inset-inline-start: 50%;
z-index: var(--fc-z-popover);
inline-size: max-content;
max-inline-size: 16rem;
padding-block: var(--fc-space-1);
padding-inline: var(--fc-space-2);
background-color: var(--fc-color-bg-inverse);
color: var(--fc-color-text-inverse);
border-radius: var(--fc-radius-md);
font-size: var(--fc-font-size-xs);
font-weight: var(--fc-font-weight-medium);
line-height: var(--fc-line-height-snug);
text-align: center;
/*
* Hidden with visibility, not display: the bubble stays in the
* accessibility tree so aria-describedby can still read it when it is not
* shown. `translate` is physical, so RTL reverses it below.
*/
visibility: hidden;
opacity: 0;
translate: -50% var(--fc-space-1);
transition:
opacity var(--fc-duration-fast) var(--fc-easing-standard),
translate var(--fc-duration-fast) var(--fc-easing-standard),
visibility 0s linear var(--fc-duration-fast);
}
/*
* Bridges the gap between control and bubble. The bubble is a child of the
* wrapper, so while the pointer is over it the wrapper stays hovered -- which
* is what lets a user move onto the bubble to read it without it vanishing
* (WCAG 1.4.13, "hoverable"). Without the bridge, crossing the gap would end
* the hover.
*/
.fc-tooltip__bubble::before {
content: "";
position: absolute;
inset-inline: 0;
inset-block-start: 100%;
block-size: var(--fc-space-1-5);
}
.fc-tooltip__bubble:dir(rtl) {
translate: 50% var(--fc-space-1);
}
/*
* A short delay before showing, so the bubble does not flash while the
* pointer merely crosses the control. Keyboard focus shows it immediately:
* a keyboard user arrived on purpose.
*/
.fc-tooltip:hover .fc-tooltip__bubble {
visibility: visible;
opacity: 1;
translate: -50% 0;
transition-delay: 400ms, 400ms, 0s;
}
.fc-tooltip:has(:focus-visible) .fc-tooltip__bubble {
visibility: visible;
opacity: 1;
translate: -50% 0;
transition-delay: 0s;
}
.fc-tooltip:hover .fc-tooltip__bubble:dir(rtl),
.fc-tooltip:has(:focus-visible) .fc-tooltip__bubble:dir(rtl) {
translate: 50% 0;
}
/* Escape was pressed: hidden until the pointer leaves or focus moves. */
.fc-tooltip[data-fc-dismissed="true"] .fc-tooltip__bubble {
visibility: hidden;
opacity: 0;
}
/* Below the control, for controls at the top edge of a scroll container.
A bubble modifier rather than a wrapper one, so it stays a single class. */
.fc-tooltip__bubble--below {
inset-block-end: auto;
inset-block-start: calc(100% + var(--fc-space-1-5));
}
.fc-tooltip__bubble--below::before {
inset-block-start: auto;
inset-block-end: 100%;
}
/* ---- Forced colors --------------------------------------------------- */
@media (forced-colors: active) {
.fc-popover {
border: var(--fc-border-width-thick) solid CanvasText;
}
.fc-tooltip__bubble {
background-color: Canvas;
color: CanvasText;
border: var(--fc-border-width-thin) solid CanvasText;
}
}
}
/*!
* future-css -- toast
*
* Brief, non-blocking notifications stacked at the inline-end, block-end corner.
*
* <section class="fc-toaster" aria-label="Notifications">
* <ol class="fc-toaster__list" role="status" aria-live="polite"></ol>
* <ol class="fc-toaster__list" role="alert" aria-live="assertive"></ol>
* </section>
*
* Two live regions, created up front and kept empty until needed: a live region
* inserted at the same moment as its content is announced unreliably or not at
* all, and the politeness of a region cannot be changed per message. The
* `toast` binding owns both and routes each message to the right one.
*
* Toasts are for things the user can afford to miss. An error the user must act
* on belongs in an alert next to its cause; a toast saying "Saved" does not.
*/
@layer fc.components {
.fc-toaster {
position: fixed;
inset-block-end: var(--fc-space-4);
inset-inline-end: var(--fc-space-4);
z-index: var(--fc-z-toast);
display: flex;
flex-direction: column;
align-items: flex-end;
gap: var(--fc-space-2);
inline-size: min(24rem, 100% - 2 * var(--fc-space-4));
/* The region spans an area of the page; only the toasts themselves catch clicks. */
pointer-events: none;
}
.fc-toaster__list {
display: flex;
flex-direction: column;
gap: var(--fc-space-2);
inline-size: 100%;
margin: 0;
padding: 0;
list-style: none;
}
.fc-toaster__list:empty {
display: none;
}
/* A top-centred placement, for layouts whose block-end corner holds a composer. */
.fc-toaster--top {
inset-block-end: auto;
inset-block-start: var(--fc-space-4);
inset-inline: 0;
margin-inline: auto;
align-items: center;
}
.fc-toast {
--fc-toast-edge: var(--fc-color-border-strong);
display: grid;
grid-template-columns: auto 1fr auto;
align-items: start;
gap: var(--fc-space-3);
inline-size: 100%;
padding: var(--fc-space-3) var(--fc-space-4);
background-color: var(--fc-color-bg-raised);
color: var(--fc-color-text-default);
border: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
border-radius: var(--fc-radius-xl);
box-shadow: var(--fc-shadow-modal);
pointer-events: auto;
transition:
opacity var(--fc-duration-normal) var(--fc-easing-decelerate),
translate var(--fc-duration-normal) var(--fc-easing-decelerate),
scale var(--fc-duration-normal) var(--fc-easing-decelerate);
}
@starting-style {
.fc-toast {
opacity: 0;
translate: 0 var(--fc-space-4);
scale: 0.98;
}
}
/* Leaving: the binding sets this, waits for the transition, then removes the node. */
.fc-toast[data-fc-leaving="true"] {
opacity: 0;
scale: 0.96;
transition-timing-function: var(--fc-easing-accelerate);
}
.fc-toast:not(:has(.fc-toast__icon)) {
grid-template-columns: 1fr auto;
}
.fc-toast__icon {
display: inline-flex;
align-items: center;
justify-content: center;
inline-size: 1.25rem;
block-size: 1lh;
color: var(--fc-toast-edge);
font-weight: var(--fc-font-weight-bold);
}
.fc-toast--success {
--fc-toast-edge: var(--fc-color-success-border);
}
.fc-toast--warning {
--fc-toast-edge: var(--fc-color-warning-border);
}
.fc-toast--danger {
--fc-toast-edge: var(--fc-color-danger-border);
}
.fc-toast--info {
--fc-toast-edge: var(--fc-color-info-border);
}
.fc-toast__content {
display: flex;
flex-direction: column;
gap: var(--fc-space-0-5);
min-inline-size: 0;
}
.fc-toast__title {
margin: 0;
font-size: var(--fc-font-size-sm);
font-weight: var(--fc-font-weight-semibold);
}
.fc-toast__body {
margin: 0;
font-size: var(--fc-font-size-sm);
color: var(--fc-color-text-muted);
overflow-wrap: anywhere;
}
.fc-toast__action {
justify-self: start;
margin-block-start: var(--fc-space-1);
padding: 0;
background: none;
color: var(--fc-color-accent-text);
border: 0;
font: inherit;
font-size: var(--fc-font-size-sm);
font-weight: var(--fc-font-weight-semibold);
cursor: pointer;
}
.fc-toast__action:hover {
text-decoration: underline;
}
.fc-toast__close {
display: inline-flex;
align-items: center;
justify-content: center;
inline-size: var(--fc-size-target-min);
block-size: var(--fc-size-target-min);
padding: 0;
background-color: transparent;
color: var(--fc-color-text-muted);
border: 0;
border-radius: var(--fc-radius-md);
cursor: pointer;
}
.fc-toast__close:hover {
background-color: var(--fc-color-bg-muted);
color: var(--fc-color-text-default);
}
/* On a phone the stack spans the screen, so each toast is readable at a glance. */
@media (width < 36rem) {
.fc-toaster {
inset-inline: var(--fc-space-2);
inset-block-end: var(--fc-space-2);
inline-size: auto;
}
}
/* ---- Forced colors --------------------------------------------------- */
@media (forced-colors: active) {
.fc-toast {
border: var(--fc-border-width-thick) solid CanvasText;
}
}
}
/*!
* future-css -- tabs
*
* The APG tabs pattern: a `tablist` of `tab` buttons, each controlling one
* `tabpanel`. Selection is `aria-selected`, and only that -- the indicator, the
* colour, and the announced state all read from the one attribute.
*
* <div class="fc-tabs">
* <div class="fc-tabs__list" role="tablist" aria-label="Output">
* <button class="fc-tabs__tab" role="tab" aria-selected="true"
* aria-controls="p-preview" id="t-preview">Preview</button>
* <button class="fc-tabs__tab" role="tab" aria-selected="false"
* aria-controls="p-code" id="t-code" tabindex="-1">Code</button>
* </div>
* <div class="fc-tabs__panel" role="tabpanel" id="p-preview"
* aria-labelledby="t-preview" tabindex="0">…</div>
* <div class="fc-tabs__panel" role="tabpanel" id="p-code"
* aria-labelledby="t-code" tabindex="0" hidden>…</div>
* </div>
*
* The `tabs` binding supplies the keyboard contract (arrows, Home, End) and
* keeps exactly one tab in the tab order.
*/
@layer fc.components {
.fc-tabs {
display: flex;
flex-direction: column;
min-inline-size: 0;
}
.fc-tabs__list {
display: flex;
gap: var(--fc-space-1);
border-block-end: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
/*
* A tab strip wider than its container scrolls rather than wrapping: a
* wrapped strip reorders tabs visually between rows, which breaks the
* spatial model the arrow keys rely on.
*/
overflow-x: auto;
scrollbar-width: thin;
/* Room for the focus ring, which the scroll container would otherwise clip. */
padding: var(--fc-space-1) var(--fc-space-1) 0;
}
/*
* The tab reads its geometry from custom properties that the list variants
* set. A variant then restyles its tabs without a descendant selector, and a
* consumer can tune one property without overriding the whole rule.
*/
.fc-tabs__tab {
position: relative;
display: inline-flex;
align-items: center;
gap: var(--fc-space-2);
flex-shrink: 0;
min-block-size: var(--fc-tabs-tab-size, var(--fc-size-target-comfortable));
padding-inline: var(--fc-space-3);
background-color: transparent;
color: var(--fc-color-text-muted);
border: 0;
/* The indicator is a border, not a shadow, so forced colours keep it. */
border-block-end: var(--fc-tabs-indicator, var(--fc-border-width-thick)) solid transparent;
border-radius: var(--fc-tabs-tab-radius, var(--fc-radius-md) var(--fc-radius-md) 0 0);
margin-block-end: var(--fc-tabs-overlap, calc(var(--fc-border-width-thin) * -1));
font: inherit;
font-size: var(--fc-font-size-sm);
font-weight: var(--fc-font-weight-medium);
white-space: nowrap;
cursor: pointer;
transition:
color var(--fc-duration-fast) var(--fc-easing-standard),
background-color var(--fc-duration-fast) var(--fc-easing-standard),
border-color var(--fc-duration-normal) var(--fc-easing-standard),
box-shadow var(--fc-duration-fast) var(--fc-easing-standard);
}
.fc-tabs__tab:hover {
color: var(--fc-color-text-default);
background-color: var(--fc-tabs-hover-bg, var(--fc-color-bg-subtle));
}
.fc-tabs__tab[aria-selected="true"] {
color: var(--fc-color-selected-fg);
background-color: var(--fc-tabs-selected-bg, var(--fc-color-selected-bg));
border-block-end-color: var(--fc-color-accent-border);
box-shadow: var(--fc-tabs-selected-shadow, none);
}
.fc-tabs__tab:disabled,
.fc-tabs__tab[aria-disabled="true"] {
color: var(--fc-color-disabled-fg);
background-color: transparent;
cursor: not-allowed;
}
/* A count or status inside a tab: "Problems 3". */
.fc-tabs__count {
min-inline-size: 1.25rem;
padding-inline: var(--fc-space-1-5);
background-color: var(--fc-color-bg-muted);
color: var(--fc-color-text-muted);
border-radius: var(--fc-radius-full);
font-size: var(--fc-font-size-xs);
font-variant-numeric: tabular-nums;
text-align: center;
}
.fc-tabs__panel {
padding-block: var(--fc-space-4);
}
/* Panels are focusable so a panel with no focusable content is still reachable. */
.fc-tabs__panel:focus-visible {
outline-offset: var(--fc-space-1);
}
/* ---- Segmented ------------------------------------------------------- */
/*
* Segmented control: the same pattern drawn as a pill group, for switching
* views of one thing (Preview / Code, Day / Week). The modifier goes on the
* list, and reaches the tabs only through the properties above.
*/
.fc-tabs__list--segmented {
--fc-tabs-tab-size: 2rem;
--fc-tabs-indicator: 0;
--fc-tabs-tab-radius: var(--fc-radius-md);
--fc-tabs-overlap: 0;
--fc-tabs-hover-bg: var(--fc-color-bg-subtle);
--fc-tabs-selected-bg: var(--fc-color-selected-bg);
--fc-tabs-selected-shadow: var(--fc-shadow-raised);
display: inline-flex;
align-self: flex-start;
max-inline-size: 100%;
gap: var(--fc-space-0-5);
padding: var(--fc-space-0-5);
background-color: var(--fc-color-bg-muted);
border: 0;
border-radius: var(--fc-radius-lg);
}
/* ---- Vertical -------------------------------------------------------- */
/*
* Vertical tabs are driven by aria-orientation on the list -- the attribute a
* screen reader already uses to decide which arrow keys apply -- so the
* layout and the announced orientation cannot disagree. The indicator moves
* to the inline-end edge, which mirrors in RTL with no extra rule.
*/
.fc-tabs:has(> [aria-orientation="vertical"]) {
flex-direction: row;
}
.fc-tabs__list[aria-orientation="vertical"] {
--fc-tabs-indicator: 0;
--fc-tabs-overlap: 0;
--fc-tabs-tab-radius: var(--fc-radius-md) 0 0 var(--fc-radius-md);
flex-direction: column;
flex-shrink: 0;
border-block-end: 0;
border-inline-end: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
overflow-x: visible;
padding-block: var(--fc-space-1);
padding-inline: var(--fc-space-1) 0;
}
/*
* The radius shorthand is physical (it lists corners clockwise from the top
* left), so the rounded side has to be flipped explicitly for RTL.
*/
.fc-tabs__list[aria-orientation="vertical"]:dir(rtl) {
--fc-tabs-tab-radius: 0 var(--fc-radius-md) var(--fc-radius-md) 0;
}
.fc-tabs__list[aria-orientation="vertical"] .fc-tabs__tab {
justify-content: flex-start;
margin-inline-end: calc(var(--fc-border-width-thin) * -1);
border-inline-end: var(--fc-border-width-thick) solid transparent;
}
.fc-tabs__list[aria-orientation="vertical"] .fc-tabs__tab[aria-selected="true"] {
border-inline-end-color: var(--fc-color-accent-border);
}
.fc-tabs:has(> [aria-orientation="vertical"]) .fc-tabs__panel {
flex: 1;
min-inline-size: 0;
padding-block: 0;
padding-inline: var(--fc-space-5);
}
/* ---- Forced colors --------------------------------------------------- */
@media (forced-colors: active) {
.fc-tabs__tab[aria-selected="true"] {
border-block-end-color: Highlight;
}
.fc-tabs__list[aria-orientation="vertical"] .fc-tabs__tab[aria-selected="true"] {
border-inline-end-color: Highlight;
}
/* The segmented pill's selected fill is discarded; an outline replaces it. */
.fc-tabs__list--segmented [aria-selected="true"] {
outline: var(--fc-border-width-thin) solid Highlight;
}
}
}
/*!
* future-css -- breadcrumb
*
* <nav class="fc-breadcrumb" aria-label="Breadcrumb">
* <ol class="fc-breadcrumb__list">
* <li class="fc-breadcrumb__item"><a class="fc-breadcrumb__link" href="/">Projects</a></li>
* <li class="fc-breadcrumb__item"><a class="fc-breadcrumb__link" href="/p/1">future-css</a></li>
* <li class="fc-breadcrumb__item"><a class="fc-breadcrumb__link" aria-current="page">Tokens</a></li>
* </ol>
* </nav>
*
* An ordered list, because the order is the information. Separators are
* generated content with empty alternative text, so a screen reader reads
* "Projects, future-css, Tokens" rather than "Projects slash future-css slash".
*/
@layer fc.components {
.fc-breadcrumb {
min-inline-size: 0;
}
.fc-breadcrumb__list {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--fc-space-1);
margin: 0;
padding: 0;
list-style: none;
font-size: var(--fc-font-size-sm);
}
.fc-breadcrumb__item {
display: inline-flex;
align-items: center;
gap: var(--fc-space-1);
min-inline-size: 0;
}
/*
* A slash, not a chevron: a chevron points a direction and would need
* mirroring in RTL, while a slash reads correctly both ways.
*/
.fc-breadcrumb__item + .fc-breadcrumb__item::before {
content: "/" / "";
color: var(--fc-color-text-subtle);
}
.fc-breadcrumb__link {
display: inline-block;
/* Long names truncate rather than pushing the trail onto three lines. */
max-inline-size: 16rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-block: var(--fc-space-0-5);
padding-inline: var(--fc-space-1);
border-radius: var(--fc-radius-sm);
color: var(--fc-color-text-muted);
text-decoration: none;
}
.fc-breadcrumb__link:hover {
color: var(--fc-color-text-default);
text-decoration: underline;
text-underline-offset: 0.2em;
}
/* The current page is a location, not a destination: no link styling. */
.fc-breadcrumb__link[aria-current="page"] {
color: var(--fc-color-text-default);
font-weight: var(--fc-font-weight-medium);
text-decoration: none;
cursor: default;
}
@media (forced-colors: active) {
.fc-breadcrumb__link[aria-current="page"] {
color: CanvasText;
}
}
}
/*!
* future-css -- pagination
*
* <nav class="fc-pagination" aria-label="Pagination">
* <ul class="fc-pagination__list">
* <li><a class="fc-pagination__link fc-pagination__link--prev" aria-disabled="true">Previous</a></li>
* <li><a class="fc-pagination__link" href="?page=1" aria-current="page">1</a></li>
* <li><a class="fc-pagination__link" href="?page=2">2</a></li>
* <li><span class="fc-pagination__gap" aria-hidden="true">…</span></li>
* <li><a class="fc-pagination__link fc-pagination__link--next" href="?page=2">Next</a></li>
* </ul>
* </nav>
*
* Links, not buttons: every page has a URL, so it can be opened in a new tab,
* bookmarked, and shared. The disabled "Previous" on page one is an anchor with
* no href and aria-disabled, which keeps its position stable instead of making
* the control row jump when it appears.
*/
@layer fc.components {
.fc-pagination {
min-inline-size: 0;
}
.fc-pagination__list {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--fc-space-1);
margin: 0;
padding: 0;
list-style: none;
}
.fc-pagination__link,
.fc-pagination__gap {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--fc-space-1);
min-inline-size: var(--fc-size-target-comfortable);
min-block-size: var(--fc-size-target-comfortable);
padding-inline: var(--fc-space-2);
font-size: var(--fc-font-size-sm);
font-variant-numeric: tabular-nums;
}
.fc-pagination__link {
color: var(--fc-color-text-default);
border: var(--fc-border-width-thin) solid transparent;
border-radius: var(--fc-radius-md);
text-decoration: none;
transition: background-color var(--fc-duration-fast) var(--fc-easing-standard);
}
.fc-pagination__link:hover {
background-color: var(--fc-color-bg-muted);
}
.fc-pagination__link:active {
background-color: var(--fc-color-bg-subtle);
}
.fc-pagination__link[aria-current="page"] {
background-color: var(--fc-color-accent-bg-subtle);
color: var(--fc-color-accent-text);
border-color: var(--fc-color-accent-border);
font-weight: var(--fc-font-weight-semibold);
}
.fc-pagination__link[aria-disabled="true"] {
color: var(--fc-color-disabled-fg);
background-color: transparent;
pointer-events: none;
}
.fc-pagination__gap {
color: var(--fc-color-text-subtle);
}
/*
* Arrows are generated so they mirror with the direction. In RTL "Previous"
* points right, because the previous page is to the reader's right.
*/
.fc-pagination__link--prev::before {
content: "\2190" / "";
}
.fc-pagination__link--next::after {
content: "\2192" / "";
}
.fc-pagination__link--prev:dir(rtl)::before {
content: "\2192" / "";
}
.fc-pagination__link--next:dir(rtl)::after {
content: "\2190" / "";
}
@media (forced-colors: active) {
.fc-pagination__link[aria-current="page"] {
border-color: Highlight;
border-width: var(--fc-border-width-thick);
}
.fc-pagination__link[aria-disabled="true"] {
color: GrayText;
}
}
}
/*!
* future-css -- spinners
*
* "Working, extent unknown." One block, several drawings, chosen by what the
* work is rather than by taste:
*
* ring (default) generic loading: a page, a request, a save
* --arc the same, drawn as a tapering arc; a softer default
* --dots someone is composing a reply: the typing indicator
* --pulse something is live and listening: a connected agent, a recording
* --bars a stream is flowing: tokens arriving, audio playing
* --orbit background work with several parts: indexing, syncing
* --sparkle a model is thinking: the AI "working on it" mark
*
* <span role="status">
* <span class="fc-spinner fc-spinner--dots" aria-hidden="true">
* <span></span><span></span><span></span>
* </span>
* <span class="fc-u-visually-hidden">Assistant is typing</span>
* </span>
*
* --dots takes three child spans and --bars takes four; the others are a
* single empty element. A spinner is always aria-hidden: what is happening is
* said in words, by the status text beside it, never by the animation.
*
* Every drawing is built from `currentColor`, so `color` restyles any of them,
* and sized from one property, `--fc-spinner-size`.
*
* REDUCED MOTION. A spinner is the only cue that work is in progress, so it
* never stops outright -- a frozen spinner reads as a hung page. Rotating ones
* slow to a third of their speed; the ones that bounce, scale, or ripple swap
* to an opacity-only fade, which carries "still working" without movement.
* Spinners are excluded from the reset's reduced-motion block by name, because
* that block is `!important` in the earliest layer and nothing later can
* loosen it.
*/
@layer fc.components {
.fc-spinner {
--fc-spinner-size: 1.25rem;
--fc-spinner-stroke: max(var(--fc-border-width-thick), calc(var(--fc-spinner-size) / 10));
/* 1 normally, 3 under reduced motion: every duration is multiplied by it. */
--fc-spinner-slow: 1;
position: relative;
display: inline-block;
flex-shrink: 0;
inline-size: var(--fc-spinner-size);
block-size: var(--fc-spinner-size);
vertical-align: middle;
color: var(--fc-color-accent-text);
/* The default drawing: a ring with one solid quarter. */
border: var(--fc-spinner-stroke) solid color-mix(in oklch, currentColor 20%, transparent);
border-block-start-color: currentColor;
border-radius: var(--fc-radius-full);
animation: fc-spinner-rotate calc(0.7s * var(--fc-spinner-slow)) linear infinite;
}
/* ---- Sizes and tones ------------------------------------------------- */
.fc-spinner--xs {
--fc-spinner-size: 0.75rem;
}
.fc-spinner--sm {
--fc-spinner-size: 1rem;
}
.fc-spinner--lg {
--fc-spinner-size: 2rem;
}
.fc-spinner--xl {
--fc-spinner-size: 3rem;
}
.fc-spinner--muted {
color: var(--fc-color-text-muted);
}
/* Takes the colour of the text around it, e.g. inside a button or a badge. */
.fc-spinner--current {
color: inherit;
}
/* ---- Arc ------------------------------------------------------------- */
/*
* A conic gradient fading from transparent to the full colour, cut to a ring
* by a radial mask. The mask uses currentColor rather than a colour literal:
* only its alpha matters.
*/
.fc-spinner--arc {
border: 0;
background-image: conic-gradient(transparent 10%, currentColor);
mask-image: radial-gradient(
farthest-side,
transparent calc(100% - var(--fc-spinner-stroke) - 0.03rem),
currentColor calc(100% - var(--fc-spinner-stroke))
);
animation-duration: calc(0.9s * var(--fc-spinner-slow));
}
/* ---- Dots: the typing indicator --------------------------------------- */
.fc-spinner--dots {
display: inline-flex;
align-items: center;
justify-content: space-between;
inline-size: calc(var(--fc-spinner-size) * 1.6);
border: 0;
border-radius: 0;
animation: none;
}
.fc-spinner--dots > * {
inline-size: calc(var(--fc-spinner-size) * 0.32);
block-size: calc(var(--fc-spinner-size) * 0.32);
background-color: currentColor;
border-radius: var(--fc-radius-full);
animation: fc-spinner-bounce calc(1.2s * var(--fc-spinner-slow)) var(--fc-easing-standard) infinite;
}
/* Staggered, so the dots read as a wave moving in the reading direction. */
.fc-spinner--dots > :nth-child(2) {
animation-delay: calc(0.15s * var(--fc-spinner-slow));
}
.fc-spinner--dots > :nth-child(3) {
animation-delay: calc(0.3s * var(--fc-spinner-slow));
}
/* ---- Pulse ----------------------------------------------------------- */
/*
* A dot with a ripple. The ripple is a spread shadow that grows and fades,
* so it needs no extra element and never affects layout.
*/
.fc-spinner--pulse {
border: 0;
background-color: currentColor;
scale: 0.5;
animation: fc-spinner-pulse calc(1.6s * var(--fc-spinner-slow)) var(--fc-easing-decelerate) infinite;
}
/* ---- Bars: a stream flowing ------------------------------------------ */
.fc-spinner--bars {
display: inline-flex;
align-items: center;
justify-content: space-between;
border: 0;
border-radius: 0;
animation: none;
}
.fc-spinner--bars > * {
inline-size: 18%;
block-size: 100%;
background-color: currentColor;
border-radius: var(--fc-radius-full);
scale: 1 0.35;
animation: fc-spinner-bars calc(1s * var(--fc-spinner-slow)) ease-in-out infinite;
}
.fc-spinner--bars > :nth-child(2) {
animation-delay: calc(-0.8s * var(--fc-spinner-slow));
}
.fc-spinner--bars > :nth-child(3) {
animation-delay: calc(-0.6s * var(--fc-spinner-slow));
}
.fc-spinner--bars > :nth-child(4) {
animation-delay: calc(-0.4s * var(--fc-spinner-slow));
}
/* ---- Orbit ----------------------------------------------------------- */
/*
* Two satellites, one full strength and one faint, drawn as radial gradients
* on the element itself and turned with it. No pseudo-elements, so nothing
* here depends on the reset's handling of ::before and ::after.
*/
.fc-spinner--orbit {
border: 0;
background-image:
radial-gradient(circle at 50% 14%, currentColor 0 13%, transparent 14%),
radial-gradient(
circle at 50% 86%,
color-mix(in oklch, currentColor 40%, transparent) 0 11%,
transparent 12%
),
radial-gradient(
circle,
transparent 0 46%,
color-mix(in oklch, currentColor 15%, transparent) 47% 50%,
transparent 51%
);
animation-duration: calc(1.4s * var(--fc-spinner-slow));
animation-timing-function: var(--fc-easing-standard);
}
/* ---- Sparkle: a model is thinking ------------------------------------ */
/*
* A four-point star that turns and breathes. The fill is a gradient across
* two theme tokens, which a product can replace through
* `--fc-spinner-fill` with its own brand gradient.
*/
.fc-spinner--sparkle {
border: 0;
border-radius: 0;
background-image: var(
--fc-spinner-fill,
linear-gradient(135deg, var(--fc-color-accent-bg), var(--fc-color-info-bg))
);
clip-path: polygon(
50% 0%, 61% 39%, 100% 50%, 61% 61%,
50% 100%, 39% 61%, 0% 50%, 39% 39%
);
animation: fc-spinner-sparkle calc(1.6s * var(--fc-spinner-slow)) var(--fc-easing-standard) infinite;
}
/* ---- Keyframes ------------------------------------------------------- */
@keyframes fc-spinner-rotate {
to {
rotate: 1turn;
}
}
@keyframes fc-spinner-bounce {
0%, 70%, 100% {
translate: 0 0;
opacity: 0.35;
}
35% {
translate: 0 -45%;
opacity: 1;
}
}
@keyframes fc-spinner-pulse {
0% {
scale: 0.45;
box-shadow: 0 0 0 0 color-mix(in oklch, currentColor 50%, transparent);
}
60% {
scale: 0.55;
box-shadow: 0 0 0 calc(var(--fc-spinner-size) * 0.45) transparent;
}
100% {
scale: 0.45;
box-shadow: 0 0 0 0 transparent;
}
}
@keyframes fc-spinner-bars {
0%, 100% {
scale: 1 0.35;
}
50% {
scale: 1 1;
}
}
/* A star with four-fold symmetry repeats every quarter turn. */
@keyframes fc-spinner-sparkle {
0% {
rotate: 0deg;
scale: 0.7;
}
50% {
rotate: 45deg;
scale: 1;
}
100% {
rotate: 90deg;
scale: 0.7;
}
}
/* Reduced-motion replacement for anything that moves rather than turns. */
@keyframes fc-spinner-fade {
0%, 100% {
opacity: 0.35;
}
50% {
opacity: 1;
}
}
/* ---- Shimmering status text ------------------------------------------ */
/*
* Text that says "in progress" by shimmering: "Searching 12 sources",
* "Running tests". The gradient runs between two text tokens that both pass
* contrast on a surface, so the words are legible at every frame, not only at
* the bright one. The sweep follows the reading direction.
*/
.fc-shimmer-text {
background-image: linear-gradient(
90deg,
var(--fc-color-text-muted) 0%,
var(--fc-color-text-muted) 35%,
var(--fc-color-text-default) 50%,
var(--fc-color-text-muted) 65%,
var(--fc-color-text-muted) 100%
);
background-size: 250% 100%;
background-clip: text;
color: transparent;
animation: fc-spinner-shimmer 2s linear infinite;
}
.fc-shimmer-text:dir(rtl) {
animation-direction: reverse;
}
@keyframes fc-spinner-shimmer {
from {
background-position: 100% 0;
}
to {
background-position: 0% 0;
}
}
/* ---- Reduced motion -------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
.fc-spinner {
--fc-spinner-slow: 3;
}
.fc-spinner--dots > *,
.fc-spinner--bars > * {
translate: none;
scale: none;
animation-name: fc-spinner-fade;
}
.fc-spinner--pulse {
scale: 0.5;
animation-name: fc-spinner-fade;
}
.fc-spinner--sparkle {
scale: 0.85;
animation-name: fc-spinner-fade;
}
/* Shimmer is emphasis on words that already say "working"; it stops. */
.fc-shimmer-text {
animation: none;
background-image: none;
color: var(--fc-color-text-muted);
}
}
/* ---- Forced colors --------------------------------------------------- */
/*
* Forced colours replace background colours, which would erase every drawing
* here except the ring. The spinners opt out of the adjustment and draw in
* CanvasText, so they stay visible -- and in the user's chosen colour.
*/
@media (forced-colors: active) {
.fc-spinner {
forced-color-adjust: none;
color: CanvasText;
}
.fc-spinner--sparkle {
background-image: none;
background-color: CanvasText;
}
.fc-shimmer-text {
background-image: none;
color: CanvasText;
animation: none;
}
}
}
/*!
* future-css -- agent status
*
* The one-line "what is the agent doing right now" row in an agent chat:
*
* <div class="fc-agent-status" data-fc-kind="agent-status" data-fc-state="running">
* <span class="fc-spinner fc-spinner--sparkle fc-spinner--sm" aria-hidden="true"></span>
* <span class="fc-agent-status__label" role="status">Searching 12 sources</span>
* <span class="fc-agent-status__meta">14s</span>
* </div>
*
* `role="status"` is on the LABEL, not the row. The elapsed time in `__meta`
* changes every second, and inside a live region it would be announced every
* second. The label changes at step boundaries ("Reading files" -> "Running
* tests"), which is exactly how often a screen reader user wants to hear it.
*
* The state attribute drives everything: while it is pending, running, or
* streaming the label shimmers and the spinner turns; once it is complete,
* error, aborted, or denied the spinner is hidden and a glyph states the
* outcome. The visual "still working" cue cannot outlive the state.
*/
@layer fc.components {
.fc-agent-status {
display: inline-flex;
align-items: center;
gap: var(--fc-space-2);
min-block-size: var(--fc-size-target-min);
max-inline-size: 100%;
font-size: var(--fc-font-size-sm);
color: var(--fc-color-text-muted);
}
.fc-agent-status__label {
min-inline-size: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.fc-agent-status__meta {
flex-shrink: 0;
color: var(--fc-color-text-subtle);
font-size: var(--fc-font-size-xs);
font-variant-numeric: tabular-nums;
}
/* In progress: the label shimmers, like .fc-shimmer-text. */
.fc-agent-status:is([data-fc-state="pending"], [data-fc-state="running"], [data-fc-state="streaming"], [data-fc-state="queued"]) .fc-agent-status__label {
background-image: linear-gradient(
90deg,
var(--fc-color-text-muted) 0%,
var(--fc-color-text-muted) 35%,
var(--fc-color-text-default) 50%,
var(--fc-color-text-muted) 65%,
var(--fc-color-text-muted) 100%
);
background-size: 250% 100%;
background-clip: text;
color: transparent;
animation: fc-agent-status-shimmer 2s linear infinite;
}
.fc-agent-status:is([data-fc-state="pending"], [data-fc-state="running"], [data-fc-state="streaming"], [data-fc-state="queued"]):dir(rtl) .fc-agent-status__label {
animation-direction: reverse;
}
@keyframes fc-agent-status-shimmer {
from {
background-position: 100% 0;
}
to {
background-position: 0% 0;
}
}
/* Finished: the spinner goes, and a glyph says how it ended. */
.fc-agent-status:is([data-fc-state="complete"], [data-fc-state="error"], [data-fc-state="aborted"], [data-fc-state="denied"]) .fc-spinner {
display: none;
}
.fc-agent-status[data-fc-state="complete"]::before {
content: "\2713" / "";
color: var(--fc-color-success-text);
font-weight: var(--fc-font-weight-bold);
}
.fc-agent-status[data-fc-state="error"]::before,
.fc-agent-status[data-fc-state="denied"]::before {
content: "\2715" / "";
color: var(--fc-color-danger-text);
font-weight: var(--fc-font-weight-bold);
}
.fc-agent-status[data-fc-state="aborted"]::before {
content: "\25A0" / "";
color: var(--fc-color-warning-text);
}
.fc-agent-status[data-fc-state="error"] .fc-agent-status__label {
color: var(--fc-color-danger-text);
}
/* ---- Reduced motion -------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
.fc-agent-status:is([data-fc-state="pending"], [data-fc-state="running"], [data-fc-state="streaming"], [data-fc-state="queued"]) .fc-agent-status__label {
animation: none;
background-image: none;
color: var(--fc-color-text-muted);
}
}
/* ---- Forced colors --------------------------------------------------- */
/* Clipped-to-text backgrounds would leave the label invisible. */
@media (forced-colors: active) {
.fc-agent-status:is([data-fc-state="pending"], [data-fc-state="running"], [data-fc-state="streaming"], [data-fc-state="queued"]) .fc-agent-status__label {
background-image: none;
color: CanvasText;
animation: none;
}
}
}
/*!
* future-css -- skeleton and progress
*
* Two answers to "something is happening" (the third, the spinner, has its
* own file -- spinner.css -- because it has a family of styles):
*
* skeleton content is coming, and its shape is known
* progress work is happening, and how much is done is known
*
* Neither is announced by itself. A skeleton's region
* carries `aria-busy="true"` until the content lands; progress is the native
* `<progress>` element, which assistive technology already understands.
*/
@layer fc.components {
/* ---- Skeleton -------------------------------------------------------- */
/*
* A placeholder in the shape of the content to come, so the layout does not
* jump when it arrives. The shimmer runs along the inline axis; the RTL
* branch reverses it, because a sweep against the reading direction reads
* as going backwards.
*/
.fc-skeleton {
display: block;
block-size: 1lh;
background-color: var(--fc-color-bg-muted);
background-image: linear-gradient(
90deg,
transparent 0%,
color-mix(in oklch, var(--fc-color-bg-surface) 60%, transparent) 50%,
transparent 100%
);
background-size: 200% 100%;
background-repeat: no-repeat;
border-radius: var(--fc-radius-md);
animation: fc-loading-shimmer 1.6s ease-in-out infinite;
}
.fc-skeleton:dir(rtl) {
animation-direction: reverse;
}
/* Text lines are shorter than the line box, like real glyphs. */
.fc-skeleton--text {
block-size: 0.75lh;
margin-block: 0.125lh;
}
.fc-skeleton--text:last-child {
inline-size: 70%;
}
.fc-skeleton--circle {
inline-size: var(--fc-skeleton-size, 2.5rem);
block-size: var(--fc-skeleton-size, 2.5rem);
border-radius: var(--fc-radius-full);
}
.fc-skeleton--block {
block-size: var(--fc-skeleton-size, 8rem);
border-radius: var(--fc-radius-lg);
}
@keyframes fc-loading-shimmer {
from {
background-position: 150% 0;
}
to {
background-position: -50% 0;
}
}
/* ---- Progress -------------------------------------------------------- */
/*
* The native element, restyled through its vendor parts. It keeps its role,
* its value, and its announcements, which a div with a width would not.
*/
.fc-progress {
appearance: none;
display: block;
inline-size: 100%;
block-size: var(--fc-progress-size, 0.5rem);
background-color: var(--fc-color-bg-muted);
color: var(--fc-color-accent-bg);
border: 0;
border-radius: var(--fc-radius-full);
overflow: hidden;
}
.fc-progress::-webkit-progress-bar {
background-color: var(--fc-color-bg-muted);
}
.fc-progress::-webkit-progress-value {
background-color: var(--fc-color-accent-bg);
border-radius: var(--fc-radius-full);
transition: inline-size var(--fc-duration-slow) var(--fc-easing-standard);
}
.fc-progress::-moz-progress-bar {
background-color: var(--fc-color-accent-bg);
border-radius: var(--fc-radius-full);
}
.fc-progress--sm {
--fc-progress-size: 0.25rem;
}
/*
* Indeterminate: a value-less <progress>. Drawn as a moving band, never as a
* bar frozen at some width, which would claim a percentage nobody knows.
*/
.fc-progress:indeterminate {
background-image: linear-gradient(
90deg,
transparent 0%,
var(--fc-color-accent-bg) 40%,
var(--fc-color-accent-bg) 60%,
transparent 100%
);
background-size: 40% 100%;
background-repeat: no-repeat;
animation: fc-loading-sweep 1.4s var(--fc-easing-standard) infinite;
}
.fc-progress:indeterminate::-webkit-progress-bar {
background-color: transparent;
}
.fc-progress:indeterminate::-moz-progress-bar {
background-color: transparent;
}
.fc-progress:indeterminate:dir(rtl) {
animation-direction: reverse;
}
@keyframes fc-loading-sweep {
from {
background-position: -60% 0;
}
to {
background-position: 160% 0;
}
}
/* ---- Progress ring --------------------------------------------------- */
/*
* Circular determinate progress: an upload, a context window filling up.
* There is no native circular element, so the role and values are ARIA,
* and the drawn value is a custom property the markup states beside them:
*
* <div class="fc-progress-ring" role="progressbar" aria-label="Uploading"
* aria-valuemin="0" aria-valuemax="100" aria-valuenow="42"
* style="--fc-progress-value: 42">
* <span class="fc-progress-ring__label">42%</span>
* </div>
*
* It only ever shows a known amount; an unknown one is a spinner, so the
* ring never pretends to know. Whoever updates `aria-valuenow` updates the
* property in the same write.
*/
.fc-progress-ring {
--fc-progress-ring-size: 2.5rem;
--fc-progress-ring-width: 0.25rem;
--fc-progress-ring-fill: var(--fc-color-accent-border);
position: relative;
display: inline-grid;
place-items: center;
flex-shrink: 0;
inline-size: var(--fc-progress-ring-size);
block-size: var(--fc-progress-ring-size);
color: var(--fc-color-text-default);
font-size: var(--fc-font-size-xs);
font-variant-numeric: tabular-nums;
/* Animates between values because the property is registered below. */
transition: --fc-progress-value var(--fc-duration-slow) var(--fc-easing-standard);
}
/*
* Registered as a number so a change of value interpolates rather than
* jumping; unregistered custom properties cannot transition.
*/
@property --fc-progress-value {
syntax: "<number>";
inherits: true;
initial-value: 0;
}
/*
* A conic fill cut to a ring by a radial mask. The fill starts at the top
* and runs clockwise in both directions, like a clock face, which is how a
* circular gauge is read in RTL scripts too.
*/
.fc-progress-ring::before {
content: "";
position: absolute;
inset: 0;
border-radius: var(--fc-radius-full);
background: conic-gradient(
var(--fc-progress-ring-fill) calc(var(--fc-progress-value, 0) * 1%),
var(--fc-color-bg-muted) 0
);
/* A mask reads only alpha, so any opaque colour cuts the same ring. */
mask: radial-gradient(
farthest-side,
transparent calc(100% - var(--fc-progress-ring-width) - 0.5px),
currentColor calc(100% - var(--fc-progress-ring-width))
);
}
.fc-progress-ring__label {
position: relative;
}
.fc-progress-ring--sm {
--fc-progress-ring-size: 1.5rem;
--fc-progress-ring-width: 0.1875rem;
}
.fc-progress-ring--lg {
--fc-progress-ring-size: 4rem;
--fc-progress-ring-width: 0.375rem;
font-size: var(--fc-font-size-sm);
}
/* A failed run keeps its ring at the point it stopped, in the danger colour. */
.fc-progress-ring--error {
--fc-progress-ring-fill: var(--fc-color-danger-border);
}
/* ---- Reduced motion -------------------------------------------------- */
/*
* The shimmer and sweep are decoration on top of a shape that already says
* "loading", so they stop, and
* the indeterminate bar becomes a static, full-width hatch that cannot be read
* as a percentage.
*/
@media (prefers-reduced-motion: reduce) {
.fc-skeleton {
animation: none;
background-image: none;
}
.fc-progress:indeterminate {
animation: none;
background-size: auto;
background-image: repeating-linear-gradient(
45deg,
var(--fc-color-accent-bg) 0 0.375rem,
transparent 0.375rem 0.75rem
);
}
}
/* ---- Forced colors --------------------------------------------------- */
@media (forced-colors: active) {
.fc-skeleton {
border: var(--fc-border-width-thin) dashed GrayText;
}
.fc-progress {
border: var(--fc-border-width-thin) solid CanvasText;
}
/* Gradient colours are not forced, so the ring restates them itself. */
.fc-progress-ring {
--fc-progress-ring-fill: Highlight;
forced-color-adjust: none;
color: CanvasText;
}
.fc-progress-ring::before {
border: var(--fc-border-width-thin) solid CanvasText;
}
}
}
/*!
* future-css -- avatar
*
* <span class="fc-avatar">
* <img class="fc-avatar__image" src="…" alt="Ada Lovelace">
* </span>
* <span class="fc-avatar" role="img" aria-label="Ada Lovelace">AL</span>
*
* An image with a name, or initials with a name. When the avatar sits next to
* the visible name -- as it does in a chat message header -- it is decoration
* and takes `alt=""` / `aria-hidden="true"` instead, so the name is not read
* twice.
*
* Square avatars (`--square`) are for non-people: an agent, a tool, an
* organisation. The shape difference is a convention users already read in
* chat products, and it lets "who said this" be parsed at a glance.
*/
@layer fc.components {
.fc-avatar {
--fc-avatar-size: 2rem;
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
inline-size: var(--fc-avatar-size);
block-size: var(--fc-avatar-size);
background-color: var(--fc-color-accent-bg-subtle);
color: var(--fc-color-accent-text);
border-radius: var(--fc-radius-full);
overflow: hidden;
font-size: calc(var(--fc-avatar-size) * 0.4);
font-weight: var(--fc-font-weight-semibold);
line-height: 1;
text-transform: uppercase;
user-select: none;
}
.fc-avatar__image {
inline-size: 100%;
block-size: 100%;
object-fit: cover;
}
.fc-avatar--xs {
--fc-avatar-size: 1.25rem;
}
.fc-avatar--sm {
--fc-avatar-size: 1.5rem;
}
.fc-avatar--lg {
--fc-avatar-size: 2.75rem;
}
.fc-avatar--xl {
--fc-avatar-size: 4rem;
}
.fc-avatar--square {
border-radius: var(--fc-radius-lg);
}
.fc-avatar--neutral {
background-color: var(--fc-color-bg-muted);
color: var(--fc-color-text-muted);
}
/*
* A stack of avatars ("3 people are here"). The ring is an outline in the
* surface colour, so each avatar separates from the one it overlaps without
* an extra element.
*/
.fc-avatar-group {
display: inline-flex;
align-items: center;
}
.fc-avatar-group > * {
outline: var(--fc-border-width-thick) solid var(--fc-color-bg-surface);
}
.fc-avatar-group > * + * {
margin-inline-start: calc(var(--fc-avatar-size, 2rem) * -0.2);
}
@media (forced-colors: active) {
.fc-avatar {
border: var(--fc-border-width-thin) solid CanvasText;
}
}
}
/*!
* future-css -- empty state
*
* What a list, a search, or a new chat shows when there is nothing in it yet.
* The job is to say why it is empty and what to do next, in that order:
*
* <div class="fc-empty">
* <div class="fc-empty__icon" aria-hidden="true">…</div>
* <h2 class="fc-empty__title">No results for "quarterly"</h2>
* <p class="fc-empty__body">Try a shorter search, or check the spelling.</p>
* <div class="fc-empty__actions"><button class="fc-btn">Clear search</button></div>
* </div>
*
* The error variant is the same shape for "this failed to load", so a failure
* never renders as a blank region that looks like an empty result.
*/
@layer fc.components {
.fc-empty {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--fc-space-2);
max-inline-size: 28rem;
margin-inline: auto;
padding-block: var(--fc-space-12);
padding-inline: var(--fc-space-4);
text-align: center;
}
.fc-empty__icon {
display: flex;
align-items: center;
justify-content: center;
inline-size: 3rem;
block-size: 3rem;
margin-block-end: var(--fc-space-2);
background-color: var(--fc-empty-icon-bg, var(--fc-color-bg-subtle));
color: var(--fc-empty-icon-fg, var(--fc-color-text-muted));
border-radius: var(--fc-radius-xl);
font-size: var(--fc-font-size-xl);
}
.fc-empty__title {
margin: 0;
font-size: var(--fc-font-size-lg);
font-weight: var(--fc-font-weight-semibold);
color: var(--fc-color-text-default);
text-wrap: balance;
}
.fc-empty__body {
margin: 0;
font-size: var(--fc-font-size-sm);
color: var(--fc-color-text-muted);
text-wrap: pretty;
}
.fc-empty__actions {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: var(--fc-space-2);
margin-block-start: var(--fc-space-3);
}
.fc-empty--compact {
padding-block: var(--fc-space-6);
}
/* The modifier reaches the icon through custom properties, not a descendant selector. */
.fc-empty--error {
--fc-empty-icon-bg: var(--fc-color-danger-bg-subtle);
--fc-empty-icon-fg: var(--fc-color-danger-text);
}
@media (forced-colors: active) {
.fc-empty__icon {
border: var(--fc-border-width-thin) solid CanvasText;
}
}
}
/*!
* future-css -- disclosure and accordion
*
* Built on `<details>`/`<summary>`, which are keyboard-operable, announced as
* expandable, searchable with find-in-page (closed content opens to show a
* match), and work with JavaScript off.
*
* <details class="fc-disclosure" name="faq">
* <summary class="fc-disclosure__summary">What does it cost?</summary>
* <div class="fc-disclosure__content">…</div>
* </details>
*
* An accordion is several disclosures. Giving them the same `name` makes them
* exclusive -- opening one closes the others -- natively, with no script.
*
* Opening animates its height where `::details-content` and
* `interpolate-size` are supported; elsewhere it opens instantly, which is the
* correct fallback.
*/
@layer fc.components {
.fc-disclosure {
/* Lets `block-size: auto` be a transition endpoint. */
interpolate-size: allow-keywords;
border-block-end: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
}
.fc-disclosure__summary {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--fc-space-3);
min-block-size: var(--fc-size-target-comfortable);
padding-block: var(--fc-space-3);
padding-inline: var(--fc-space-1);
font-weight: var(--fc-font-weight-medium);
list-style: none;
cursor: pointer;
border-radius: var(--fc-radius-md);
}
/* WebKit still draws its own marker unless told not to. */
.fc-disclosure__summary::-webkit-details-marker {
display: none;
}
.fc-disclosure__summary:hover {
color: var(--fc-color-accent-text);
}
/*
* The chevron is two borders of a rotated square. `rotate` is physical, so the
* closed state's "points toward the content's reading direction" needs an RTL
* branch; the open state points down in both directions.
*/
.fc-disclosure__summary::after {
content: "";
flex-shrink: 0;
inline-size: 0.5rem;
block-size: 0.5rem;
margin-inline-end: var(--fc-space-1);
border-block-end: var(--fc-border-width-thick) solid currentColor;
border-inline-end: var(--fc-border-width-thick) solid currentColor;
rotate: -45deg;
transition: rotate var(--fc-duration-normal) var(--fc-easing-standard);
}
.fc-disclosure__summary:dir(rtl)::after {
rotate: 45deg;
}
.fc-disclosure[open] > .fc-disclosure__summary::after {
rotate: 45deg;
}
.fc-disclosure[open] > .fc-disclosure__summary:dir(rtl)::after {
rotate: -45deg;
}
.fc-disclosure::details-content {
block-size: 0;
overflow: clip;
transition:
block-size var(--fc-duration-normal) var(--fc-easing-standard),
content-visibility var(--fc-duration-normal) allow-discrete;
}
.fc-disclosure[open]::details-content {
block-size: auto;
}
.fc-disclosure__content {
padding-block: 0 var(--fc-space-4);
padding-inline: var(--fc-space-1);
color: var(--fc-color-text-muted);
font-size: var(--fc-font-size-sm);
}
/* Boxed: a bordered card per item, for settings pages and FAQ blocks. */
.fc-disclosure--boxed {
background-color: var(--fc-color-bg-surface);
border: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
border-radius: var(--fc-radius-lg);
padding-inline: var(--fc-space-3);
}
.fc-disclosure--boxed + .fc-disclosure--boxed {
margin-block-start: var(--fc-space-2);
}
@media (forced-colors: active) {
.fc-disclosure {
border-block-end-color: CanvasText;
}
}
}
/*!
* future-css -- reasoning trace
*
* The collapsible "thinking" block a model emits before its answer. It is a
* `<details>` so it is collapsed by default, expandable by keyboard, and
* readable with JavaScript off:
*
* <details class="fc-reasoning" data-fc-kind="reasoning" data-fc-state="streaming">
* <summary class="fc-reasoning__summary">
* <span class="fc-reasoning__label">Thinking</span>
* </summary>
* <div class="fc-reasoning__body">…</div>
* </details>
*
* When it completes, the application sets `data-fc-state="complete"` and
* replaces the label ("Thought for 12s"), adding `data-fc-ts` or a duration
* attribute for machine consumers. The shimmer is driven by the state
* attribute, so the visual "still thinking" cue cannot outlive the state.
*
* The body is NOT a live region. Reasoning streams token by token; announcing
* it would drown the answer that follows. A user who wants it opens it.
*/
@layer fc.components {
.fc-reasoning {
interpolate-size: allow-keywords;
max-inline-size: var(--fc-size-measure);
font-size: var(--fc-font-size-sm);
}
.fc-reasoning__summary {
display: inline-flex;
align-items: center;
gap: var(--fc-space-1-5);
min-block-size: var(--fc-size-target-min);
padding-block: var(--fc-space-1);
padding-inline: var(--fc-space-2) var(--fc-space-1);
margin-inline-start: calc(var(--fc-space-2) * -1);
color: var(--fc-color-text-muted);
border-radius: var(--fc-radius-md);
list-style: none;
cursor: pointer;
}
.fc-reasoning__summary::-webkit-details-marker {
display: none;
}
.fc-reasoning__summary:hover {
background-color: var(--fc-color-bg-subtle);
color: var(--fc-color-text-default);
}
/* Chevron: same construction as the disclosure, smaller. */
.fc-reasoning__summary::after {
content: "";
inline-size: 0.375rem;
block-size: 0.375rem;
border-block-end: var(--fc-border-width-thin) solid currentColor;
border-inline-end: var(--fc-border-width-thin) solid currentColor;
rotate: -45deg;
transition: rotate var(--fc-duration-normal) var(--fc-easing-standard);
}
.fc-reasoning__summary:dir(rtl)::after {
rotate: 45deg;
}
.fc-reasoning[open] > .fc-reasoning__summary::after {
rotate: 45deg;
}
.fc-reasoning[open] > .fc-reasoning__summary:dir(rtl)::after {
rotate: -45deg;
}
.fc-reasoning__label {
font-weight: var(--fc-font-weight-medium);
}
/*
* Still thinking: the label shimmers. The gradient runs between two text
* tokens that both pass contrast on the surface, so the label is legible at
* every frame of the animation, not only at its brightest.
*/
.fc-reasoning[data-fc-state="streaming"] .fc-reasoning__label,
.fc-reasoning[data-fc-state="pending"] .fc-reasoning__label {
background-image: linear-gradient(
90deg,
var(--fc-color-text-muted) 0%,
var(--fc-color-text-muted) 35%,
var(--fc-color-text-default) 50%,
var(--fc-color-text-muted) 65%,
var(--fc-color-text-muted) 100%
);
background-size: 250% 100%;
background-clip: text;
color: transparent;
animation: fc-reasoning-shimmer 2s linear infinite;
}
.fc-reasoning[data-fc-state="streaming"] .fc-reasoning__label:dir(rtl),
.fc-reasoning[data-fc-state="pending"] .fc-reasoning__label:dir(rtl) {
animation-direction: reverse;
}
@keyframes fc-reasoning-shimmer {
from {
background-position: 100% 0;
}
to {
background-position: 0% 0;
}
}
.fc-reasoning[data-fc-state="error"] .fc-reasoning__label,
.fc-reasoning[data-fc-state="aborted"] .fc-reasoning__label {
color: var(--fc-color-warning-text);
}
.fc-reasoning__duration {
color: var(--fc-color-text-subtle);
font-variant-numeric: tabular-nums;
}
.fc-reasoning::details-content {
block-size: 0;
overflow: clip;
transition:
block-size var(--fc-duration-normal) var(--fc-easing-standard),
content-visibility var(--fc-duration-normal) allow-discrete;
}
.fc-reasoning[open]::details-content {
block-size: auto;
}
.fc-reasoning__body {
margin-block: var(--fc-space-2);
padding-inline-start: var(--fc-space-3);
border-inline-start: var(--fc-border-width-thick) solid var(--fc-color-border-subtle);
color: var(--fc-color-text-muted);
white-space: pre-wrap;
overflow-wrap: anywhere;
/* A long trace scrolls inside itself rather than pushing the answer away. */
max-block-size: 24rem;
overflow-y: auto;
}
/* ---- Reduced motion -------------------------------------------------- */
/* The label text already says "Thinking"; the shimmer is redundant emphasis. */
@media (prefers-reduced-motion: reduce) {
.fc-reasoning[data-fc-state="streaming"] .fc-reasoning__label,
.fc-reasoning[data-fc-state="pending"] .fc-reasoning__label {
animation: none;
background-image: none;
color: var(--fc-color-text-muted);
}
}
/* ---- Forced colors --------------------------------------------------- */
/* background-clip text would render as invisible text once colours are forced. */
@media (forced-colors: active) {
.fc-reasoning[data-fc-state="streaming"] .fc-reasoning__label,
.fc-reasoning[data-fc-state="pending"] .fc-reasoning__label {
background-image: none;
color: CanvasText;
animation: none;
}
.fc-reasoning__body {
border-inline-start-color: CanvasText;
}
}
}
/*!
* future-css -- citations and sources
*
* Inline citation markers in an answer, and the list of sources they point to.
*
* <p>The bundle is 153 KB<a class="fc-cite" href="#src-1"
* aria-label="Source 1: PLAN.md">1</a>.</p>
*
* <ol class="fc-sources" aria-label="Sources">
* <li class="fc-source" id="src-1">
* <a class="fc-source__link" href="https://…">
* <span class="fc-source__index">1</span>
* <span class="fc-source__title">PLAN.md</span>
* <span class="fc-source__domain">github.com</span>
* </a>
* </li>
* </ol>
*
* A marker's visible text is a number, which is a useless accessible name on
* its own -- "link, 1" -- so it carries an aria-label naming the source. The
* marker links to the source entry in the page, and the entry links out; the
* reader can check a claim without leaving the answer.
*/
@layer fc.components {
.fc-cite {
display: inline-flex;
align-items: center;
justify-content: center;
min-inline-size: 1.25em;
block-size: 1.25em;
margin-inline: 0.15em;
padding-inline: 0.3em;
/* Raised off the baseline like a footnote, without shrinking line spacing. */
vertical-align: 0.2em;
background-color: var(--fc-color-bg-muted);
color: var(--fc-color-text-muted);
border-radius: var(--fc-radius-full);
font-size: 0.7em;
font-weight: var(--fc-font-weight-semibold);
font-variant-numeric: tabular-nums;
line-height: 1;
text-decoration: none;
transition:
background-color var(--fc-duration-fast) var(--fc-easing-standard),
color var(--fc-duration-fast) var(--fc-easing-standard);
}
.fc-cite:hover {
background-color: var(--fc-color-accent-bg-subtle);
color: var(--fc-color-accent-text);
}
/* ---- Sources --------------------------------------------------------- */
.fc-sources {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(100%, 12rem), 1fr));
gap: var(--fc-space-2);
margin: 0;
padding: 0;
list-style: none;
}
.fc-source {
min-inline-size: 0;
border-radius: var(--fc-radius-lg);
/* `scroll-margin` keeps a jumped-to source clear of a sticky header. */
scroll-margin-block: var(--fc-space-16);
transition: background-color var(--fc-duration-slow) var(--fc-easing-standard);
}
/* Arriving from a citation marker: the entry is briefly highlighted. */
.fc-source:target {
background-color: var(--fc-color-accent-bg-subtle);
}
.fc-source__link {
display: grid;
grid-template-columns: auto 1fr;
grid-template-areas:
"index title"
"index domain";
column-gap: var(--fc-space-2);
align-items: center;
block-size: 100%;
padding: var(--fc-space-2) var(--fc-space-3);
background-color: var(--fc-color-bg-surface);
color: var(--fc-color-text-default);
border: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
border-radius: inherit;
text-decoration: none;
transition: border-color var(--fc-duration-fast) var(--fc-easing-standard);
}
.fc-source__link:hover {
border-color: var(--fc-color-border-strong);
}
.fc-source__index {
grid-area: index;
display: inline-flex;
align-items: center;
justify-content: center;
inline-size: 1.5rem;
block-size: 1.5rem;
background-color: var(--fc-color-bg-muted);
color: var(--fc-color-text-muted);
border-radius: var(--fc-radius-md);
font-size: var(--fc-font-size-xs);
font-weight: var(--fc-font-weight-semibold);
font-variant-numeric: tabular-nums;
}
/* A favicon in place of the number, when the application has one. */
.fc-source__icon {
grid-area: index;
inline-size: 1.5rem;
block-size: 1.5rem;
border-radius: var(--fc-radius-md);
object-fit: contain;
}
.fc-source__title {
grid-area: title;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: var(--fc-font-size-sm);
font-weight: var(--fc-font-weight-medium);
}
.fc-source__domain {
grid-area: domain;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: var(--fc-font-size-xs);
color: var(--fc-color-text-muted);
}
@media (forced-colors: active) {
.fc-cite {
border: var(--fc-border-width-thin) solid LinkText;
}
.fc-source:target .fc-source__link {
border: var(--fc-border-width-thick) solid Highlight;
}
}
}
/*!
* future-css -- suggested prompts
*
* Starter prompts on an empty chat, and follow-up suggestions under an answer.
* Each one is a button whose label is the prompt itself, so the accessible
* name is exactly what will be sent.
*
* <ul class="fc-suggestions" aria-label="Suggested prompts">
* <li><button class="fc-suggestion" type="button">
* <span class="fc-suggestion__title">Summarise this thread</span>
* <span class="fc-suggestion__hint">in three bullet points</span>
* </button></li>
* </ul>
*
* A list, so a screen reader announces how many there are before reading
* them, and so a consumer reading the DOM can enumerate them without guessing.
*/
@layer fc.components {
.fc-suggestions {
display: flex;
flex-wrap: wrap;
gap: var(--fc-space-2);
margin: 0;
padding: 0;
list-style: none;
}
/* A two-column grid of larger cards, for the empty-chat starter set. */
.fc-suggestions--grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
}
.fc-suggestion {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--fc-space-0-5);
inline-size: 100%;
min-block-size: var(--fc-size-target-comfortable);
padding-block: var(--fc-space-2);
padding-inline: var(--fc-space-3);
background-color: var(--fc-color-bg-surface);
color: var(--fc-color-text-default);
border: var(--fc-border-width-thin) solid var(--fc-color-border-default);
border-radius: var(--fc-radius-lg);
font: inherit;
font-size: var(--fc-font-size-sm);
text-align: start;
cursor: pointer;
transition:
background-color var(--fc-duration-fast) var(--fc-easing-standard),
border-color var(--fc-duration-fast) var(--fc-easing-standard),
translate var(--fc-duration-fast) var(--fc-easing-standard);
}
.fc-suggestion:hover {
background-color: var(--fc-color-bg-subtle);
border-color: var(--fc-color-border-strong);
}
.fc-suggestion:active {
translate: 0 var(--fc-space-px);
background-color: var(--fc-color-bg-muted);
}
.fc-suggestion:disabled,
.fc-suggestion[aria-disabled="true"] {
color: var(--fc-color-disabled-fg);
border-color: var(--fc-color-disabled-border);
background-color: transparent;
cursor: not-allowed;
}
.fc-suggestion__title {
font-weight: var(--fc-font-weight-medium);
}
.fc-suggestion__hint {
font-size: var(--fc-font-size-xs);
color: var(--fc-color-text-muted);
}
/* Follow-ups under an answer are pills, quieter than the starter cards. */
.fc-suggestion--pill {
flex-direction: row;
align-items: center;
inline-size: auto;
min-block-size: 2rem;
padding-block: var(--fc-space-1);
border-radius: var(--fc-radius-full);
}
@media (forced-colors: active) {
.fc-suggestion {
border-color: ButtonText;
}
}
}
/*!
* future-css -- data table
*
* A real <table> with sortable headers. For a spreadsheet-like surface with a
* cell cursor and editing, use `fc-grid`; this is for reading rows.
*
* <div class="fc-table-wrap" role="region" aria-labelledby="runs-cap" tabindex="0">
* <table class="fc-table fc-table--sticky">
* <caption id="runs-cap">Recent runs</caption>
* <thead>
* <tr>
* <th scope="col" aria-sort="ascending">
* <button class="fc-table__sort" type="button">Name</button>
* </th>
* <th scope="col" class="fc-table__cell--num">
* <button class="fc-table__sort" type="button">Tokens</button>
* </th>
* </tr>
* </thead>
* <tbody>
* <tr><td>Index docs</td><td class="fc-table__cell--num">12,480</td></tr>
* </tbody>
* </table>
* </div>
*
* Sorting is `aria-sort` on the <th>, and only that: the arrow, the header
* emphasis, and what a screen reader announces all read from the one
* attribute. APG puts `aria-sort` on one header at a time; a column that can be
* sorted but is not is marked by its button, not by `aria-sort="none"`.
*
* The sort control is a <button> inside the header, not a click handler on the
* <th>, so it is in the tab order and announces as a control. The `table`
* binding wires it; without the binding the arrows still show whatever order
* the server rendered.
*/
@layer fc.components {
/*
* The scroll container is focusable and named so a keyboard user can scroll
* a wide table sideways: overflow on a non-focusable element is reachable by
* mouse only. This is also what keeps a wide table from making the page
* scroll horizontally at 320px.
*/
.fc-table-wrap {
max-inline-size: 100%;
overflow: auto;
border: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
border-radius: var(--fc-radius-lg);
background-color: var(--fc-color-bg-surface);
scrollbar-width: thin;
}
.fc-table-wrap:focus-visible {
outline-offset: var(--fc-space-0-5);
}
/*
* Parts read custom properties the modifiers set, so a modifier restyles
* every cell without a descendant class selector.
*/
.fc-table {
inline-size: 100%;
border-collapse: separate;
border-spacing: 0;
font-size: var(--fc-font-size-sm);
color: var(--fc-color-text-default);
}
.fc-table caption {
padding: var(--fc-space-3) var(--fc-space-3) var(--fc-space-2);
color: var(--fc-color-text-default);
font-weight: var(--fc-font-weight-semibold);
text-align: start;
}
.fc-table :is(th, td) {
padding-block: var(--fc-table-cell-pad-block, var(--fc-space-2));
padding-inline: var(--fc-table-cell-pad-inline, var(--fc-space-3));
/* Through a property, because this rule outranks a single-class cell
modifier: `.fc-table th` is (0,1,1), `.fc-table__cell--num` (0,1,0). */
text-align: var(--fc-table-cell-align, start);
vertical-align: top;
border-block-end: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
}
.fc-table thead th {
/* Sticky only under --sticky; `static` keeps the default table cheap. */
position: var(--fc-table-head-position, static);
inset-block-start: 0;
z-index: var(--fc-z-raised);
background-color: var(--fc-color-bg-subtle);
color: var(--fc-color-text-muted);
font-weight: var(--fc-font-weight-semibold);
white-space: nowrap;
vertical-align: bottom;
border-block-end-color: var(--fc-color-border-default);
}
.fc-table tbody tr:last-child > :is(th, td) {
border-block-end: 0;
}
.fc-table tbody th {
font-weight: var(--fc-font-weight-medium);
}
.fc-table tbody tr:nth-child(even) {
background-color: var(--fc-table-stripe, transparent);
}
.fc-table tbody tr:hover {
background-color: var(--fc-color-bg-subtle);
}
/*
* A selected row. `aria-selected` is the state because it is what assistive
* technology reads in a table that supports selection; a class would be a
* second source of truth.
*/
.fc-table tbody tr[aria-selected="true"] {
background-color: var(--fc-color-selected-bg);
color: var(--fc-color-selected-fg);
}
/*
* Numeric cells align to the end edge with tabular figures, so digits line
* up by place value. `end` rather than `right` keeps them on the trailing
* side in RTL, which is where Arabic and Hebrew readers expect numbers too.
*/
.fc-table__cell--num {
--fc-table-sort-flow: row-reverse;
--fc-table-cell-align: end;
font-variant-numeric: tabular-nums;
}
/* The full-width cell for "no rows" and "failed to load". */
.fc-table__empty {
/* Through the cell properties, for the same specificity reason as above. */
--fc-table-cell-pad-block: var(--fc-space-8);
--fc-table-cell-align: center;
color: var(--fc-color-text-muted);
}
/* ---- Sort control ---------------------------------------------------- */
.fc-table thead th[aria-sort] {
color: var(--fc-color-text-default);
}
.fc-table thead th[aria-sort="ascending"] {
--fc-table-sort-glyph: "\2191";
--fc-table-sort-glyph-color: var(--fc-color-accent-text);
}
.fc-table thead th[aria-sort="descending"] {
--fc-table-sort-glyph: "\2193";
--fc-table-sort-glyph-color: var(--fc-color-accent-text);
}
/*
* The button fills the header so the whole header is the target, and pulls
* back the cell padding with negative margins so the label stays aligned
* with the column's data.
*/
.fc-table__sort {
display: inline-flex;
/* Numeric headers reverse it, putting the arrow before the label so the
label stays flush with the end-aligned digits beneath it. */
flex-direction: var(--fc-table-sort-flow, row);
align-items: center;
gap: var(--fc-space-1);
min-block-size: var(--fc-size-target-min);
/* A two-letter header ("ID") would otherwise be a sliver of a target. */
min-inline-size: var(--fc-size-target-min);
margin-block: calc(var(--fc-space-1) * -1);
margin-inline: calc(var(--fc-space-1-5) * -1);
padding-block: var(--fc-space-1);
padding-inline: var(--fc-space-1-5);
background-color: transparent;
color: inherit;
border: 0;
border-radius: var(--fc-radius-sm);
font: inherit;
text-align: inherit;
cursor: pointer;
transition:
color var(--fc-duration-fast) var(--fc-easing-standard),
background-color var(--fc-duration-fast) var(--fc-easing-standard);
}
/*
* The glyph is generated with empty alternative text: `aria-sort` already
* announces the direction, and a spoken "up arrow" would say it twice. An
* unsorted column shows a faint double arrow so the affordance is visible
* before anyone hovers.
*/
.fc-table__sort::after {
content: var(--fc-table-sort-glyph, "\2195") / "";
color: var(--fc-table-sort-glyph-color, var(--fc-color-text-subtle));
font-size: var(--fc-font-size-xs);
line-height: var(--fc-line-height-none);
}
.fc-table__sort:hover {
color: var(--fc-color-text-default);
background-color: var(--fc-color-bg-muted);
}
.fc-table__sort:active {
background-color: var(--fc-color-bg-subtle);
}
.fc-table__sort:disabled,
.fc-table__sort[aria-disabled="true"] {
color: var(--fc-color-disabled-fg);
background-color: transparent;
cursor: not-allowed;
}
/*
* Loading: the table keeps its rows (or skeleton rows) and says it is busy.
* Dimming would lower contrast below the declared pairings, so the cue is
* the cursor plus whatever skeletons the app renders.
*/
.fc-table[aria-busy="true"] {
cursor: progress;
}
/* ---- Modifiers ------------------------------------------------------- */
.fc-table--sticky {
--fc-table-head-position: sticky;
}
.fc-table--striped {
--fc-table-stripe: var(--fc-color-bg-subtle);
}
.fc-table--compact {
--fc-table-cell-pad-block: var(--fc-space-1);
--fc-table-cell-pad-inline: var(--fc-space-2);
}
/*
* Reduced motion: nothing here moves. The only motion is the sort button's
* colour transition, which the reset's reduced-motion block already zeroes.
*/
/*
* Forced colours drop row backgrounds, so selection and the sorted column
* are restated with system colours rather than vanishing.
*/
@media (forced-colors: active) {
.fc-table tbody tr[aria-selected="true"] {
outline: var(--fc-border-width-thick) solid Highlight;
outline-offset: calc(var(--fc-border-width-thick) * -1);
}
.fc-table thead th[aria-sort] {
--fc-table-sort-glyph-color: Highlight;
}
.fc-table__sort:disabled,
.fc-table__sort[aria-disabled="true"] {
color: GrayText;
}
}
}
/*!
* future-css -- description list
*
* Key/value metadata: a run's model and duration, a file's size and owner.
*
* <dl class="fc-dl fc-dl--divided">
* <div class="fc-dl__item">
* <dt>Model</dt>
* <dd>Large, extended thinking</dd>
* </div>
* <div class="fc-dl__item">
* <dt>Duration</dt>
* <dd class="fc-dl__num">14.2 s</dd>
* </div>
* </dl>
*
* Each pair sits in a <div>, which HTML allows inside <dl> and which gives the
* pair one box to lay out and divide. Assistive technology ignores the <div>,
* so the term/description relationship is read exactly as without it.
*
* The layout is intrinsic rather than a breakpoint: the term takes a fixed
* share and the description wraps under it when the two no longer fit side by
* side. A list in a narrow sidebar stacks, the same list in a wide panel does
* not, and neither needs a media or container query.
*/
@layer fc.components {
.fc-dl {
display: flex;
flex-direction: column;
gap: var(--fc-dl-gap, var(--fc-space-2));
margin: 0;
font-size: var(--fc-font-size-sm);
}
.fc-dl__item {
display: flex;
flex-wrap: wrap;
column-gap: var(--fc-space-4);
row-gap: var(--fc-space-0-5);
padding-block-end: var(--fc-dl-item-pad, 0);
border-block-end: var(--fc-dl-divider, 0) solid var(--fc-color-border-subtle);
}
.fc-dl__item:last-child {
padding-block-end: 0;
border-block-end: 0;
}
.fc-dl dt {
flex: 0 0 var(--fc-dl-term-size, 10rem);
/* A long term wraps inside its column rather than pushing the value off. */
min-inline-size: 0;
overflow-wrap: anywhere;
color: var(--fc-color-text-muted);
font-weight: var(--fc-font-weight-medium);
}
.fc-dl dd {
/* The basis is where the value gives up and wraps under its term. */
flex: 1 1 var(--fc-dl-value-min, 12rem);
min-inline-size: 0;
margin: 0;
color: var(--fc-color-text-default);
/* Ids, hashes and URLs have no break opportunities and would overflow. */
overflow-wrap: anywhere;
}
.fc-dl__num {
font-variant-numeric: tabular-nums;
}
/* ---- Modifiers ------------------------------------------------------- */
.fc-dl--divided {
--fc-dl-gap: var(--fc-space-3);
--fc-dl-item-pad: var(--fc-space-3);
--fc-dl-divider: var(--fc-border-width-thin);
}
/* Term above value at every width: dense panels and long terms. */
.fc-dl--stacked {
--fc-dl-term-size: 100%;
}
/*
* Reduced motion and forced colours: nothing here moves, and nothing carries
* meaning in a background colour; the divider is a border, which forced
* colours keep.
*/
}
/*!
* future-css -- navigation sidebar
*
* The app's primary navigation: a column of links, optionally grouped, that
* can collapse to an icon rail.
*
* <nav class="fc-nav" id="app-nav" aria-label="Main">
* <div class="fc-nav__header">
* <span class="fc-nav__title">Workspace</span>
* <button class="fc-nav__toggle" type="button" aria-label="Sidebar"
* aria-expanded="true" aria-controls="app-nav"></button>
* </div>
* <ul class="fc-nav__list">
* <li>
* <a class="fc-nav__link" href="/chats" aria-current="page">
* <svg class="fc-nav__icon" aria-hidden="true">…</svg>
* <span class="fc-nav__label">Chats</span>
* <span class="fc-nav__meta">3</span>
* </a>
* </li>
* </ul>
* <details class="fc-nav__group" open>
* <summary class="fc-nav__group-label">Projects</summary>
* <ul class="fc-nav__list">…</ul>
* </details>
* </nav>
*
* Links, not a menu or a tree: every destination has a URL, the Tab key walks
* them, and screen readers list them as the page's navigation landmark.
* `role="tree"` would demand arrow-key navigation users do not expect from a
* sidebar, and would hide the links from link lists.
*
* The current page is `aria-current="page"`. An ancestor of it (a section whose
* child page is open) is `aria-current="true"`, so the path stays visible.
*
* Collapsing is the toggle's `aria-expanded`, and only that: the rail reads
* the attribute through `:has()`, so the announced state and the drawn state
* cannot disagree. The `nav` binding flips it; without the binding the
* sidebar simply stays expanded.
*/
@layer fc.components {
.fc-nav {
display: flex;
flex-direction: column;
gap: var(--fc-space-4);
inline-size: var(--fc-nav-size, 16rem);
max-inline-size: 100%;
block-size: 100%;
padding: var(--fc-space-3) var(--fc-space-2);
background-color: var(--fc-color-bg-subtle);
border-inline-end: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
overflow-x: hidden;
overflow-y: auto;
scrollbar-width: thin;
font-size: var(--fc-font-size-sm);
transition: inline-size var(--fc-duration-normal) var(--fc-easing-standard);
}
/*
* Collapsed to a rail. Everything that changes reads a custom property set
* here, so the parts need no descendant selectors and a consumer can
* restyle the rail by setting the same properties.
*/
.fc-nav:has(.fc-nav__toggle[aria-expanded="false"]) {
--fc-nav-size: calc(var(--fc-size-target-comfortable) + var(--fc-space-4));
--fc-nav-text-display: none;
--fc-nav-nested-display: none;
--fc-nav-toggle-turn: 180deg;
--fc-nav-header-pad: 0;
/* Visually hidden, but still each link's accessible name. */
--fc-nav-label-position: absolute;
--fc-nav-label-size: var(--fc-space-px);
--fc-nav-label-clip: inset(50%);
--fc-nav-label-wrap: nowrap;
}
.fc-nav__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--fc-space-2);
padding-inline-start: var(--fc-nav-header-pad, var(--fc-space-2));
}
.fc-nav__title {
display: var(--fc-nav-text-display, block);
min-inline-size: 0;
font-weight: var(--fc-font-weight-semibold);
color: var(--fc-color-text-default);
}
/* ---- Toggle ---------------------------------------------------------- */
.fc-nav__toggle {
display: inline-flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
inline-size: var(--fc-size-target-comfortable);
block-size: var(--fc-size-target-comfortable);
padding: 0;
background-color: transparent;
color: var(--fc-color-text-muted);
border: 0;
border-radius: var(--fc-radius-md);
cursor: pointer;
transition:
color var(--fc-duration-fast) var(--fc-easing-standard),
background-color var(--fc-duration-fast) var(--fc-easing-standard);
}
/*
* A double chevron pointing at the edge the sidebar collapses toward, which
* is the inline start: left in LTR, right in RTL. It turns to point outward
* once collapsed.
*/
.fc-nav__toggle::before {
content: "\00AB" / "";
/* Inline boxes ignore `rotate`; without this the glyph never turns. */
display: inline-block;
font-size: var(--fc-font-size-lg);
line-height: var(--fc-line-height-none);
rotate: var(--fc-nav-toggle-turn, 0deg);
transition: rotate var(--fc-duration-normal) var(--fc-easing-standard);
}
.fc-nav__toggle:dir(rtl)::before {
content: "\00BB" / "";
}
.fc-nav__toggle:hover {
color: var(--fc-color-text-default);
background-color: var(--fc-color-bg-muted);
}
.fc-nav__toggle:active {
background-color: var(--fc-color-bg-surface);
}
.fc-nav__toggle:disabled,
.fc-nav__toggle[aria-disabled="true"] {
color: var(--fc-color-disabled-fg);
background-color: transparent;
cursor: not-allowed;
}
/* ---- Lists and groups ------------------------------------------------ */
.fc-nav__list {
display: flex;
flex-direction: column;
gap: var(--fc-space-0-5);
margin: 0;
padding: 0;
list-style: none;
}
/* A second level, under the item it belongs to. */
.fc-nav__list--nested {
display: var(--fc-nav-nested-display, flex);
padding-block-start: var(--fc-space-0-5);
padding-inline-start: var(--fc-space-6);
}
/* A static section label. Use a group when the section should fold. */
.fc-nav__heading {
display: var(--fc-nav-text-display, block);
margin: 0 0 var(--fc-space-1);
padding-inline: var(--fc-space-2);
color: var(--fc-color-text-muted);
font-size: var(--fc-font-size-xs);
font-weight: var(--fc-font-weight-semibold);
}
/*
* A foldable section on <details>, so it is keyboard-operable, announced as
* expandable, and works with no script.
*/
.fc-nav__group-label {
display: var(--fc-nav-text-display, flex);
align-items: center;
justify-content: space-between;
gap: var(--fc-space-2);
min-block-size: var(--fc-size-target-min);
margin-block-end: var(--fc-space-1);
padding-inline: var(--fc-space-2);
color: var(--fc-color-text-muted);
font-size: var(--fc-font-size-xs);
font-weight: var(--fc-font-weight-semibold);
list-style: none;
border-radius: var(--fc-radius-md);
cursor: pointer;
}
.fc-nav__group-label::-webkit-details-marker {
display: none;
}
.fc-nav__group-label:hover {
color: var(--fc-color-text-default);
background-color: var(--fc-color-bg-muted);
}
/*
* The chevron is two borders of a rotated square. `rotate` is physical and
* the borders are logical, so each state has an RTL branch: closed points
* along the reading direction, open points down in both.
*/
.fc-nav__group-label::after {
content: "";
flex-shrink: 0;
inline-size: 0.4rem;
block-size: 0.4rem;
border-block-end: var(--fc-border-width-thick) solid currentColor;
border-inline-end: var(--fc-border-width-thick) solid currentColor;
rotate: -45deg;
transition: rotate var(--fc-duration-normal) var(--fc-easing-standard);
}
.fc-nav__group-label:dir(rtl)::after {
rotate: 45deg;
}
.fc-nav__group[open] > .fc-nav__group-label::after {
rotate: 45deg;
}
/* The open turn is mirrored too: the border corner is on the other side. */
.fc-nav__group[open] > .fc-nav__group-label:dir(rtl)::after {
rotate: -45deg;
}
/* ---- Links ----------------------------------------------------------- */
.fc-nav__link {
position: relative;
display: flex;
align-items: center;
gap: var(--fc-space-2);
min-block-size: var(--fc-size-target-comfortable);
padding-block: var(--fc-space-1);
padding-inline: var(--fc-space-2);
color: var(--fc-color-text-muted);
text-decoration: none;
border-radius: var(--fc-radius-md);
/*
* The current-page bar is a border, not a background or shadow, so it
* survives forced colours. Transparent until current, so the text does
* not shift when it appears.
*/
border-inline-start: var(--fc-border-width-thick) solid transparent;
transition:
color var(--fc-duration-fast) var(--fc-easing-standard),
background-color var(--fc-duration-fast) var(--fc-easing-standard);
}
.fc-nav__link:hover {
color: var(--fc-color-text-default);
background-color: var(--fc-color-bg-muted);
}
.fc-nav__link:focus-visible {
outline-color: var(--fc-nav-link-focus, var(--fc-nav-current-focus, var(--fc-color-focus-ring)));
}
.fc-nav__link:active {
background-color: var(--fc-color-bg-surface);
}
.fc-nav__link[aria-current="page"] {
color: var(--fc-nav-selected-fg, var(--fc-color-accent-text));
background-color: var(--fc-nav-selected-bg, var(--fc-color-accent-bg-subtle));
/*
* The selected surface can be a solid fill, so its count and ring follow the
* selected text by default. These are private names: assigning the public
* slots here would discard a value the consumer set on an ancestor.
*/
--fc-nav-current-meta: var(--fc-nav-selected-fg, var(--fc-color-text-subtle));
--fc-nav-current-focus: var(--fc-nav-selected-fg, var(--fc-color-focus-ring));
border-inline-start-color: var(--fc-color-accent-border);
font-weight: var(--fc-font-weight-medium);
}
/* The section that contains the current page: on the path, not the page. */
.fc-nav__link[aria-current="true"] {
color: var(--fc-color-text-default);
font-weight: var(--fc-font-weight-medium);
}
/*
* A destination that exists but cannot be opened (no access, not set up).
* An anchor without href plus aria-disabled keeps it in the list so the
* user learns it exists.
*/
.fc-nav__link[aria-disabled="true"] {
color: var(--fc-color-disabled-fg);
background-color: transparent;
cursor: not-allowed;
}
.fc-nav__icon {
flex-shrink: 0;
inline-size: 1.25rem;
block-size: 1.25rem;
}
/*
* Labels wrap rather than truncate, so a long project name is never clipped
* without a `data-fc-truncated` declaration. In the rail they are visually
* hidden through the properties above and still name the link.
*/
.fc-nav__label {
flex: 1 1 auto;
min-inline-size: 0;
position: var(--fc-nav-label-position, static);
inline-size: var(--fc-nav-label-size, auto);
block-size: var(--fc-nav-label-size, auto);
overflow: hidden;
clip-path: var(--fc-nav-label-clip, none);
white-space: var(--fc-nav-label-wrap, normal);
overflow-wrap: anywhere;
}
/* A count or status at the end of a link: unread threads, failing runs. */
.fc-nav__meta {
display: var(--fc-nav-text-display, inline-block);
flex-shrink: 0;
min-inline-size: 1.25rem;
padding-inline: var(--fc-space-1-5);
background-color: var(--fc-color-bg-muted);
color: var(--fc-nav-meta-fg, var(--fc-nav-current-meta, var(--fc-color-text-muted)));
border-radius: var(--fc-radius-full);
font-size: var(--fc-font-size-xs);
font-variant-numeric: tabular-nums;
text-align: center;
}
/* Settings, account: pinned to the bottom of a full-height sidebar. */
.fc-nav__footer {
margin-block-start: auto;
padding-block-start: var(--fc-space-3);
border-block-start: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
}
/*
* Reduced motion: the rail's width change, the toggle's turn, and the group
* chevron are transitions, which the reset's reduced-motion block zeroes,
* so collapsing is instant. Nothing here animates on its own.
*/
/*
* Forced colours drop backgrounds, so the current page keeps its
* inline-start bar in Highlight and a disabled link reads as GrayText.
*/
@media (forced-colors: active) {
.fc-nav__link[aria-current="page"] {
color: LinkText;
border-inline-start-color: Highlight;
}
.fc-nav__link[aria-disabled="true"],
.fc-nav__toggle:disabled,
.fc-nav__toggle[aria-disabled="true"] {
color: GrayText;
}
.fc-nav__meta {
border: var(--fc-border-width-thin) solid CanvasText;
}
}
}
/*!
* future-css -- masthead
*
* An application header, independent of the page layout. Links keep normal
* navigation semantics; buttons, search, and menus reuse their own components.
* Wrap rather than hide destinations when a narrow container runs out of room.
*/
@layer fc.components {
.fc-masthead {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--fc-space-3) var(--fc-space-6);
min-inline-size: 0;
padding: var(--fc-space-3) var(--fc-space-4);
background-color: var(--fc-masthead-bg, var(--fc-color-bg-surface));
color: var(--fc-masthead-fg, var(--fc-color-text-default));
border-block-end: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
}
.fc-masthead__brand {
display: inline-flex;
align-items: center;
gap: var(--fc-space-3);
min-inline-size: var(--fc-size-target-min);
min-block-size: var(--fc-size-target-comfortable);
max-inline-size: 100%;
padding-inline: var(--fc-space-1);
color: var(--fc-masthead-fg, var(--fc-color-text-default));
border-radius: var(--fc-radius-md);
font-weight: var(--fc-font-weight-semibold);
text-decoration: none;
overflow-wrap: anywhere;
}
.fc-masthead__mark {
display: grid;
place-items: center;
flex-shrink: 0;
inline-size: var(--fc-size-target-comfortable);
block-size: var(--fc-size-target-comfortable);
background-color: var(--fc-color-accent-bg);
color: var(--fc-color-text-on-accent);
border-radius: var(--fc-radius-lg);
font-size: var(--fc-font-size-sm);
}
.fc-masthead__nav {
display: flex;
flex-wrap: wrap;
gap: var(--fc-space-1);
min-inline-size: 0;
margin: 0;
padding: 0;
list-style: none;
}
.fc-masthead__link {
display: inline-flex;
align-items: center;
min-inline-size: var(--fc-size-target-min);
min-block-size: var(--fc-size-target-comfortable);
padding: var(--fc-space-2) var(--fc-space-3);
border: var(--fc-border-width-thin) solid transparent;
border-radius: var(--fc-radius-md);
color: var(--fc-masthead-muted, var(--fc-color-text-muted));
font-size: var(--fc-font-size-sm);
font-weight: var(--fc-font-weight-medium);
text-decoration: none;
overflow-wrap: anywhere;
}
.fc-masthead__brand:hover,
.fc-masthead__link:hover {
background-color: var(--fc-masthead-interaction-bg, var(--fc-color-bg-subtle));
color: var(--fc-masthead-fg, var(--fc-color-text-default));
}
.fc-masthead__brand:active,
.fc-masthead__link:active {
background-color: var(--fc-masthead-interaction-bg, var(--fc-color-bg-muted));
}
.fc-masthead__link[aria-current="page"] {
background-color: var(--fc-color-accent-bg-subtle);
color: var(--fc-color-accent-text);
border-block-end-color: var(--fc-color-accent-border);
}
.fc-masthead__brand:focus-visible,
.fc-masthead__link:focus-visible {
outline: var(--fc-border-width-focus) solid var(--fc-masthead-focus, var(--fc-color-focus-ring));
outline-offset: var(--fc-space-0-5);
}
.fc-masthead__brand:disabled,
.fc-masthead__brand[aria-disabled="true"],
.fc-masthead__link:disabled,
.fc-masthead__link[aria-disabled="true"] {
background-color: transparent;
color: var(--fc-masthead-disabled-fg, var(--fc-color-disabled-fg));
border-color: transparent;
opacity: 1;
cursor: not-allowed;
}
.fc-masthead__actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--fc-space-2);
min-inline-size: 0;
margin-inline-start: auto;
}
/* No autonomous motion or lifecycle: child controls own loading and errors.
Disabled links should omit href; aria-disabled alone does not cancel navigation. */
@media (forced-colors: active) {
.fc-masthead {
border-color: CanvasText;
}
.fc-masthead__link[aria-current="page"] {
color: LinkText;
border-block-end-color: Highlight;
}
.fc-masthead__mark {
background-color: Highlight;
color: HighlightText;
}
.fc-masthead__brand[aria-disabled="true"],
.fc-masthead__link[aria-disabled="true"],
.fc-masthead__brand:disabled,
.fc-masthead__link:disabled {
color: GrayText;
opacity: 1;
}
}
}
/*!
* future-css -- control group
*
* A named collection of related controls, not a toolbar keyboard model.
* Buttons and selects retain their own states and Tab stops. Use role="group"
* with an accessible name, or a fieldset/legend for related form fields.
*/
@layer fc.components {
.fc-control-group {
display: inline-flex;
flex-direction: var(--fc-control-group-direction, row);
flex-wrap: wrap;
align-items: var(--fc-control-group-align, center);
gap: var(--fc-space-1);
min-inline-size: 0;
max-inline-size: 100%;
margin: 0;
padding: var(--fc-space-1);
background-color: var(--fc-color-bg-subtle);
color: var(--fc-color-text-default);
border: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
border-radius: var(--fc-radius-lg);
--fc-select-block-size: 2rem;
}
/* Joined parts keep their own borders and focus rings; clipping the wrapper
would cut off the focus indicator at the outer edges. */
.fc-control-group--joined {
flex-wrap: nowrap;
align-items: stretch;
inline-size: 100%;
padding: 0;
gap: 0;
--fc-btn-press-scale: 1;
background-color: transparent;
border: 0;
}
.fc-control-group__joined-item {
flex: 0 1 auto;
min-inline-size: var(--fc-size-target-min);
border-radius: var(--fc-joined-inner-radius, 0);
white-space: normal;
overflow-wrap: anywhere;
}
.fc-control-group__joined-item:first-child {
border-start-start-radius: min(var(--fc-btn-radius, var(--fc-radius-lg)), var(--fc-btn-corner-limit, calc(var(--fc-size-target-comfortable) / 2)));
border-end-start-radius: min(var(--fc-btn-radius, var(--fc-radius-lg)), var(--fc-btn-corner-limit, calc(var(--fc-size-target-comfortable) / 2)));
}
.fc-control-group__joined-item:last-child {
border-start-end-radius: min(var(--fc-btn-radius, var(--fc-radius-lg)), var(--fc-btn-corner-limit, calc(var(--fc-size-target-comfortable) / 2)));
border-end-end-radius: min(var(--fc-btn-radius, var(--fc-radius-lg)), var(--fc-btn-corner-limit, calc(var(--fc-size-target-comfortable) / 2)));
}
.fc-control-group__control { flex: 1 1 8rem; inline-size: 0; }
/* Overlapping one border removes doubled seams without clipping focus rings. */
.fc-control-group__joined-item:not(:first-child) {
margin-inline-start: calc(-1 * var(--fc-border-width-control, var(--fc-border-width-thin)));
}
.fc-control-group__joined-item:focus-visible {
position: relative;
z-index: var(--fc-z-raised);
}
.fc-control-group--plain {
padding: 0;
background-color: transparent;
border-color: transparent;
}
.fc-control-group--vertical {
--fc-control-group-direction: column;
--fc-control-group-align: stretch;
}
.fc-control-group__label {
padding-inline: var(--fc-space-2);
color: var(--fc-color-text-muted);
font-size: var(--fc-font-size-sm);
font-weight: var(--fc-font-weight-medium);
overflow-wrap: anywhere;
}
.fc-control-group__item {
inline-size: auto;
min-inline-size: 0;
max-inline-size: 100%;
white-space: normal;
overflow-wrap: anywhere;
}
/* Pressed state belongs to the real control, never a parallel selected class. */
.fc-control-group__item[aria-pressed="true"] {
background-color: var(--fc-color-selected-bg);
color: var(--fc-color-selected-fg);
border-color: var(--fc-color-accent-border);
font-weight: var(--fc-font-weight-semibold);
}
.fc-control-group__item[aria-pressed="true"]:hover:not(:disabled, [aria-disabled="true"]) {
text-decoration: underline;
text-underline-offset: 0.2em;
}
.fc-control-group__item[aria-pressed="true"]:active:not(:disabled, [aria-disabled="true"]) {
box-shadow: inset 0 0 0 var(--fc-border-width-thin) var(--fc-color-accent-border);
}
.fc-control-group__item:disabled,
.fc-control-group__item[aria-disabled="true"] {
background-color: var(--fc-color-disabled-bg);
color: var(--fc-color-disabled-fg);
border-color: var(--fc-color-disabled-border);
cursor: not-allowed;
}
/* The wrapper has no hover, active, or loading state: its controls own those
states and focus rings. No autonomous motion is introduced here. */
@media (forced-colors: active) {
.fc-control-group {
border-color: CanvasText;
}
.fc-control-group__item[aria-pressed="true"] {
background-color: Highlight;
color: HighlightText;
border-color: Highlight;
}
.fc-control-group__item:disabled,
.fc-control-group__item[aria-disabled="true"] {
color: GrayText;
border-color: GrayText;
}
}
}
/*!
* future-css -- connected button group
*
* Material's connected button group: a short row of related buttons drawn as
* one joined shape, for a few peer actions or a toggle set that should read
* as one control (text alignment, view density, playback speed).
*
* <div class="fc-button-group" role="group" aria-label="Alignment">
* <button class="fc-btn fc-btn--secondary fc-button-group__item" type="button" aria-pressed="true">Start</button>
* <button class="fc-btn fc-btn--secondary fc-button-group__item" type="button" aria-pressed="false">Center</button>
* <button class="fc-btn fc-btn--secondary fc-button-group__item" type="button" aria-pressed="false">End</button>
* </div>
*
* Every button keeps its own Tab stop and its own state: this is a visual
* join, not a composite widget. A set where exactly one value is chosen and
* submitted with a form is `fc-segmented`, on native radios; a loose cluster
* of mixed controls is `fc-control-group`, with an optional joined modifier.
*
* Dedicated parts keep position-specific corners independent of the general
* control group and its mixed fields. Load after `button.css` (the bundle
* does): the corners win at equal specificity by coming later.
*/
@layer fc.components {
.fc-button-group {
display: inline-flex;
align-items: stretch;
/* Each adjoining border shares one seam. */
gap: 0;
--fc-btn-press-scale: 1;
min-inline-size: 0;
max-inline-size: 100%;
}
/*
* Inner corners are tight and outer corners round, so the row reads as one
* continuous shape separated into targets. Logical corner longhands keep the round ends on
* the outside in RTL.
*/
.fc-button-group__item {
flex: 1 1 auto;
min-inline-size: 0;
border-radius: var(--fc-joined-inner-radius, 0);
/* fc-btn never wraps; a long or translated label wraps inside its own
segment rather than pushing the row past a narrow screen. */
white-space: normal;
overflow-wrap: anywhere;
}
.fc-button-group__item:first-child {
border-start-start-radius: min(var(--fc-btn-radius, var(--fc-radius-lg)), var(--fc-btn-corner-limit, calc(var(--fc-size-target-comfortable) / 2)));
border-end-start-radius: min(var(--fc-btn-radius, var(--fc-radius-lg)), var(--fc-btn-corner-limit, calc(var(--fc-size-target-comfortable) / 2)));
}
.fc-button-group__item:last-child {
border-start-end-radius: min(var(--fc-btn-radius, var(--fc-radius-lg)), var(--fc-btn-corner-limit, calc(var(--fc-size-target-comfortable) / 2)));
border-end-end-radius: min(var(--fc-btn-radius, var(--fc-radius-lg)), var(--fc-btn-corner-limit, calc(var(--fc-size-target-comfortable) / 2)));
}
/*
* Selection changes the fill and pressed semantics without breaking the seam.
* aria-pressed carries the state independently of color.
*/
.fc-button-group__item[aria-pressed="true"] {
background-color: var(--fc-color-selected-bg);
color: var(--fc-color-selected-fg);
border-color: var(--fc-color-accent-border);
}
/* The fill above outranks the button's hover fill, so hover on the pressed
segment needs a cue of its own. */
@media (hover: hover) {
.fc-button-group__item[aria-pressed="true"]:hover:not(:disabled, [aria-disabled="true"]) {
text-decoration: underline;
text-underline-offset: 0.2em;
}
}
.fc-button-group__item:disabled,
.fc-button-group__item[aria-disabled="true"] {
background-color: var(--fc-color-disabled-bg);
color: var(--fc-color-disabled-fg);
border-color: var(--fc-color-disabled-border);
cursor: not-allowed;
}
/* Overlapping one border removes doubled seams without clipping focus rings. */
.fc-button-group__item:not(:first-child) {
margin-inline-start: calc(-1 * var(--fc-border-width-control, var(--fc-border-width-thin)));
}
.fc-button-group__item:focus-visible {
position: relative;
z-index: var(--fc-z-raised);
}
/* ---- Forced colors --------------------------------------------------- */
@media (forced-colors: active) {
.fc-button-group__item[aria-pressed="true"] {
background-color: Highlight;
color: HighlightText;
border-color: Highlight;
}
/* A pressed button can also be disabled; the Highlight fill above must
not survive behind GrayText. */
.fc-button-group__item:disabled,
.fc-button-group__item[aria-disabled="true"] {
background-color: ButtonFace;
color: GrayText;
border-color: GrayText;
}
}
/* Hover, active, focus and loading come from fc-btn. Motion: the
transitions above, zeroed by the reset under reduced motion. */
}
/*!
* future-css -- segmented button
*
* A row of joined options that picks a VALUE: a sort order, a unit, a set of
* filters. Material calls it a segmented button. It is not the segmented tabs
* list (`fc-tabs__list--segmented`), which switches between VIEWS and owns
* panels; using tabs to pick a value announces panels that do not exist.
*
* <fieldset class="fc-segmented">
* <legend class="fc-u-visually-hidden">Units</legend>
* <label class="fc-segmented__item">
* <input class="fc-segmented__input" type="radio" name="unit" value="c" checked>
* <span class="fc-segmented__check" aria-hidden="true"></span>Celsius
* </label>
* ...
* </fieldset>
*
* Built on real radio inputs (one choice) or checkboxes (several), so the
* value is a form value, arrow keys move a radio group for free, and the
* selected state is the input's `:checked` -- there is no class to fall out of
* sync with what the form submits. A row too narrow for its labels wraps the
* labels, never scrolls: a scrolled row hides options without saying so.
*
* Motion is limited to colour transitions, which the reset's reduced-motion
* block already zeroes.
*/
@layer fc.components {
.fc-segmented {
display: inline-flex;
max-inline-size: 100%;
margin: 0;
padding: 0;
border: 0;
min-inline-size: 0;
/* Its own stacking context, so lifting a segment over its neighbours'
shared edge can never lift it over the page. */
isolation: isolate;
}
.fc-segmented__item {
position: relative;
display: inline-flex;
/*
* Segments share the row and their labels wrap inside them, rather than
* the row wrapping: a wrapped row breaks the joined shape into pieces
* whose rounded ends no longer mean "first" and "last".
*/
flex: 1 1 auto;
min-inline-size: var(--fc-size-target-comfortable);
overflow-wrap: anywhere;
align-items: center;
justify-content: center;
gap: var(--fc-space-1-5);
/* The shared control height, so a segmented button lines up with a field. */
min-block-size: var(--fc-size-target-comfortable);
padding-block: var(--fc-space-2);
padding-inline: var(--fc-space-4);
background-color: var(--fc-color-bg-surface);
color: var(--fc-color-text-default);
border: var(--fc-border-width-thin) solid var(--fc-segmented-border, var(--fc-color-border-interactive));
font-size: var(--fc-font-size-sm);
font-weight: var(--fc-font-weight-medium);
line-height: var(--fc-line-height-snug);
text-align: center;
cursor: pointer;
user-select: none;
touch-action: manipulation;
transition:
background-color var(--fc-duration-fast) var(--fc-easing-standard),
color var(--fc-duration-fast) var(--fc-easing-standard);
}
/*
* Neighbours share one border: each item after the first pulls back over the
* previous one's edge, so the joins are 1px, not 2px.
*/
.fc-segmented__item + .fc-segmented__item {
margin-inline-start: calc(var(--fc-border-width-thin) * -1);
}
/*
* The outer corners only. Logical corner longhands, so the rounded ends swap
* sides in RTL with no direction rule.
*/
.fc-segmented__item:first-of-type {
border-start-start-radius: var(--fc-radius-full);
border-end-start-radius: var(--fc-radius-full);
padding-inline-start: var(--fc-space-5);
}
.fc-segmented__item:last-of-type {
border-start-end-radius: var(--fc-radius-full);
border-end-end-radius: var(--fc-radius-full);
padding-inline-end: var(--fc-space-5);
}
/*
* The input covers its item: the whole segment is the hit target and the
* native input still receives focus, arrow keys and form submission. Opacity
* rather than display:none, which would remove it from the tab order.
*/
.fc-segmented__input {
position: absolute;
inset: 0;
inline-size: 100%;
block-size: 100%;
margin: 0;
opacity: 0;
cursor: inherit;
}
/*
* The check mark appears on selection, so the selected state is carried by a
* shape and not by fill colour alone (WCAG 1.4.1). It takes no space while
* hidden, so labels do not shift sideways until chosen.
*/
.fc-segmented__check {
display: var(--fc-segmented-check, none);
inline-size: 0.6em;
block-size: 1em;
border-block-end: var(--fc-border-width-thick) solid currentColor;
border-inline-end: var(--fc-border-width-thick) solid currentColor;
rotate: 45deg;
translate: 0 -0.1em;
/* A check mark does not mirror. Its borders are logical, so the span
pins its own direction: the stroke stays on the physical right under
a dir="rtl" page and inside an LTR-isolated row such as the time
picker, where a :dir(rtl) correction would flip it the wrong way. */
direction: ltr;
}
/* The item tells its check mark to show through a property the part reads,
rather than a descendant selector reaching into it. */
.fc-segmented__item:has(:checked) {
--fc-segmented-check: block;
z-index: var(--fc-z-raised);
background-color: var(--fc-color-selected-bg);
color: var(--fc-color-selected-fg);
}
@media (hover: hover) {
.fc-segmented__item:hover {
background-color: color-mix(in oklab, currentColor calc(var(--fc-opacity-hover-overlay) * 100%), var(--fc-color-bg-surface));
}
.fc-segmented__item:has(:checked):hover {
background-color: color-mix(in oklab, currentColor calc(var(--fc-opacity-hover-overlay) * 100%), var(--fc-color-selected-bg));
}
}
.fc-segmented__item:active {
background-color: color-mix(in oklab, currentColor calc(var(--fc-opacity-pressed-overlay) * 100%), var(--fc-color-bg-surface));
}
/* The ring is drawn on the segment, since the input itself is invisible. */
.fc-segmented__item:has(:focus-visible) {
z-index: var(--fc-z-sticky);
outline: var(--fc-border-width-thick) solid var(--fc-color-focus-ring);
outline-offset: var(--fc-border-width-thick);
}
.fc-segmented__item:has(:disabled) {
background-color: var(--fc-color-disabled-bg);
color: var(--fc-color-disabled-fg);
border-color: var(--fc-color-disabled-border);
cursor: not-allowed;
}
/* An invalid group (a required choice left empty) outlines every segment. */
.fc-segmented:has([aria-invalid="true"], :user-invalid) {
--fc-segmented-border: var(--fc-color-danger-border);
}
/* ---- Forced colors --------------------------------------------------- */
/*
* The selected fill is discarded in forced colors; the check mark survives
* because it is a border, and Highlight restates the selection.
*/
@media (forced-colors: active) {
.fc-segmented__item {
border-color: ButtonText;
}
.fc-segmented__item:has(:checked) {
background-color: Highlight;
color: HighlightText;
forced-color-adjust: none;
}
.fc-segmented__item:has(:disabled) {
color: GrayText;
border-color: GrayText;
}
}
}
/*!
* future-css -- split button
*
* A default action joined to a menu of alternatives: Send / Send later, Merge /
* Squash and merge. Two real buttons, because they are two actions with two
* accessible names; one button with a hot zone would give a screen reader one
* name for both.
*
* <div class="fc-split-btn" role="group" aria-label="Send">
* <button class="fc-btn fc-btn--primary fc-split-btn__action" type="button">Send</button>
* <button class="fc-btn fc-btn--primary fc-btn--icon fc-split-btn__toggle" type="button"
* aria-haspopup="menu" aria-expanded="false" aria-controls="send-menu"
* aria-label="More send options">
* <span class="fc-split-btn__chevron" aria-hidden="true"></span>
* </button>
* </div>
* <div class="fc-menu" id="send-menu" role="menu" popover>…</div>
*
* Colour, size and every interaction state come from `.fc-btn` and its
* variants; this file only joins the pair. The menu is `fc-menu` with its
* binding, which owns `aria-expanded` and focus. Load this file after
* `button.css` (the bundle does), since the joined corners override the
* button's own radius at equal specificity.
*/
@layer fc.components {
.fc-split-btn {
display: inline-flex;
align-items: stretch;
/* Adjoining borders form the separator without an extra painted gap. */
gap: 0;
--fc-btn-press-scale: 1;
max-inline-size: 100%;
}
/* Logical corner longhands, so the seam stays between the two in RTL. */
.fc-split-btn__action {
border-start-start-radius: min(var(--fc-btn-radius, var(--fc-radius-lg)), var(--fc-btn-corner-limit, calc(var(--fc-size-target-comfortable) / 2)));
border-end-start-radius: min(var(--fc-btn-radius, var(--fc-radius-lg)), var(--fc-btn-corner-limit, calc(var(--fc-size-target-comfortable) / 2)));
border-start-end-radius: var(--fc-joined-inner-radius, 0);
border-end-end-radius: var(--fc-joined-inner-radius, 0);
min-inline-size: 0;
/* fc-btn never wraps; a shrunk action would paint its label over the
toggle, so a long or translated label wraps inside it instead. */
white-space: normal;
overflow-wrap: anywhere;
}
.fc-split-btn__toggle {
border-start-end-radius: min(var(--fc-btn-radius, var(--fc-radius-lg)), var(--fc-btn-corner-limit, calc(var(--fc-size-target-comfortable) / 2)));
border-end-end-radius: min(var(--fc-btn-radius, var(--fc-radius-lg)), var(--fc-btn-corner-limit, calc(var(--fc-size-target-comfortable) / 2)));
border-start-start-radius: var(--fc-joined-inner-radius, 0);
border-end-start-radius: var(--fc-joined-inner-radius, 0);
}
/*
* The toggle's open state is its `aria-expanded`, the attribute the menu
* binding already maintains, so the drawn and announced states agree. Open,
* the chevron flips while the joined edge retains its shape.
*/
.fc-split-btn__toggle[aria-expanded="true"] {
--fc-split-btn-chevron: 225deg;
}
/* Overlapping one border removes doubled seams without clipping focus rings. */
.fc-split-btn__action:not(:first-child) {
margin-inline-start: calc(-1 * var(--fc-border-width-control, var(--fc-border-width-thin)));
}
.fc-split-btn__action:focus-visible {
position: relative;
z-index: var(--fc-z-raised);
}
.fc-split-btn__toggle:not(:first-child) {
margin-inline-start: calc(-1 * var(--fc-border-width-control, var(--fc-border-width-thin)));
}
.fc-split-btn__toggle:focus-visible {
position: relative;
z-index: var(--fc-z-raised);
}
.fc-split-btn__chevron {
inline-size: 0.5em;
block-size: 0.5em;
border-block-end: var(--fc-border-width-thick) solid currentColor;
border-inline-end: var(--fc-border-width-thick) solid currentColor;
rotate: var(--fc-split-btn-chevron, 45deg);
translate: 0 -0.15em;
transition: rotate var(--fc-duration-fast) var(--fc-easing-standard);
}
/* The chevron points down in both directions; see `.fc-segmented__check`. */
.fc-split-btn__chevron:dir(rtl) {
border-inline-end: 0;
border-inline-start: var(--fc-border-width-thick) solid currentColor;
}
/*
* Reduced motion: the chevron flips without turning. The reset zeroes the
* duration; stated here because the flip is the open-state cue.
*/
@media (prefers-reduced-motion: reduce) {
.fc-split-btn__chevron {
transition: none;
}
}
/* Forced colors: each half is a `.fc-btn`, whose forced-colors block draws
its border; the shared border remains visible. */
@media (forced-colors: active) {
.fc-split-btn__toggle[aria-expanded="true"] {
border-color: Highlight;
}
}
}
/*!
* future-css -- floating action button and FAB menu
*
* The one most important action on a screen, floating above the content:
* compose, new chat, add. One per screen; two FABs compete and neither wins.
*
* <button class="fc-fab" type="button" aria-label="New chat">
* <svg class="fc-fab__icon" aria-hidden="true">…</svg>
* </button>
*
* <button class="fc-fab fc-fab--extended" type="button">
* <svg class="fc-fab__icon" aria-hidden="true">…</svg>New chat
* </button>
*
* An icon-only FAB needs `aria-label`: the glyph is decorative and the button
* otherwise has no name. The extended form carries its own text, which is why
* it is the better choice whenever there is room.
*
* FAB MENU. A FAB that opens a short column of related actions, on the native
* popover API, so light dismiss, Escape and returning focus to the FAB come
* from the platform:
*
* <button class="fc-fab" popovertarget="create" aria-label="Create">+</button>
* <div class="fc-fab-menu" id="create" popover>
* <button class="fc-fab-menu__item" type="button">Document</button>
* <button class="fc-fab-menu__item" type="button">Folder</button>
* </div>
*
* Items are real buttons in tab order, not a role="menu" -- three to six
* actions do not need a menu's arrow-key contract, and Tab is what users try.
*/
@layer fc.components {
.fc-fab {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--fc-space-3);
/* 3.5rem: well past the comfortable target, because a FAB is aimed at
with a thumb, often one-handed. */
min-block-size: 3.5rem;
min-inline-size: 3.5rem;
padding-inline: var(--fc-space-4);
background-color: var(--fc-color-accent-bg-subtle);
color: var(--fc-color-accent-text);
border: var(--fc-border-width-thin) solid transparent;
border-radius: var(--fc-radius-xl);
box-shadow: var(--fc-shadow-floating);
font-family: inherit;
font-size: var(--fc-font-size-md);
font-weight: var(--fc-font-weight-medium);
line-height: var(--fc-line-height-snug);
/* A translated label wraps inside the FAB rather than pushing it off the
screen; a FAB that overflows has lost the one job it had. */
max-inline-size: 100%;
text-align: start;
cursor: pointer;
user-select: none;
touch-action: manipulation;
transition:
background-color var(--fc-duration-fast) var(--fc-easing-standard),
box-shadow var(--fc-duration-fast) var(--fc-easing-standard),
scale var(--fc-duration-fast) var(--fc-easing-standard);
}
/* Sized in em by font-size, so an SVG and a glyph icon come out the same. */
.fc-fab__icon {
display: inline-grid;
place-items: center;
inline-size: 1em;
block-size: 1em;
flex-shrink: 0;
font-size: 1.5rem;
line-height: 1;
}
/* ---- Variants -------------------------------------------------------- */
/* Solid: the strongest colour, for a screen whose surface is already busy. */
.fc-fab--primary {
background-color: var(--fc-color-accent-bg);
color: var(--fc-color-text-on-accent);
}
/* Surface: neutral, for when the accent is already spent on the page. */
.fc-fab--surface {
background-color: var(--fc-color-bg-raised);
color: var(--fc-color-text-default);
border-color: var(--fc-color-border-subtle);
}
/* Lowered: sits on a surface that is itself floating (a sheet, a card). */
.fc-fab--lowered {
box-shadow: var(--fc-shadow-raised);
}
/* ---- Sizes ----------------------------------------------------------- */
.fc-fab--sm {
min-block-size: var(--fc-size-target-comfortable);
min-inline-size: var(--fc-size-target-comfortable);
padding-inline: var(--fc-space-2);
border-radius: var(--fc-radius-lg);
font-size: var(--fc-font-size-sm);
}
.fc-fab--lg {
min-block-size: 6rem;
min-inline-size: 6rem;
border-radius: var(--fc-radius-2xl);
font-size: var(--fc-font-size-lg);
}
/* The label widens the button; the icon keeps its place at the start. */
.fc-fab--extended {
padding-inline: var(--fc-space-4) var(--fc-space-5);
}
/*
* Docked to the block-end, inline-end corner of the viewport. Logical insets
* put it bottom-left in RTL, which is where the thumb of a right-to-left
* reader rests. The safe-area inset keeps it clear of a phone's home bar.
*/
.fc-fab--fixed {
position: fixed;
inset-block-end: max(var(--fc-space-4), env(safe-area-inset-bottom));
inset-inline-end: var(--fc-space-4);
z-index: var(--fc-z-sticky);
}
/* ---- States ---------------------------------------------------------- */
@media (hover: hover) {
.fc-fab:hover {
background-color: color-mix(in oklab, currentColor calc(var(--fc-opacity-hover-overlay) * 100%), var(--fc-color-accent-bg-subtle));
box-shadow: var(--fc-shadow-overlay);
}
.fc-fab--primary:hover {
background-color: var(--fc-color-accent-bg-hover);
}
.fc-fab--surface:hover {
background-color: color-mix(in oklab, currentColor calc(var(--fc-opacity-hover-overlay) * 100%), var(--fc-color-bg-raised));
}
}
.fc-fab:active:not(:disabled, [aria-disabled="true"]) {
scale: 0.96;
box-shadow: var(--fc-shadow-raised);
}
.fc-fab--primary:active {
background-color: var(--fc-color-accent-bg-active);
}
/* The raised shadow would erase the focus ring's offset gap; restated so it
survives, as on `.fc-btn`. */
.fc-fab:focus-visible {
box-shadow:
0 0 0 var(--fc-space-0-5) var(--fc-color-focus-ring-offset),
var(--fc-shadow-floating);
}
/* An open FAB menu turns its FAB into the selected state, so the invoker
shows which menu is open without a separate icon swap. */
.fc-fab:has(+ .fc-fab-menu:popover-open) {
background-color: var(--fc-color-selected-bg);
color: var(--fc-color-selected-fg);
}
.fc-fab:disabled,
.fc-fab[aria-disabled="true"] {
background-color: var(--fc-color-disabled-bg);
color: var(--fc-color-disabled-fg);
border-color: var(--fc-color-disabled-border);
box-shadow: none;
cursor: not-allowed;
}
/* Blocks pointer activation without leaving the tab order. Keyboard
activation is the host's to ignore, as for any aria-disabled fc-btn. */
.fc-fab[aria-disabled="true"],
.fc-fab-menu__item[aria-disabled="true"] {
pointer-events: none;
}
.fc-fab[aria-busy="true"] {
cursor: progress;
}
/* ---- FAB menu -------------------------------------------------------- */
.fc-fab-menu {
flex-direction: column;
align-items: flex-end;
gap: var(--fc-space-2);
margin: 0;
padding: var(--fc-space-2);
background-color: transparent;
border: 0;
overflow: visible;
/*
* Above the FAB, aligned to its inline-end edge, flipping below when there
* is no room. Without anchor positioning the UA centres it, which is
* usable rather than broken.
*/
position-area: block-start span-inline-start;
position-try-fallbacks: flip-block;
opacity: 0;
translate: 0 var(--fc-space-2);
transition:
opacity var(--fc-duration-fast) var(--fc-easing-decelerate),
translate var(--fc-duration-fast) var(--fc-easing-decelerate),
overlay var(--fc-duration-fast) allow-discrete,
display var(--fc-duration-fast) allow-discrete;
}
.fc-fab-menu:popover-open {
display: flex;
opacity: 1;
translate: 0 0;
}
@starting-style {
.fc-fab-menu:popover-open {
opacity: 0;
translate: 0 var(--fc-space-2);
}
}
.fc-fab-menu__item {
display: inline-flex;
align-items: center;
gap: var(--fc-space-2);
min-block-size: 3.5rem;
padding-inline: var(--fc-space-5);
background-color: var(--fc-color-accent-bg);
color: var(--fc-color-text-on-accent);
border: var(--fc-border-width-thin) solid transparent;
border-radius: var(--fc-radius-full);
box-shadow: var(--fc-shadow-floating);
font-family: inherit;
font-size: var(--fc-font-size-md);
font-weight: var(--fc-font-weight-medium);
max-inline-size: 100%;
text-align: start;
cursor: pointer;
}
@media (hover: hover) {
.fc-fab-menu__item:hover {
background-color: var(--fc-color-accent-bg-hover);
}
}
.fc-fab-menu__item:active {
background-color: var(--fc-color-accent-bg-active);
}
.fc-fab-menu__item:focus-visible {
box-shadow:
0 0 0 var(--fc-space-0-5) var(--fc-color-focus-ring-offset),
var(--fc-shadow-floating);
}
.fc-fab-menu__item:disabled,
.fc-fab-menu__item[aria-disabled="true"] {
background-color: var(--fc-color-disabled-bg);
color: var(--fc-color-disabled-fg);
border-color: var(--fc-color-disabled-border);
cursor: not-allowed;
}
/*
* Reduced motion: the menu appears without sliding. The reset zeroes the
* duration already; zeroing the offset too means nothing is drawn mid-move.
*/
@media (prefers-reduced-motion: reduce) {
.fc-fab-menu {
translate: 0 0;
}
}
/* ---- Forced colors --------------------------------------------------- */
/* Shadows are discarded in forced colors, so the FAB needs a border to have
an edge at all. */
@media (forced-colors: active) {
.fc-fab,
.fc-fab-menu__item {
background-color: ButtonFace;
color: ButtonText;
border-color: ButtonText;
}
.fc-fab:has(+ .fc-fab-menu:popover-open) {
background-color: Highlight;
color: HighlightText;
forced-color-adjust: none;
}
.fc-fab:disabled,
.fc-fab-menu__item:disabled {
color: GrayText;
border-color: GrayText;
}
}
}
/*!
* future-css -- divider
*
* A thin rule between groups of content. On `<hr>` when it separates
* paragraphs or sections of a document, which a screen reader announces as a
* thematic break; on an element with `role="separator"` inside a toolbar or a
* row, where the break is visual grouping:
*
* <hr class="fc-divider">
* <hr class="fc-divider fc-divider--inset">
* <div class="fc-divider fc-divider--vertical" role="separator" aria-orientation="vertical"></div>
*
* The bare `<hr>` in base.css stays as the classless default; this adds the
* inset and vertical forms and the tighter spacing a component needs.
*
* A divider is decoration of structure, so it uses the subtle border, which is
* exempt from non-text contrast (WCAG 1.4.11 covers controls and graphics that
* carry meaning, not separators). It has no states and no motion.
*/
@layer fc.components {
.fc-divider {
flex-shrink: 0;
align-self: stretch;
margin: 0;
margin-block: var(--fc-divider-space, var(--fc-space-2));
border: 0;
border-block-start: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
}
/*
* Inset from the inline-start edge by the width of a list's leading slot, so
* the rule starts where the text does. Logical, so it insets from the right
* in RTL.
*/
.fc-divider--inset {
margin-inline-start: var(--fc-divider-inset, var(--fc-space-12));
}
/* Inset from both edges, for a break inside a padded surface. */
.fc-divider--middle {
margin-inline: var(--fc-space-4);
}
/*
* Vertical: a rule between items in a row. `align-self: stretch` makes it as
* tall as the row in a flex container; `min-block-size` keeps it visible in
* a row with no height of its own.
*/
.fc-divider--vertical {
min-block-size: 1em;
margin-block: 0;
margin-inline: var(--fc-divider-space, var(--fc-space-2));
border-block-start: 0;
border-inline-start: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
}
/* Forced colors keep borders but recolour them; restated so the rule is not
lost against a system background that matches the subtle border. */
@media (forced-colors: active) {
.fc-divider {
border-color: CanvasText;
}
}
}
/*!
* future-css -- list
*
* Rows of related items with optional leading media, up to three lines of
* text, and trailing meta or a control: settings, contacts, files, search
* results. Material's list. `fc-nav` and `fc-menu` are lists with one job
* each; this is the general one.
*
* <ul class="fc-list">
* <li class="fc-list__item">
* <span class="fc-list__leading" aria-hidden="true">…</span>
* <span class="fc-list__content">
* <span class="fc-list__overline">Shared with you</span>
* <a class="fc-list__action fc-list__headline" href="/doc/1">Quarterly plan</a>
* <span class="fc-list__supporting">Edited 2 hours ago</span>
* </span>
* <span class="fc-list__trailing">PDF</span>
* </li>
* </ul>
*
* Three kinds of row, from the same parts:
* - Static: no action. Text only.
* - Linked or button row: one `.fc-list__action` (an `<a>` or `<button>`).
* Its ::after stretches over the row, so the whole row is the hit target
* while the accessible name is only the headline -- not every line of the
* row read as one run-on link.
* - Control row: `.fc-list__item` on a `<label>` whose trailing slot holds a
* checkbox or switch, so a click anywhere on the row toggles it.
*
* Selection is `aria-current` or `aria-selected="true"` on the action; there
* is no selected class to drift from what assistive technology announces.
*/
@layer fc.components {
.fc-list {
display: flex;
flex-direction: column;
margin: 0;
padding: 0;
padding-block: var(--fc-space-2);
list-style: none;
}
/*
* Rows separated by a rule. The rule belongs to the gap between rows, so
* the first row has none and nothing doubles against the container's edge.
*/
.fc-list--divided > li + li {
border-block-start: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
}
.fc-list__item {
position: relative;
display: flex;
align-items: center;
gap: var(--fc-space-4);
min-block-size: 3.5rem;
padding-block: var(--fc-space-2);
padding-inline: var(--fc-space-4);
color: var(--fc-color-text-default);
border-radius: var(--fc-radius-md);
transition: background-color var(--fc-duration-fast) var(--fc-easing-standard);
}
/* A label row: the whole row toggles its control. */
label.fc-list__item {
cursor: pointer;
}
/*
* Leading media: an icon, an avatar, or a thumbnail. A fixed slot width so
* headlines align down the list whatever each row's media is.
*/
.fc-list__leading {
display: inline-grid;
place-items: center;
flex-shrink: 0;
min-inline-size: var(--fc-size-target-comfortable);
color: var(--fc-color-text-muted);
}
.fc-list__content {
display: flex;
flex: 1 1 auto;
flex-direction: column;
min-inline-size: 0;
overflow-wrap: anywhere;
}
.fc-list__overline {
color: var(--fc-color-text-muted);
font-size: var(--fc-font-size-xs);
font-weight: var(--fc-font-weight-medium);
line-height: var(--fc-line-height-snug);
}
.fc-list__headline {
color: inherit;
font-size: var(--fc-font-size-md);
line-height: var(--fc-line-height-snug);
text-decoration: none;
}
/*
* Supporting text clamps at two lines, which is Material's three-line row.
* The clamp is declared, not silent: set `data-fc-truncated="true"` and keep
* the full text reachable (a title attribute or the destination page).
*/
.fc-list__supporting {
display: -webkit-box;
overflow: hidden;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
color: var(--fc-color-text-muted);
font-size: var(--fc-font-size-sm);
line-height: var(--fc-line-height-normal);
}
.fc-list__trailing {
display: inline-flex;
flex-shrink: 0;
align-items: center;
gap: var(--fc-space-2);
color: var(--fc-color-text-muted);
font-size: var(--fc-font-size-sm);
}
/* ---- Interactive rows ------------------------------------------------ */
/* Buttons as actions lose their chrome so they read as row text. */
.fc-list__action {
padding: 0;
background: none;
border: 0;
font: inherit;
text-align: start;
cursor: pointer;
}
.fc-list__action::after {
content: "";
position: absolute;
inset: 0;
z-index: var(--fc-z-base);
border-radius: inherit;
}
/*
* The focus ring moves from the headline to the row, because the row is
* what activates. The headline keeps a transparent outline rather than
* none, so forced-colors mode, which repaints outline colours, still draws
* a ring there too.
*/
.fc-list__action:focus-visible {
outline-color: transparent;
}
.fc-list__action:focus-visible::after {
outline: var(--fc-border-width-focus) solid var(--fc-color-focus-ring);
outline-offset: calc(var(--fc-border-width-focus) * -1);
}
/* Trailing controls sit above the stretched action, so they stay clickable. */
.fc-list__trailing :is(a, button, input, select) {
position: relative;
z-index: var(--fc-z-raised);
}
@media (hover: hover) {
.fc-list__item:has(.fc-list__action):hover,
label.fc-list__item:hover {
background-color: color-mix(in oklab, currentColor calc(var(--fc-opacity-hover-overlay) * 100%), transparent);
}
}
.fc-list__item:has(.fc-list__action):active,
label.fc-list__item:active {
background-color: color-mix(in oklab, currentColor calc(var(--fc-opacity-pressed-overlay) * 100%), transparent);
}
.fc-list__item:has([aria-current]:not([aria-current="false"]), [aria-selected="true"]) {
background-color: var(--fc-color-selected-bg);
color: var(--fc-color-selected-fg);
}
.fc-list__item:has(:disabled, [aria-disabled="true"]) {
color: var(--fc-color-disabled-fg);
cursor: not-allowed;
}
/* An aria-disabled action stays focusable but does not activate. */
.fc-list__action[aria-disabled="true"] {
pointer-events: none;
}
/* ---- Forced colors --------------------------------------------------- */
/* The selected fill is discarded; Highlight restates it. */
@media (forced-colors: active) {
.fc-list__item:has([aria-current]:not([aria-current="false"]), [aria-selected="true"]) {
background-color: Highlight;
color: HighlightText;
forced-color-adjust: none;
}
.fc-list__item:has(:disabled, [aria-disabled="true"]) {
color: GrayText;
}
}
/* Motion: one background-colour transition, zeroed by the reset under
reduced motion. */
}
/*!
* future-css -- slider
*
* A value picked from a range by dragging: temperature, volume, a top-p.
* Built on `<input type="range">`, so keyboard (arrows, Page Up/Down, Home,
* End), the slider role, `aria-valuenow`, and form submission all come from
* the platform:
*
* <div class="fc-slider">
* <input class="fc-slider__input" type="range" id="temp"
* min="0" max="2" step="0.1" value="0.7" aria-labelledby="temp-label">
* <output class="fc-slider__value" for="temp">0.7</output>
* <ol class="fc-slider__marks" aria-hidden="true"><li>0</li><li>1</li><li>2</li></ol>
* </div>
*
* The filled part of the track is drawn from `--fc-slider-pct`, which the
* `slider` binding sets on input (Firefox draws it natively). Without the
* binding the track is unfilled and the thumb alone shows the value, which is
* complete information, just less of it.
*
* Marks are a visible scale, hidden from assistive technology because the
* input already announces its value; `<datalist>` would be the semantic form,
* but no engine renders one under a styled range.
*/
@layer fc.components {
.fc-slider {
--fc-slider-thumb: 1.25rem;
--fc-slider-track: 0.375rem;
/* Both declared as 3:1 boundaries against a surface (WCAG 1.4.11): the
checked-control colour and the off-switch track. */
--fc-slider-fill: var(--fc-color-accent-border);
--fc-slider-rest: var(--fc-color-border-strong);
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
column-gap: var(--fc-space-3);
max-inline-size: 100%;
}
.fc-slider__input {
-webkit-appearance: none;
appearance: none;
inline-size: 100%;
/* The input box is the hit target, taller than the track it draws. */
block-size: var(--fc-size-target-comfortable);
margin: 0;
padding: 0;
/* The base layer draws a field border on inputs; a range has no field. */
border: 0;
background: transparent;
cursor: pointer;
touch-action: pan-y;
}
/*
* The track. A gradient with a hard stop at the value: filled before it,
* rest after. The direction is physical because the browser fills a range
* from the left in LTR and from the right in RTL; the :dir(rtl) branch
* below follows it.
*/
.fc-slider__input::-webkit-slider-runnable-track {
block-size: var(--fc-slider-track);
border-radius: var(--fc-radius-full);
background: linear-gradient(to right, var(--fc-slider-fill) var(--fc-slider-pct, 0%), var(--fc-slider-rest) var(--fc-slider-pct, 0%));
}
.fc-slider__input:dir(rtl)::-webkit-slider-runnable-track {
background: linear-gradient(to left, var(--fc-slider-fill) var(--fc-slider-pct, 0%), var(--fc-slider-rest) var(--fc-slider-pct, 0%));
}
.fc-slider__input::-moz-range-track {
block-size: var(--fc-slider-track);
border-radius: var(--fc-radius-full);
background-color: var(--fc-slider-rest);
}
.fc-slider__input::-moz-range-progress {
block-size: var(--fc-slider-track);
border-radius: var(--fc-radius-full);
background-color: var(--fc-slider-fill);
}
/*
* The thumb carries a ring of the surface colour, so it stays distinct from
* the filled track it sits on.
*/
.fc-slider__input::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
inline-size: var(--fc-slider-thumb);
block-size: var(--fc-slider-thumb);
margin-block-start: calc((var(--fc-slider-track) - var(--fc-slider-thumb)) / 2);
background-color: var(--fc-slider-fill);
border: var(--fc-border-width-thick) solid var(--fc-color-bg-surface);
border-radius: var(--fc-radius-full);
box-shadow: var(--fc-shadow-raised);
transition: scale var(--fc-duration-fast) var(--fc-easing-standard);
}
.fc-slider__input::-moz-range-thumb {
inline-size: var(--fc-slider-thumb);
block-size: var(--fc-slider-thumb);
background-color: var(--fc-slider-fill);
border: var(--fc-border-width-thick) solid var(--fc-color-bg-surface);
border-radius: var(--fc-radius-full);
box-shadow: var(--fc-shadow-raised);
}
@media (hover: hover) {
.fc-slider__input:hover {
--fc-slider-fill: var(--fc-color-accent-bg-hover);
}
}
/* Dragging enlarges the thumb, so it stays visible under a fingertip. */
.fc-slider__input:active::-webkit-slider-thumb {
scale: 1.15;
}
.fc-slider__input:active {
--fc-slider-fill: var(--fc-color-accent-bg-active);
}
.fc-slider__input:disabled {
--fc-slider-fill: var(--fc-color-disabled-fg);
--fc-slider-rest: var(--fc-color-disabled-border);
cursor: not-allowed;
}
.fc-slider__input[aria-invalid="true"] {
--fc-slider-fill: var(--fc-color-danger-bg);
}
.fc-slider__value {
min-inline-size: 3ch;
color: var(--fc-color-text-default);
font-size: var(--fc-font-size-sm);
font-variant-numeric: tabular-nums;
text-align: end;
}
/*
* Inset by half a thumb on each side, which is where the browser puts the
* track's ends, so the first and last marks sit under the thumb's extremes.
*/
.fc-slider__marks {
display: flex;
grid-column: 1;
justify-content: space-between;
margin: 0;
padding: 0;
padding-inline: calc(var(--fc-slider-thumb) / 2);
list-style: none;
color: var(--fc-color-text-muted);
font-size: var(--fc-font-size-xs);
font-variant-numeric: tabular-nums;
}
/* Each mark is centred on its point rather than starting at it. */
.fc-slider__marks > li {
inline-size: 0;
display: flex;
justify-content: center;
white-space: nowrap;
}
/*
* Reduced motion: the thumb does not grow. The reset zeroes the duration, so
* the growth would jump; the larger thumb is not needed to see it either.
*/
@media (prefers-reduced-motion: reduce) {
.fc-slider__input:active::-webkit-slider-thumb {
scale: 1;
}
}
/* ---- Forced colors --------------------------------------------------- */
/*
* Gradients and shadows are dropped in forced colors. The track and thumb
* are restated in system colours, so the value is still visible.
*/
@media (forced-colors: active) {
.fc-slider__input::-webkit-slider-runnable-track {
background: CanvasText;
}
.fc-slider__input::-webkit-slider-thumb {
background-color: Highlight;
border-color: Canvas;
}
.fc-slider__input::-moz-range-track {
background-color: CanvasText;
}
.fc-slider__input::-moz-range-thumb,
.fc-slider__input::-moz-range-progress {
background-color: Highlight;
}
.fc-slider__input:disabled::-webkit-slider-thumb {
background-color: GrayText;
}
}
}
/*!
* future-css -- splitter
*
* Drag-to-resize, on the WAI-ARIA window splitter pattern. The control is one
* focusable `role="separator"` whose `aria-valuenow` is the size of the
* element it names in `aria-controls`. It appears in two layouts.
*
* A split: two panes and a splitter between them. The value is a percentage
* of the split, so the panes keep their proportion when the split resizes.
*
* <div class="fc-split">
* <div class="fc-split__pane fc-split__pane--primary" id="files">...</div>
* <div class="fc-splitter" role="separator" tabindex="0" aria-orientation="vertical"
* aria-controls="files" aria-label="File list" aria-valuenow="30"
* aria-valuemin="15" aria-valuemax="70"></div>
* <div class="fc-split__pane">...</div>
* </div>
*
* A resizable element: a panel whose inline-end (or, with `--block`,
* block-end) edge is the handle. The value is in CSS pixels, because the
* element has no container whose proportion it should keep.
*
* <aside class="fc-resizable" id="panel">
* ...
* <div class="fc-splitter fc-splitter--edge" role="separator" tabindex="0"
* aria-orientation="vertical" aria-controls="panel" aria-label="Panel"
* aria-valuemin="160" aria-valuemax="480"></div>
* </aside>
*
* `aria-orientation` is the orientation of the bar, not of the drag: a
* vertical bar sits between side-by-side panes and moves along the inline
* axis. It is required, because a separator defaults to horizontal. It is also
* what the styles key off, so the bar's look cannot disagree with what a
* screen reader is told.
*
* The size is read from `--fc-split-size` on the controlled element. The
* `splitter` binding keeps it equal to `aria-valuenow` and supplies the drag
* and the keyboard. Without the binding the layout renders at whatever size
* the markup sets, and the bar is a visible, inert divider.
*/
@layer fc.components {
.fc-split {
display: flex;
min-inline-size: 0;
min-block-size: 0;
}
/*
* Panes stacked in the block direction. A percentage flex-basis resolves
* only against a definite block size, so the split needs one; the slot lets
* a page give it the viewport or a parent's height instead.
*/
.fc-split--stacked {
flex-direction: column;
block-size: var(--fc-split-block-size, 20rem);
}
/*
* Panes scroll their own content. The zero minimums let a pane shrink below
* its content's width, which is what a flex item refuses to do by default.
*/
.fc-split__pane {
flex: 1 1 0;
min-inline-size: 0;
min-block-size: 0;
overflow: auto;
}
.fc-split__pane--primary {
flex: 0 0 var(--fc-split-size, 50%);
}
/*
* Content past the size is clipped rather than spilling under the handle;
* a panel that can outgrow itself puts a scroller inside.
*/
.fc-resizable {
position: relative;
flex: none;
inline-size: var(--fc-split-size, auto);
max-inline-size: 100%;
min-inline-size: 0;
overflow: clip;
}
.fc-resizable--block {
inline-size: auto;
max-inline-size: none;
block-size: var(--fc-split-size, auto);
}
/* ---- The handle ---------------------------------------------------- */
/*
* The hit area is the minimum target size, but the layout gives it one
* hairline: negative margins pull the panes back under it. The bar drawn
* in the middle is that hairline; the grip on it is the declared 3:1
* interactive boundary, so the control is identifiable as one (WCAG 1.4.11)
* while the bar itself stays as quiet as a divider.
*/
.fc-splitter {
--fc-splitter-line: var(--fc-color-border-default);
--fc-splitter-grip: var(--fc-color-border-interactive);
position: relative;
z-index: var(--fc-z-raised);
flex: none;
align-self: stretch;
display: grid;
place-items: center;
inline-size: var(--fc-size-target-min);
margin-inline: calc((var(--fc-border-width-thin) - var(--fc-size-target-min)) / 2);
cursor: col-resize;
/* The drag is the binding's; a touch must not scroll the page instead. */
touch-action: none;
user-select: none;
}
.fc-splitter::before {
content: "";
position: absolute;
inset-block: 0;
inset-inline: calc(50% - var(--fc-border-width-thin) / 2);
background-color: var(--fc-splitter-line);
transition: background-color var(--fc-duration-fast) var(--fc-easing-standard);
}
.fc-splitter::after {
content: "";
position: relative;
inline-size: var(--fc-space-1);
block-size: var(--fc-space-6);
border-radius: var(--fc-radius-full);
background-color: var(--fc-splitter-grip);
transition: background-color var(--fc-duration-fast) var(--fc-easing-standard);
}
.fc-splitter[aria-orientation="horizontal"] {
inline-size: auto;
block-size: var(--fc-size-target-min);
margin-inline: 0;
margin-block: calc((var(--fc-border-width-thin) - var(--fc-size-target-min)) / 2);
cursor: row-resize;
}
.fc-splitter[aria-orientation="horizontal"]::before {
inset-inline: 0;
inset-block: calc(50% - var(--fc-border-width-thin) / 2);
}
.fc-splitter[aria-orientation="horizontal"]::after {
inline-size: var(--fc-space-6);
block-size: var(--fc-space-1);
}
/*
* On an edge the hit area lies inside the element, so it never pokes past
* a panel that is already as wide as the page. The bar is drawn on the
* edge itself and the grip sits against it.
*/
.fc-splitter--edge {
position: absolute;
inset-block: 0;
inset-inline-end: 0;
margin: 0;
justify-items: end;
}
.fc-splitter--edge::before {
inset-inline: auto 0;
}
.fc-splitter--edge[aria-orientation="horizontal"] {
inset-inline: 0;
inset-block: auto 0;
margin: 0;
justify-items: center;
align-items: end;
}
.fc-splitter--edge[aria-orientation="horizontal"]::before {
inset-block: auto 0;
}
/* The element clips its overflow, so the ring is drawn inside the handle. */
.fc-splitter--edge:focus-visible {
outline-offset: calc(var(--fc-border-width-focus) * -1);
}
/* ---- States -------------------------------------------------------- */
@media (hover: hover) {
.fc-splitter:hover {
--fc-splitter-line: var(--fc-color-accent-border);
--fc-splitter-grip: var(--fc-color-accent-border);
}
}
.fc-splitter:focus-visible {
--fc-splitter-line: var(--fc-color-accent-border);
--fc-splitter-grip: var(--fc-color-accent-border);
}
/* Held for the whole drag: pointer capture keeps the press on the handle. */
.fc-splitter:active {
--fc-splitter-line: var(--fc-color-accent-bg-active);
--fc-splitter-grip: var(--fc-color-accent-bg-active);
}
/*
* Disabled stays focusable, as aria-disabled does, so a keyboard user can
* still find the boundary and hear that it is fixed.
*/
.fc-splitter[aria-disabled="true"] {
--fc-splitter-line: var(--fc-color-disabled-border);
--fc-splitter-grip: var(--fc-color-disabled-fg);
cursor: not-allowed;
}
/*
* Reduced motion needs no branch: the only motion is a colour fade, which
* the reset's zeroed durations turn into an instant change. The drag itself
* follows the pointer, which is direct manipulation, not animation.
*/
/* ---- Forced colors --------------------------------------------------- */
/*
* Backgrounds are replaced in forced colors, which would erase both the bar
* and the grip. Each is restated in a system colour, with Highlight for the
* engaged states so hover and drag stay distinguishable.
*/
@media (forced-colors: active) {
.fc-splitter {
--fc-splitter-line: CanvasText;
--fc-splitter-grip: CanvasText;
}
/* Only the drawn parts opt out, so the focus ring keeps its system colour. */
.fc-splitter::before,
.fc-splitter::after {
forced-color-adjust: none;
}
.fc-splitter:hover,
.fc-splitter:focus-visible,
.fc-splitter:active {
--fc-splitter-line: Highlight;
--fc-splitter-grip: Highlight;
}
.fc-splitter[aria-disabled="true"] {
--fc-splitter-line: GrayText;
--fc-splitter-grip: GrayText;
}
}
}
/*!
* future-css -- toolbar
*
* A row of controls that act on one thing: formatting for an editor, actions
* for a selection, tools for a canvas. Material's docked and floating
* toolbars, and the rich text editor's formatting bar, are this component.
*
* <div class="fc-toolbar fc-toolbar--floating" role="toolbar" aria-label="Selection">
* <button class="fc-toolbar__item" type="button" aria-pressed="false">Bold</button>
* <span class="fc-toolbar__divider" role="separator"></span>
* <button class="fc-toolbar__item fc-toolbar__item--icon" type="button" aria-label="Delete">…</button>
* </div>
*
* `role="toolbar"` with roving tabindex: one item is tabbable and the arrow
* keys move within, so a screen-reader user does not tab through every
* button on every visit. The `toolbar` binding provides that; without it
* every item is a plain tab stop, which is slower but complete.
*
* Pressed state is `aria-pressed`, not a class, so the drawn and announced
* states cannot disagree.
*/
@layer fc.components {
.fc-toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--fc-space-1);
padding: var(--fc-space-2);
border-block-end: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
background-color: var(--fc-color-bg-subtle);
/* Wraps rather than scrolling, so it stays usable on a phone. */
min-inline-size: 0;
}
.fc-toolbar__item {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--fc-space-1-5);
/* 24px by default, the WCAG 2.2 minimum, for a dense formatting bar; the
docked and floating forms raise it to the shared control size. */
min-inline-size: var(--fc-toolbar-item-size, var(--fc-size-target-min));
min-block-size: var(--fc-toolbar-item-size, var(--fc-size-target-min));
padding-inline: var(--fc-space-2);
background-color: transparent;
color: var(--fc-color-text-default);
border: var(--fc-border-width-thin) solid transparent;
border-radius: var(--fc-toolbar-item-radius, var(--fc-radius-sm));
font-family: inherit;
font-size: var(--fc-font-size-sm);
font-weight: var(--fc-font-weight-medium);
line-height: 1;
cursor: pointer;
transition:
background-color var(--fc-duration-fast) var(--fc-easing-standard),
border-color var(--fc-duration-fast) var(--fc-easing-standard);
}
@media (hover: hover) {
.fc-toolbar__item:hover {
background-color: color-mix(in oklab, currentColor calc(var(--fc-opacity-hover-overlay) * 100%), transparent);
}
}
.fc-toolbar__item:active {
background-color: color-mix(in oklab, currentColor calc(var(--fc-opacity-pressed-overlay) * 100%), transparent);
}
.fc-toolbar__item[aria-pressed="true"] {
background-color: var(--fc-color-selected-bg);
color: var(--fc-color-selected-fg);
border-color: var(--fc-color-accent-border);
}
.fc-toolbar__item:disabled,
.fc-toolbar__item[aria-disabled="true"] {
background-color: transparent;
color: var(--fc-color-disabled-fg);
cursor: not-allowed;
}
/* aria-disabled items keep their tab stop in the roving order, per the APG
toolbar pattern, but do not act. */
.fc-toolbar__item[aria-disabled="true"] {
pointer-events: none;
}
/* A visible text label inside an item, set in the UI face even when the
item's glyph is not. */
.fc-toolbar__label {
font-family: var(--fc-font-family-sans);
}
/* Icon-only items stay square so the hit area matches the glyph. */
.fc-toolbar__item--icon {
padding-inline: var(--fc-space-1-5);
}
/* A visual separator between groups. Its geometry comes from properties the
toolbar sets, so a vertical toolbar turns it without reaching into it. */
.fc-toolbar__divider {
inline-size: var(--fc-toolbar-divider-inline, var(--fc-border-width-thin));
block-size: var(--fc-toolbar-divider-block, auto);
align-self: stretch;
margin-inline: var(--fc-toolbar-divider-margin-inline, var(--fc-space-1));
margin-block: var(--fc-toolbar-divider-margin-block, 0);
background-color: var(--fc-color-border-default);
}
/* ---- Docked ---------------------------------------------------------- */
/*
* Docked to the block-end of its scroll container, full width, for the
* actions of a whole screen. Sticky rather than fixed, so it belongs to its
* region and never covers another one's content.
*/
.fc-toolbar--docked {
--fc-toolbar-item-size: var(--fc-size-target-comfortable);
--fc-toolbar-item-radius: var(--fc-radius-lg);
position: sticky;
inset-block-end: 0;
z-index: var(--fc-z-sticky);
justify-content: space-around;
padding-block-end: max(var(--fc-space-2), env(safe-area-inset-bottom));
background-color: var(--fc-color-bg-surface);
border-block-start: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
border-block-end: 0;
}
/* ---- Floating -------------------------------------------------------- */
/* A pill that floats over content: actions for a selection or a canvas. */
.fc-toolbar--floating {
--fc-toolbar-item-size: var(--fc-size-target-comfortable);
--fc-toolbar-item-radius: var(--fc-radius-full);
display: inline-flex;
/* Its own width even in a stretching parent: a floating bar that spans
the column reads as a docked one. */
inline-size: fit-content;
max-inline-size: 100%;
padding: var(--fc-space-1);
background-color: var(--fc-color-bg-raised);
border: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
border-radius: var(--fc-toolbar-radius, var(--fc-radius-full));
box-shadow: var(--fc-shadow-floating);
}
/*
* Vertical is the orientation the role announces, so the layout follows
* `aria-orientation` and the two cannot disagree. The binding reads the
* same attribute to pick Up/Down over Left/Right.
*/
.fc-toolbar[aria-orientation="vertical"] {
--fc-toolbar-divider-inline: auto;
--fc-toolbar-divider-block: var(--fc-border-width-thin);
--fc-toolbar-divider-margin-inline: 0;
--fc-toolbar-divider-margin-block: var(--fc-space-1);
/* A tall pill has semicircular ends wider than its items; a rounded
rectangle keeps the shape without wasting the ends. */
--fc-toolbar-radius: var(--fc-radius-2xl);
flex-direction: column;
flex-wrap: nowrap;
align-items: stretch;
}
/* ---- Forced colors --------------------------------------------------- */
/*
* The group background and the pressed tint are both discarded, so the
* pressed state needs Highlight to stay distinguishable -- otherwise a
* screen-reader user hears "pressed" while a sighted user in high contrast
* sees nothing at all.
*/
@media (forced-colors: active) {
.fc-toolbar {
background-color: Canvas;
border-color: CanvasText;
}
.fc-toolbar__item[aria-pressed="true"] {
background-color: Highlight;
color: HighlightText;
border-color: Highlight;
}
.fc-toolbar__item:disabled,
.fc-toolbar__item[aria-disabled="true"] {
color: GrayText;
}
.fc-toolbar__divider {
background-color: CanvasText;
}
}
/* Motion: colour transitions only, zeroed by the reset under reduced motion. */
}
/*!
* future-css -- navigation bar
*
* Three to five top-level destinations along the block-end edge of a phone
* layout. Material's navigation bar; `fc-nav` is the drawer and rail for wider
* screens, and an app typically swaps one for the other at a breakpoint.
*
* <nav class="fc-navbar fc-navbar--fixed" aria-label="Primary">
* <a class="fc-navbar__item" href="/chats" aria-current="page">
* <span class="fc-navbar__icon" aria-hidden="true">…</span>
* <span class="fc-navbar__label">Chats</span>
* </a>
* <a class="fc-navbar__item" href="/inbox">
* <span class="fc-navbar__icon" aria-hidden="true">…
* <span class="fc-navbar__badge">3</span></span>
* <span class="fc-navbar__label">Inbox</span>
* <span class="fc-u-visually-hidden">, 3 unread</span>
* </a>
* </nav>
*
* Labels are always shown. Material allows icon-only bars; an icon alone is
* a guess for anyone who has not learned it, and the label costs nothing here.
* The current destination is `aria-current="page"` -- the attribute a screen
* reader announces -- and it is drawn as an indicator behind the icon plus a
* heavier label, so it does not rest on colour.
*
* A badge is decorative to assistive technology; say the count in the link's
* name, as the visually hidden text above does.
*/
@layer fc.components {
.fc-navbar {
display: flex;
align-items: stretch;
justify-content: space-around;
gap: var(--fc-space-1);
padding-block: var(--fc-space-2);
padding-inline: var(--fc-space-2);
background-color: var(--fc-color-bg-surface);
border-block-start: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
}
/*
* Fixed to the viewport's block-end edge, above the phone's home indicator.
* Pages using it reserve the space themselves (padding on the scroll
* container), since only they know what else is pinned.
*/
.fc-navbar--fixed {
position: fixed;
inset-inline: 0;
inset-block-end: 0;
z-index: var(--fc-z-sticky);
padding-block-end: max(var(--fc-space-2), env(safe-area-inset-bottom));
}
.fc-navbar__item {
display: flex;
flex: 1 1 0;
flex-direction: column;
align-items: center;
gap: var(--fc-space-1);
min-inline-size: 0;
min-block-size: var(--fc-size-target-comfortable);
padding-block: var(--fc-space-1);
color: var(--fc-color-text-muted);
border-radius: var(--fc-radius-lg);
font-size: var(--fc-font-size-xs);
font-weight: var(--fc-font-weight-medium);
line-height: var(--fc-line-height-snug);
text-align: center;
text-decoration: none;
-webkit-tap-highlight-color: transparent;
}
/*
* The indicator: a pill behind the icon. It carries the hover and pressed
* tints too, so the feedback lands where the thumb is aimed.
*/
.fc-navbar__icon {
position: relative;
display: inline-grid;
place-items: center;
inline-size: 4rem;
max-inline-size: 100%;
block-size: 2rem;
font-size: 1.25rem;
line-height: 1;
background-color: var(--fc-navbar-indicator, transparent);
border-radius: var(--fc-radius-full);
transition: background-color var(--fc-duration-fast) var(--fc-easing-standard);
}
.fc-navbar__label {
max-inline-size: 100%;
overflow-wrap: anywhere;
}
/* A count or a dot at the icon's inline-end top corner. */
.fc-navbar__badge {
position: absolute;
inset-block-start: 0;
inset-inline-start: 55%;
min-inline-size: 1rem;
block-size: 1rem;
padding-inline: var(--fc-space-1);
background-color: var(--fc-color-danger-bg);
color: var(--fc-color-text-on-danger);
border-radius: var(--fc-radius-full);
font-size: var(--fc-font-size-xs);
font-weight: var(--fc-font-weight-semibold);
line-height: 1rem;
text-align: center;
}
/* An empty badge is a dot: "something new" without a number. */
.fc-navbar__badge:empty {
min-inline-size: 0.5rem;
block-size: 0.5rem;
padding: 0;
inset-block-start: var(--fc-space-1);
}
@media (hover: hover) {
.fc-navbar__item:hover {
--fc-navbar-indicator: color-mix(in oklab, currentColor calc(var(--fc-opacity-hover-overlay) * 100%), transparent);
color: var(--fc-color-text-default);
}
}
.fc-navbar__item:active {
--fc-navbar-indicator: color-mix(in oklab, currentColor calc(var(--fc-opacity-pressed-overlay) * 100%), transparent);
}
.fc-navbar__item[aria-current="page"] {
--fc-navbar-indicator: var(--fc-color-selected-bg);
color: var(--fc-color-text-default);
font-weight: var(--fc-font-weight-semibold);
}
/* Without an href an anchor is not a link; this is the disabled form, with
`aria-disabled` for the announcement. */
.fc-navbar__item[aria-disabled="true"] {
color: var(--fc-color-disabled-fg);
cursor: not-allowed;
}
/* ---- Forced colors --------------------------------------------------- */
@media (forced-colors: active) {
.fc-navbar {
border-block-start-color: CanvasText;
}
.fc-navbar__item[aria-current="page"] {
--fc-navbar-indicator: Highlight;
--fc-navbar-icon-fg: HighlightText;
color: LinkText;
}
/* The indicator is drawn by the component, so the icon keeps the colours
it is given instead of having them forced back to the text colour. */
.fc-navbar__icon {
color: var(--fc-navbar-icon-fg, inherit);
forced-color-adjust: none;
}
.fc-navbar__badge {
border: var(--fc-border-width-thin) solid CanvasText;
}
.fc-navbar__item[aria-disabled="true"] {
color: GrayText;
}
}
/* Motion: the indicator's colour transition, zeroed by the reset under
reduced motion. */
}
/*!
* future-css -- search
*
* Material's search bar and search view: a prominent field for searching an
* app's content, and a panel beneath it for suggestions, recent searches, or
* results.
*
* <form class="fc-search" role="search" action="/search">
* <div class="fc-search__bar">
* <span class="fc-search__icon" aria-hidden="true">…</span>
* <input class="fc-search__input" type="search" name="q"
* aria-label="Search conversations" placeholder="Search conversations">
* <span class="fc-search__trailing">…</span>
* </div>
* <div class="fc-search__view" aria-label="Recent searches" role="region">
* <ul class="fc-list">…</ul>
* </div>
* </form>
*
* `role="search"` (or the `<search>` element) makes it a landmark. The view
* holds ordinary links and buttons, so it needs no listbox contract; when the
* suggestions should be picked with arrow keys instead, use `fc-select`, whose
* combobox does exactly that.
*
* The view shows while focus is inside the search, with no script, and can
* be held open or closed with `data-fc-open`. Its loading state is
* `aria-busy="true"` on the view.
*/
@layer fc.components {
.fc-search {
position: relative;
display: block;
max-inline-size: 100%;
}
.fc-search__bar {
display: flex;
align-items: center;
gap: var(--fc-space-2);
/* The shared control height, so a search bar lines up with buttons. */
min-block-size: var(--fc-size-target-comfortable);
padding-inline: var(--fc-space-3) var(--fc-space-1);
background-color: var(--fc-color-bg-surface);
color: var(--fc-color-text-default);
border: var(--fc-border-width-thin) solid var(--fc-color-border-interactive);
border-radius: var(--fc-radius-full);
transition:
border-color var(--fc-duration-fast) var(--fc-easing-standard),
box-shadow var(--fc-duration-fast) var(--fc-easing-standard);
}
@media (hover: hover) {
.fc-search__bar:hover {
border-color: color-mix(in oklab, var(--fc-color-border-interactive), var(--fc-color-text-default) 40%);
}
}
/*
* The ring is drawn on the bar, the shape a user sees as the field. The
* input inside keeps a transparent outline rather than none, so forced
* colors, which repaints outline colours, still draws a ring on it.
*/
.fc-search__bar:has(:focus-visible) {
border-color: var(--fc-color-accent-border);
outline: var(--fc-border-width-focus) solid var(--fc-color-focus-ring);
outline-offset: var(--fc-space-0-5);
}
.fc-search__input:focus-visible {
outline-color: transparent;
}
.fc-search__icon {
display: inline-grid;
flex-shrink: 0;
place-items: center;
color: var(--fc-color-text-muted);
}
.fc-search__input {
flex: 1 1 auto;
min-inline-size: 0;
min-block-size: var(--fc-size-target-min);
margin: 0;
padding: 0;
background: transparent;
color: inherit;
border: 0;
font: inherit;
font-size: var(--fc-font-size-md);
}
.fc-search__input::placeholder {
color: var(--fc-color-text-subtle);
}
/* The browser's own clear button stays by default: it is the only one a
search gets without script. `--own-clear` hides it when the trailing slot
supplies a named "Clear search" button, so there are never two. */
.fc-search__input--own-clear::-webkit-search-cancel-button {
-webkit-appearance: none;
appearance: none;
}
.fc-search__trailing {
display: inline-flex;
flex-shrink: 0;
align-items: center;
gap: var(--fc-space-1);
}
.fc-search__bar:has(:disabled) {
background-color: var(--fc-color-disabled-bg);
color: var(--fc-color-disabled-fg);
border-color: var(--fc-color-disabled-border);
cursor: not-allowed;
}
.fc-search__bar:has([aria-invalid="true"]) {
border-color: var(--fc-color-danger-border);
}
/* ---- View ------------------------------------------------------------ */
/*
* The view overlays the content below rather than pushing it down, so the
* page does not jump when focus enters the field.
*/
.fc-search__view {
position: absolute;
inset-inline: 0;
inset-block-start: calc(100% + var(--fc-space-1));
z-index: var(--fc-z-dropdown);
display: var(--fc-search-view, none);
max-block-size: min(60vh, 28rem);
overflow-y: auto;
overscroll-behavior-block: contain;
/* Inset on every side, as fc-menu and fc-select inset their items. Flush
to the sides, a row's highlight met the view's edge with the row's
smaller radius, so its corners read as a second shape that no theme's
radius scale could reconcile. */
padding: var(--fc-space-1);
background-color: var(--fc-color-bg-raised);
color: var(--fc-color-text-default);
border: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
border-radius: var(--fc-radius-xl);
box-shadow: var(--fc-shadow-overlay);
}
/* Focus inside the search opens the view, through a property it reads. */
.fc-search:focus-within {
--fc-search-view: block;
}
.fc-search__view[data-fc-open="true"] {
display: block;
}
/* Held closed by the app, focus or not (a query with nothing to suggest). */
.fc-search__view[data-fc-open="false"] {
display: none;
}
/* Results are being fetched: the stale list stays readable but recedes. */
.fc-search__view[aria-busy="true"] {
border-style: dashed;
}
.fc-search__view-label {
margin: 0;
padding-block: var(--fc-space-2) var(--fc-space-1);
padding-inline: var(--fc-space-4);
color: var(--fc-color-text-muted);
font-size: var(--fc-font-size-xs);
font-weight: var(--fc-font-weight-medium);
}
/* Docked: the view is part of the page, for a full-screen search on a phone. */
.fc-search__view--docked {
position: static;
margin-block-start: var(--fc-space-2);
box-shadow: none;
}
/* ---- Forced colors --------------------------------------------------- */
@media (forced-colors: active) {
.fc-search__bar,
.fc-search__view {
border-color: CanvasText;
}
.fc-search__bar:has(:disabled) {
color: GrayText;
border-color: GrayText;
}
}
/* Motion: border and shadow transitions only, zeroed by the reset under
reduced motion. */
}
/*!
* future-css -- carousel
*
* A row of items that scrolls sideways: featured content, image galleries,
* suggested prompts with artwork. Built on a scroll container with scroll
* snap, so touch, trackpad, Shift+wheel, and the arrow keys on the focused
* track all scroll it with no script:
*
* <section class="fc-carousel" aria-roledescription="carousel" aria-label="Featured">
* <div class="fc-carousel__controls" hidden>
* <button class="fc-btn fc-btn--secondary fc-btn--icon" type="button"
* data-fc-carousel="prev" aria-label="Previous">‹</button>
* <button class="fc-btn fc-btn--secondary fc-btn--icon" type="button"
* data-fc-carousel="next" aria-label="Next">›</button>
* </div>
* <ul class="fc-carousel__track" tabindex="0" aria-label="Featured items">
* <li class="fc-carousel__item" aria-roledescription="slide" aria-label="1 of 4">…</li>
* </ul>
* </section>
*
* The track is focusable so a keyboard user can scroll it. Previous and next
* buttons are optional and need the `carousel` binding, so the controls ship
* with `hidden` and the binding removes it: a button that does nothing is
* worse than none.
*
* Never autoplays. Content that moves on its own needs a pause control
* (WCAG 2.2.2), and a carousel that advances while someone reads it is the
* most reliably disliked pattern on the web.
*/
@layer fc.components {
.fc-carousel {
/* Width of one item as a share of the track: a multi-browse layout shows
the next item peeking in, which is what says "this scrolls". */
--fc-carousel-item: clamp(12rem, 42%, 22rem);
display: grid;
/* One column that takes the container's width, never the track's: a
max-content column would size the carousel to every item side by side
and nothing would scroll. */
grid-template-columns: minmax(0, 1fr);
inline-size: 100%;
gap: var(--fc-space-2);
min-inline-size: 0;
}
.fc-carousel__track {
display: flex;
gap: var(--fc-space-2);
margin: 0;
padding: 0;
padding-block-end: var(--fc-space-2);
list-style: none;
overflow-x: auto;
overscroll-behavior-inline: contain;
scroll-snap-type: var(--fc-carousel-snap, inline mandatory);
scrollbar-width: thin;
border-radius: var(--fc-radius-xl);
}
@media (prefers-reduced-motion: no-preference) {
.fc-carousel__track {
scroll-behavior: smooth;
}
}
.fc-carousel__item {
position: relative;
flex: 0 0 var(--fc-carousel-item);
min-inline-size: 0;
overflow: hidden;
scroll-snap-align: start;
background-color: var(--fc-color-bg-muted);
color: var(--fc-color-text-default);
border-radius: var(--fc-radius-xl);
}
/* Media fills the item; the item's radius clips it. */
.fc-carousel__item > :is(img, video, picture) {
display: block;
inline-size: 100%;
block-size: 100%;
object-fit: cover;
}
/*
* A caption over the bottom of the media, on a scrim so the text is legible
* over any image. The scrim is the inverse surface at full strength under
* the text, so the declared inverse pair holds whatever the image is.
*/
.fc-carousel__caption {
position: absolute;
inset-inline: 0;
inset-block-end: 0;
margin: 0;
padding: var(--fc-space-3);
background-color: var(--fc-color-bg-inverse);
color: var(--fc-color-text-inverse);
font-size: var(--fc-font-size-sm);
font-weight: var(--fc-font-weight-medium);
}
/* Hero: one large item at a time, with the next just visible. */
.fc-carousel--hero {
--fc-carousel-item: 85%;
}
/* Free: items do not snap, for browsing a long row. */
.fc-carousel--free {
--fc-carousel-snap: none;
}
@media (hover: hover) {
.fc-carousel__item:has(a, button):hover {
background-color: color-mix(in oklab, var(--fc-color-text-default) calc(var(--fc-opacity-hover-overlay) * 100%), var(--fc-color-bg-muted));
}
}
/* An item that holds focus draws the ring on the whole item, not just the
small link inside it. */
.fc-carousel__item:has(:focus-visible) {
outline: var(--fc-border-width-focus) solid var(--fc-color-focus-ring);
outline-offset: calc(var(--fc-border-width-focus) * -1);
}
.fc-carousel__controls {
display: flex;
justify-content: flex-end;
gap: var(--fc-space-2);
}
/* ---- Forced colors --------------------------------------------------- */
@media (forced-colors: active) {
.fc-carousel__item {
border: var(--fc-border-width-thin) solid CanvasText;
}
.fc-carousel__caption {
background-color: Canvas;
color: CanvasText;
}
}
}
/*!
* future-css -- date picker and time picker
*
* DATE PICKER. A month calendar for picking one date, with the WAI-ARIA date
* grid keyboard contract supplied by the `datepicker` binding, which also
* renders the month:
*
* <div class="fc-datepicker" id="due">
* <div class="fc-datepicker__header">
* <button class="fc-btn fc-btn--ghost fc-btn--icon" type="button" data-fc-datepicker="prev" aria-label="Previous month">‹</button>
* <h2 class="fc-datepicker__title" aria-live="polite">October 2026</h2>
* <button class="fc-btn fc-btn--ghost fc-btn--icon" type="button" data-fc-datepicker="next" aria-label="Next month">›</button>
* </div>
* <table class="fc-datepicker__grid" role="grid" aria-labelledby="…">…</table>
* </div>
*
* The no-script path, and the fastest one for anyone who knows the date, is
* a native `<input type="date" class="fc-input">`: the platform's own picker,
* keyboard entry, locale formatting and validation. The calendar is for
* browsing dates, not a replacement for typing one.
*
* Selection is `aria-selected="true"` on the grid cell, today is
* `aria-current="date"` on its button, and unavailable dates are
* `aria-disabled="true"`, which keeps them reachable by arrow keys so the
* grid does not skip silently over a gap.
*
* TIME PICKER. The native `<input type="time" class="fc-input">`, plus an
* input-mode layout (`fc-timepicker`) of hour and minute fields with an
* `fc-segmented` period, for locales and forms where AM/PM must be stated
* explicitly. Material's clock dial is not offered; see docs/PLAN.md. The
* native inputs' picker button is styled in field.css.
*/
@layer fc.components {
.fc-datepicker {
display: inline-flex;
flex-direction: column;
gap: var(--fc-space-2);
/* Seven 40px days plus padding, shrinking with a narrow container. */
inline-size: 20rem;
max-inline-size: 100%;
padding: var(--fc-space-3);
background-color: var(--fc-color-bg-raised);
color: var(--fc-color-text-default);
border: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
border-radius: var(--fc-radius-xl);
}
.fc-datepicker__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--fc-space-2);
}
.fc-datepicker__title {
margin: 0;
font-size: var(--fc-font-size-sm);
font-weight: var(--fc-font-weight-semibold);
text-align: center;
}
/*
* Month arrows. Two logical borders turned 45 degrees make a corner that
* points toward inline-start or inline-end, so "previous" points backwards
* in either direction without a glyph swap.
*/
.fc-datepicker__chevron {
inline-size: 0.45em;
block-size: 0.45em;
rotate: 45deg;
}
.fc-datepicker__chevron--prev {
border-block-end: var(--fc-border-width-thick) solid currentColor;
border-inline-start: var(--fc-border-width-thick) solid currentColor;
translate: 0.1em 0;
}
.fc-datepicker__chevron--next {
border-block-start: var(--fc-border-width-thick) solid currentColor;
border-inline-end: var(--fc-border-width-thick) solid currentColor;
translate: -0.1em 0;
}
/* The rotation is physical, so the mirrored corner needs the opposite turn. */
.fc-datepicker__chevron:dir(rtl) {
rotate: -45deg;
}
.fc-datepicker__grid {
inline-size: 100%;
border-collapse: collapse;
table-layout: fixed;
}
/* The base layer rules table rows; a calendar is a grid of buttons, and
row rules would read as a week separator that means nothing. */
.fc-datepicker__weekday,
.fc-datepicker__cell {
border: 0;
}
.fc-datepicker__weekday {
padding: 0;
padding-block-end: var(--fc-space-1);
color: var(--fc-color-text-muted);
font-size: var(--fc-font-size-xs);
font-weight: var(--fc-font-weight-medium);
text-align: center;
}
.fc-datepicker__cell {
padding: var(--fc-space-0-5);
text-align: center;
}
/*
* The day: a 40px circle, the shared control size and well past the target
* minimum. Its colours come from properties the cell sets, so selection on
* the cell restyles the button without reaching into it.
*/
.fc-datepicker__day {
display: inline-grid;
place-items: center;
/* Shrinks with its column on a narrow screen, never past the 24px
target minimum the table's own minimum width implies. */
inline-size: 100%;
max-inline-size: var(--fc-size-target-comfortable);
aspect-ratio: 1;
padding: 0;
background-color: var(--fc-datepicker-day-bg, transparent);
color: var(--fc-datepicker-day-fg, var(--fc-color-text-default));
border: var(--fc-border-width-thin) solid var(--fc-datepicker-day-border, transparent);
border-radius: var(--fc-radius-full);
font: inherit;
font-size: var(--fc-font-size-sm);
font-variant-numeric: tabular-nums;
cursor: pointer;
transition: background-color var(--fc-duration-fast) var(--fc-easing-standard);
}
/* Today is ringed, not filled, so it never reads as selected. */
.fc-datepicker__day[aria-current="date"] {
--fc-datepicker-day-border: var(--fc-color-accent-border);
font-weight: var(--fc-font-weight-semibold);
}
@media (hover: hover) {
.fc-datepicker__day:hover {
background-color: color-mix(in oklab, currentColor calc(var(--fc-opacity-hover-overlay) * 100%), var(--fc-datepicker-day-bg, transparent));
}
}
.fc-datepicker__day:active {
background-color: color-mix(in oklab, currentColor calc(var(--fc-opacity-pressed-overlay) * 100%), var(--fc-datepicker-day-bg, transparent));
}
.fc-datepicker__cell[aria-selected="true"] {
--fc-datepicker-day-bg: var(--fc-color-accent-bg);
--fc-datepicker-day-fg: var(--fc-color-text-on-accent);
}
.fc-datepicker__day[aria-disabled="true"] {
color: var(--fc-color-disabled-fg);
text-decoration: line-through;
cursor: not-allowed;
}
/* ---- Time picker ----------------------------------------------------- */
/*
* <fieldset class="fc-timepicker">
* <legend class="fc-field__label">Start time</legend>
* <input class="fc-input fc-timepicker__unit" inputmode="numeric" aria-label="Hour" value="09">
* <span class="fc-timepicker__separator" aria-hidden="true">:</span>
* <input class="fc-input fc-timepicker__unit" inputmode="numeric" aria-label="Minute" value="30">
* <fieldset class="fc-segmented">…AM / PM…</fieldset>
* </fieldset>
*
* Hours and minutes always run hours-first, left to right, even in RTL
* scripts, which write times that way; the row is isolated as LTR.
*/
.fc-timepicker {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
gap: var(--fc-space-2);
min-inline-size: 0;
margin: 0;
padding: 0;
border: 0;
direction: ltr;
unicode-bidi: isolate;
}
.fc-timepicker > legend {
flex-basis: 100%;
margin-block-end: var(--fc-space-1);
padding: 0;
}
.fc-timepicker__unit {
inline-size: 4.5rem;
min-block-size: 3.5rem;
font-size: var(--fc-font-size-2xl);
font-variant-numeric: tabular-nums;
text-align: center;
}
.fc-timepicker__separator {
font-size: var(--fc-font-size-2xl);
font-weight: var(--fc-font-weight-semibold);
}
/* ---- Forced colors --------------------------------------------------- */
@media (forced-colors: active) {
/* The day buttons opt out of forcing so the selection can be drawn in
Highlight; their resting colours are restated in system colours. */
.fc-datepicker {
--fc-datepicker-day-fg: CanvasText;
border-color: CanvasText;
}
.fc-datepicker__cell[aria-selected="true"] {
--fc-datepicker-day-bg: Highlight;
--fc-datepicker-day-fg: HighlightText;
}
.fc-datepicker__day {
forced-color-adjust: none;
}
.fc-datepicker__day[aria-current="date"] {
--fc-datepicker-day-border: CanvasText;
}
.fc-datepicker__day[aria-disabled="true"] {
color: GrayText;
}
}
/* Motion: one colour transition, zeroed by the reset under reduced motion. */
}
/*! future-css -- metric
* Content wraps rather than eliding values needed to understand or authorize work.
* Controls compose the button/field/check components and retain their state styles.
* No motion is introduced; reduced-motion needs no additional override.
*/
@layer fc.components {
.fc-metric {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: var(--fc-space-3);
min-inline-size: 0;
padding: var(--fc-space-4);
margin: 0;
overflow-wrap: anywhere;
color: var(--fc-color-text-default);
background-color: var(--fc-color-bg-surface);
border: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
border-radius: var(--fc-radius-lg);
}
.fc-metric__title { margin: 0; font-size: var(--fc-font-size-md); font-weight: var(--fc-font-weight-semibold); }
.fc-metric__meta { margin: 0; color: var(--fc-color-text-muted); font-size: var(--fc-font-size-sm); }
.fc-metric__actions { display: flex; flex-wrap: wrap; gap: var(--fc-space-2); }
.fc-metric[data-fc-state="error"],
.fc-metric[data-fc-state="denied"] { border-inline-start: var(--fc-space-1) solid var(--fc-color-danger-border); }
.fc-metric[data-fc-state="pending"],
.fc-metric[data-fc-state="queued"],
.fc-metric[data-fc-state="running"],
.fc-metric[data-fc-state="streaming"] { border-inline-start: var(--fc-space-1) solid var(--fc-color-accent-border); }
.fc-metric[data-fc-state="complete"] { border-style: double; }
.fc-metric[data-fc-state="aborted"] { border-style: dashed; }
.fc-metric__value { margin: 0; font-size: var(--fc-font-size-2xl); font-weight: var(--fc-font-weight-bold); font-variant-numeric: tabular-nums; }
@media (forced-colors: active) {
.fc-metric { color: CanvasText; background-color: Canvas; border-color: CanvasText; }
}
}
/*! future-css -- transfer
* Content wraps rather than eliding values needed to understand or authorize work.
* Controls compose the button/field/check components and retain their state styles.
* No motion is introduced; reduced-motion needs no additional override.
*/
@layer fc.components {
.fc-transfer {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: var(--fc-space-3);
min-inline-size: 0;
padding: var(--fc-space-4);
margin: 0;
overflow-wrap: anywhere;
color: var(--fc-color-text-default);
background-color: var(--fc-color-bg-surface);
border: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
border-radius: var(--fc-radius-lg);
}
.fc-transfer__title { margin: 0; font-size: var(--fc-font-size-md); font-weight: var(--fc-font-weight-semibold); }
.fc-transfer__meta { margin: 0; color: var(--fc-color-text-muted); font-size: var(--fc-font-size-sm); }
.fc-transfer__actions { display: flex; flex-wrap: wrap; gap: var(--fc-space-2); }
.fc-transfer[data-fc-state="error"],
.fc-transfer[data-fc-state="denied"] { border-inline-start: var(--fc-space-1) solid var(--fc-color-danger-border); }
.fc-transfer[data-fc-state="pending"],
.fc-transfer[data-fc-state="queued"],
.fc-transfer[data-fc-state="running"],
.fc-transfer[data-fc-state="streaming"] { border-inline-start: var(--fc-space-1) solid var(--fc-color-accent-border); }
.fc-transfer[data-fc-state="complete"] { border-style: double; }
.fc-transfer[data-fc-state="aborted"] { border-style: dashed; }
@media (forced-colors: active) {
.fc-transfer { color: CanvasText; background-color: Canvas; border-color: CanvasText; }
}
}
/*! future-css -- approval
* Content wraps rather than eliding values needed to understand or authorize work.
* Controls compose the button/field/check components and retain their state styles.
* No motion is introduced; reduced-motion needs no additional override.
*/
@layer fc.components {
.fc-approval {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: var(--fc-space-3);
min-inline-size: 0;
padding: var(--fc-space-4);
margin: 0;
overflow-wrap: anywhere;
color: var(--fc-color-text-default);
background-color: var(--fc-color-bg-surface);
border: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
border-radius: var(--fc-radius-lg);
}
.fc-approval__title { margin: 0; font-size: var(--fc-font-size-md); font-weight: var(--fc-font-weight-semibold); }
.fc-approval__meta { margin: 0; color: var(--fc-color-text-muted); font-size: var(--fc-font-size-sm); }
.fc-approval__actions { display: flex; flex-wrap: wrap; gap: var(--fc-space-2); }
.fc-approval[data-fc-state="error"],
.fc-approval[data-fc-state="denied"] { border-inline-start: var(--fc-space-1) solid var(--fc-color-danger-border); }
.fc-approval[data-fc-state="pending"],
.fc-approval[data-fc-state="queued"],
.fc-approval[data-fc-state="running"],
.fc-approval[data-fc-state="streaming"] { border-inline-start: var(--fc-space-1) solid var(--fc-color-accent-border); }
.fc-approval[data-fc-state="complete"] { border-style: double; }
.fc-approval[data-fc-state="aborted"] { border-style: dashed; }
@media (forced-colors: active) {
.fc-approval { color: CanvasText; background-color: Canvas; border-color: CanvasText; }
}
}
/*! future-css -- user-prompt
* Content wraps rather than eliding values needed to understand or authorize work.
* Controls compose the button/field/check components and retain their state styles.
* No motion is introduced; reduced-motion needs no additional override.
*/
@layer fc.components {
.fc-user-prompt {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: var(--fc-space-3);
min-inline-size: 0;
padding: var(--fc-space-4);
margin: 0;
overflow-wrap: anywhere;
color: var(--fc-color-text-default);
background-color: var(--fc-color-bg-surface);
border: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
border-radius: var(--fc-radius-lg);
}
.fc-user-prompt__title { margin: 0; font-size: var(--fc-font-size-md); font-weight: var(--fc-font-weight-semibold); }
.fc-user-prompt__meta { margin: 0; color: var(--fc-color-text-muted); font-size: var(--fc-font-size-sm); }
.fc-user-prompt__actions { display: flex; flex-wrap: wrap; gap: var(--fc-space-2); }
.fc-user-prompt[data-fc-state="error"],
.fc-user-prompt[data-fc-state="denied"] { border-inline-start: var(--fc-space-1) solid var(--fc-color-danger-border); }
.fc-user-prompt[data-fc-state="pending"],
.fc-user-prompt[data-fc-state="queued"],
.fc-user-prompt[data-fc-state="running"],
.fc-user-prompt[data-fc-state="streaming"] { border-inline-start: var(--fc-space-1) solid var(--fc-color-accent-border); }
.fc-user-prompt[data-fc-state="complete"] { border-style: double; }
.fc-user-prompt[data-fc-state="aborted"] { border-style: dashed; }
@media (forced-colors: active) {
.fc-user-prompt { color: CanvasText; background-color: Canvas; border-color: CanvasText; }
}
}
/*! future-css -- device-auth
* Content wraps rather than eliding values needed to understand or authorize work.
* Controls compose the button/field/check components and retain their state styles.
* No motion is introduced; reduced-motion needs no additional override.
*/
@layer fc.components {
.fc-device-auth {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: var(--fc-space-3);
min-inline-size: 0;
padding: var(--fc-space-4);
margin: 0;
overflow-wrap: anywhere;
color: var(--fc-color-text-default);
background-color: var(--fc-color-bg-surface);
border: var(--fc-border-width-thin) solid var(--fc-color-border-subtle);
border-radius: var(--fc-radius-lg);
}
.fc-device-auth__title { margin: 0; font-size: var(--fc-font-size-md); font-weight: var(--fc-font-weight-semibold); }
.fc-device-auth__meta { margin: 0; color: var(--fc-color-text-muted); font-size: var(--fc-font-size-sm); }
.fc-device-auth__actions { display: flex; flex-wrap: wrap; gap: var(--fc-space-2); }
.fc-device-auth[data-fc-state="error"],
.fc-device-auth[data-fc-state="denied"] { border-inline-start: var(--fc-space-1) solid var(--fc-color-danger-border); }
.fc-device-auth[data-fc-state="pending"],
.fc-device-auth[data-fc-state="queued"],
.fc-device-auth[data-fc-state="running"],
.fc-device-auth[data-fc-state="streaming"] { border-inline-start: var(--fc-space-1) solid var(--fc-color-accent-border); }
.fc-device-auth[data-fc-state="complete"] { border-style: double; }
.fc-device-auth[data-fc-state="aborted"] { border-style: dashed; }
/* Codes remain selectable and isolated without entering serialized attributes. */
.fc-device-auth__code { direction: ltr; unicode-bidi: isolate; font-family: var(--fc-font-family-mono); font-size: var(--fc-font-size-xl); user-select: all; }
@media (forced-colors: active) {
.fc-device-auth { color: CanvasText; background-color: Canvas; border-color: CanvasText; }
}
}
/*! future-css -- optional named type roles */
@layer fc.utilities {
.fc-type-body { font: var(--fc-type-body); letter-spacing: var(--fc-type-body-tracking); }
.fc-type-body-small { font: var(--fc-type-body-small); letter-spacing: var(--fc-type-body-small-tracking); }
.fc-type-title { font: var(--fc-type-title); letter-spacing: var(--fc-type-title-tracking); }
.fc-type-heading-1 { font: var(--fc-type-heading-1); letter-spacing: var(--fc-type-heading-1-tracking); }
.fc-type-heading-2 { font: var(--fc-type-heading-2); letter-spacing: var(--fc-type-heading-2-tracking); }
.fc-type-heading-3 { font: var(--fc-type-heading-3); letter-spacing: var(--fc-type-heading-3-tracking); }
.fc-type-heading-4 { font: var(--fc-type-heading-4); letter-spacing: var(--fc-type-heading-4-tracking); }
.fc-type-heading-5 { font: var(--fc-type-heading-5); letter-spacing: var(--fc-type-heading-5-tracking); }
.fc-type-heading-6 { font: var(--fc-type-heading-6); letter-spacing: var(--fc-type-heading-6-tracking); }
.fc-type-code { font: var(--fc-type-code); letter-spacing: var(--fc-type-code-tracking); }
.fc-type-label { font: var(--fc-type-label); letter-spacing: var(--fc-type-label-tracking); }
}
/*!
* future-css -- utilities layer
*
* Utilities are the escape hatch, deliberately kept small. The governing policy
* is that a utility earns its place only when the alternative is a component
* variation or a one-off inline style, and it must be impossible to express
* with a component. Every utility here solves a problem that genuinely has no
* component-shaped answer.
*
* Utilities sit in the last cascade layer, so they beat component styles on
* equal specificity. That is the point: it makes the escape hatch reliable
* without `!important`.
*
* Naming: .fc-u-<property>-<value>. Predictable names beat clever ones -- a
* developer who knows the convention can guess the utility without looking it
* up.
*/
@layer fc.utilities {
/*
* Visually hidden but available to assistive technology.
*
* This is the canonical accessible-hiding pattern and the reason it lives
* here: it is used by nearly every component for skip links and icon labels.
*
* The clip-path/clip-rect form is used rather than the older
* `top: -9999px` trick because a negative-offset element can create a
* scrollable overflow region, causing the page to scroll when it should not.
*
* `!important` here is intentional and is the ONLY permitted use in the
* system: it must survive an author's own styles, since the entire point is
* to hide something the author cannot see.
*/
.fc-u-visually-hidden:not(:focus):not(:active) {
position: absolute;
inline-size: var(--fc-space-px);
block-size: var(--fc-space-px);
padding: 0;
margin: calc(-1 * var(--fc-space-px));
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
/* Keeps the hidden element focusable-by-script (e.g. a skip link target). */
border: 0;
}
/*
* The visible-on-focus variant, used by skip links: hidden until focused by
* the keyboard, then pinned to the top of the viewport. Logical inset so it
* anchors to the correct edge in RTL.
*/
.fc-u-visually-hidden:focus-visible {
position: fixed;
inset-block-start: var(--fc-space-2);
inset-inline-start: var(--fc-space-2);
z-index: var(--fc-z-toast);
inline-size: auto;
block-size: auto;
padding: var(--fc-space-2) var(--fc-space-4);
margin: 0;
overflow: visible;
clip-path: none;
background-color: var(--fc-color-bg-raised);
color: var(--fc-color-text-default);
border: var(--fc-border-width-thin) solid var(--fc-color-border-default);
border-radius: var(--fc-radius-md);
box-shadow: var(--fc-shadow-raised);
}
/* Isolate unknown-direction names and paths from surrounding punctuation. */
.fc-u-bidi { unicode-bidi: isolate; }
/* Spacing utilities. Logical, so they mirror correctly in RTL. */
.fc-u-p-0 { padding: var(--fc-space-0); }
.fc-u-p-2 { padding: var(--fc-space-2); }
.fc-u-p-4 { padding: var(--fc-space-4); }
.fc-u-p-6 { padding: var(--fc-space-6); }
.fc-u-px-4 {
padding-inline: var(--fc-space-4);
}
.fc-u-py-2 {
padding-block: var(--fc-space-2);
}
.fc-u-m-0 { margin: var(--fc-space-0); }
.fc-u-mt-4 {
margin-block-start: var(--fc-space-4);
}
.fc-u-mb-4 {
margin-block-end: var(--fc-space-4);
}
/* Text alignment, expressed with start/end so it follows text direction. */
.fc-u-text-start { text-align: start; }
.fc-u-text-center { text-align: center; }
.fc-u-text-end { text-align: end; }
/* Colour utilities, restricted to tokens -- never a literal colour. */
.fc-u-text-default { color: var(--fc-color-text-default); }
.fc-u-text-muted { color: var(--fc-color-text-muted); }
.fc-u-text-accent { color: var(--fc-color-accent-text); }
.fc-u-text-danger { color: var(--fc-color-danger-text); }
.fc-u-bg-canvas { background-color: var(--fc-color-bg-canvas); }
.fc-u-bg-surface { background-color: var(--fc-color-bg-surface); }
.fc-u-bg-subtle { background-color: var(--fc-color-bg-subtle); }
/*
* Tabular numerals. Distinct from the base layer's [data-numeric] attribute:
* this is the utility form for ad-hoc application. Both exist because
* tabular figures are genuinely needed for numeric columns and are wrong for
* running prose, where proportional digits read better.
*/
.fc-u-tabular {
font-variant-numeric: tabular-nums;
}
/* Full-bleed escape from a container's padding. Logical, direction-safe. */
.fc-u-full-bleed {
inline-size: 100vw;
margin-inline: calc(50% - 50vw);
padding-inline: 0;
}
/* Hide from all rendering, including the accessibility tree. */
.fc-u-hidden {
display: none !important;
}
/*
* Preserve whitespace. Needed for translated strings that rely on significant
* spaces (Arabic, Japanese) or for preformatted content in prose.
*/
.fc-u-prewrap {
white-space: pre-wrap;
}
/* Scroll behaviour. The smooth variant is opt-in so reduced motion is safe. */
.fc-u-scroll-smooth {
scroll-behavior: smooth;
}
/* Elevation. */
.fc-u-shadow-raised {
box-shadow: var(--fc-shadow-raised);
}
}
@layer fc.tokens {
:root {
/* border */
--fc-border-width-none: 0;
--fc-border-width-thin: 1px;
--fc-border-width-thick: 2px;
--fc-border-width-focus: 2px;
/* code */
--fc-code-bg: light-dark(#f3f4f7, #0e0f13);
--fc-code-bg-subtle: light-dark(#e6e8ec, #24262b);
--fc-code-bg-inset: light-dark(#ffffff, #050608);
--fc-code-border-default: light-dark(#86898f, #606369);
--fc-code-bg-added: light-dark(#e6f5e8, #142317);
--fc-code-bg-removed: light-dark(#ffebe8, #2c1917);
--fc-code-fg: light-dark(#0e0f13, #e6e8ec);
--fc-code-comment: light-dark(#696c72, #7a7d83);
--fc-code-keyword: light-dark(#7e5db1, #906fc5);
--fc-code-string: light-dark(#317a45, #448c56);
--fc-code-number: light-dark(#936500, #a57710);
--fc-code-function: light-dark(#007598, #0a8aae);
--fc-code-type: light-dark(#b14f46, #c56157);
--fc-code-operator: light-dark(#696c72, #7a7d83);
--fc-code-punctuation: light-dark(#696c71, #7b7d82);
--fc-code-property: light-dark(#376daf, #497fc3);
--fc-code-constant: light-dark(#955890, #a86aa2);
--fc-code-tag: light-dark(#b14f46, #c56157);
--fc-code-attribute: light-dark(#906606, #a27825);
--fc-code-regexp: light-dark(#955890, #a86aa2);
--fc-code-heading: light-dark(#2d6fbe, #3d7ece);
--fc-code-link: light-dark(#00769c, #0088af);
--fc-code-invalid: light-dark(#bd423a, #d2554c);
--fc-code-added-fg: light-dark(#0b5d2a, #76cf8a);
--fc-code-removed-fg: light-dark(#902824, #f98f84);
/* color */
--fc-color-surface-depth-0: var(--fc-color-bg-canvas);
--fc-color-surface-depth-1: var(--fc-color-bg-surface);
--fc-color-surface-depth-2: var(--fc-color-bg-raised);
--fc-color-surface-depth-3: var(--fc-color-bg-overlay);
--fc-color-surface-depth-4: var(--fc-color-bg-sunken);
--fc-color-bg-canvas: light-dark(#f9fafb, #050608);
--fc-color-bg-surface: light-dark(#ffffff, #0e0f13);
--fc-color-bg-raised: light-dark(#ffffff, #24262b);
--fc-color-bg-overlay: light-dark(#ffffff, #0e0f13);
--fc-color-bg-sunken: light-dark(#e6e8ec, #000000);
--fc-color-bg-subtle: light-dark(#e6e8ec, #0e0f13);
--fc-color-bg-muted: light-dark(#d2d4da, #24262b);
--fc-color-bg-thumb: light-dark(#ffffff, #f9fafb);
--fc-color-bg-inverse: light-dark(#0e0f13, #ffffff);
--fc-color-text-inverse-muted: light-dark(#b7bbc1, #3a3d44);
--fc-color-text-default: light-dark(#0e0f13, #f9fafb);
--fc-color-text-muted: light-dark(#3a3d44, #b7bbc1);
--fc-color-text-subtle: light-dark(#51555d, #9598a0);
--fc-color-text-inverse: light-dark(#ffffff, #000000);
--fc-color-text-on-accent: light-dark(#ffffff, #000000);
--fc-color-text-on-danger: light-dark(#ffffff, #000000);
--fc-color-selected-bg: light-dark(#e5f3ff, #041f3f);
--fc-color-selected-fg: light-dark(#0051b0, #a3ceff);
--fc-color-disabled-bg: light-dark(#e6e8ec, #0e0f13);
--fc-color-disabled-fg: light-dark(#3a3d44, #b7bbc1);
--fc-color-disabled-border: light-dark(#b7bbc1, #3a3d44);
--fc-color-text-on-success: light-dark(#ffffff, #000000);
--fc-color-text-on-info: light-dark(#ffffff, #000000);
--fc-color-text-on-warning: light-dark(#000000, #000000);
--fc-color-border-default: light-dark(#b7bbc1, #3a3d44);
--fc-color-border-subtle: light-dark(#d2d4da, #24262b);
--fc-color-border-strong: light-dark(#70747d, #70747d);
--fc-color-border-interactive: light-dark(#51555d, #9598a0);
--fc-color-accent-bg: light-dark(#0063d7, #4699fe);
--fc-color-accent-bg-hover: light-dark(#0051b0, #76b4ff);
--fc-color-accent-bg-active: light-dark(#004388, #a3ceff);
--fc-color-accent-bg-subtle: light-dark(#e5f3ff, #0b3565);
--fc-color-accent-text: light-dark(#0051b0, #a3ceff);
--fc-color-accent-border: light-dark(#0063d7, #4699fe);
--fc-color-success-bg: light-dark(#196632, #56d57b);
--fc-color-success-bg-hover: light-dark(#154f27, #86e79c);
--fc-color-success-bg-active: light-dark(#103a1c, #b7f3c2);
--fc-color-success-text: light-dark(#154f27, #86e79c);
--fc-color-success-bg-subtle: light-dark(#ddfbe2, #03210c);
--fc-color-success-border: light-dark(#218041, #37bb62);
--fc-color-warning-bg: light-dark(#c18500, #e29c00);
--fc-color-warning-bg-hover: light-dark(#e29c00, #f9ae00);
--fc-color-warning-bg-active: light-dark(#f9ae00, #ffc43d);
--fc-color-warning-text: light-dark(#483000, #ffdd8e);
--fc-color-warning-bg-subtle: light-dark(#fff2c8, #291a00);
--fc-color-warning-border: light-dark(#9c6a00, #c18500);
--fc-color-danger-bg: light-dark(#be1219, #ff534b);
--fc-color-danger-bg-hover: light-dark(#970911, #ff7f72);
--fc-color-danger-bg-active: light-dark(#76080c, #ffa89c);
--fc-color-danger-text: light-dark(#970911, #ffa89c);
--fc-color-danger-bg-subtle: light-dark(#ffe7e2, #320102);
--fc-color-danger-border: light-dark(#be1219, #ff534b);
--fc-color-info-bg: light-dark(#005c76, #57c0e6);
--fc-color-info-bg-hover: light-dark(#00475b, #87d7f7);
--fc-color-info-bg-active: light-dark(#063645, #b7ebff);
--fc-color-info-text: light-dark(#00475b, #87d7f7);
--fc-color-info-bg-subtle: light-dark(#daf7ff, #011e28);
--fc-color-info-border: light-dark(#007494, #26a7d0);
--fc-color-focus-ring: light-dark(#0063d7, #76b4ff);
--fc-color-focus-ring-offset: light-dark(#ffffff, #050608);
/* duration */
--fc-duration-instant: 0ms;
--fc-duration-fast: 120ms;
--fc-duration-normal: 200ms;
--fc-duration-slow: 300ms;
/* easing */
--fc-easing-linear: linear;
--fc-easing-standard: cubic-bezier(0.2, 0, 0, 1);
--fc-easing-decelerate: cubic-bezier(0, 0, 0, 1);
--fc-easing-accelerate: cubic-bezier(0.3, 0, 1, 1);
--fc-easing-spring: cubic-bezier(0.3, 1.25, 0.5, 1);
/* font */
--fc-font-size-3xs: 0.625rem;
--fc-font-size-2xs: 0.6875rem;
--fc-font-size-xs: 0.75rem;
--fc-font-size-sm: 0.875rem;
--fc-font-size-md: 1rem;
--fc-font-size-lg: 1.125rem;
--fc-font-size-xl: 1.25rem;
--fc-font-size-2xl: 1.5rem;
--fc-font-size-3xl: 1.875rem;
--fc-font-size-4xl: 2.25rem;
--fc-font-size-5xl: 3rem;
--fc-font-size-6xl: 3.75rem;
--fc-font-weight-regular: 400;
--fc-font-weight-medium: 500;
--fc-font-weight-semibold: 600;
--fc-font-weight-bold: 700;
--fc-font-family-sans: system-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif;
--fc-font-family-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
/* line */
--fc-line-height-none: 1;
--fc-line-height-tight: 1.15;
--fc-line-height-snug: 1.4;
--fc-line-height-normal: 1.6;
--fc-line-height-relaxed: 1.75;
/* opacity */
--fc-opacity-disabled: 0.55;
--fc-opacity-hover-overlay: 0.08;
--fc-opacity-pressed-overlay: 0.12;
--fc-opacity-scrim: 0.55;
/* radius */
--fc-radius-none: 0;
--fc-radius-sm: 0.375rem;
--fc-radius-md: 0.5rem;
--fc-radius-lg: 0.75rem;
--fc-radius-xl: 1rem;
--fc-radius-2xl: 1.5rem;
--fc-radius-full: 9999px;
/* shadow */
--fc-shadow-floating: 0 12px 32px -8px light-dark(rgb(16 18 27 / 0.14), rgb(0 0 0 / 0.5));
--fc-shadow-dialog: 0 32px 64px -12px light-dark(rgb(16 18 27 / 0.24), rgb(0 0 0 / 0.65));
--fc-shadow-raised: 0 1px 2px 0 light-dark(rgb(16 18 27 / 0.06), rgb(0 0 0 / 0.4)), 0 4px 12px -4px light-dark(rgb(16 18 27 / 0.08), rgb(0 0 0 / 0.3));
--fc-shadow-overlay: 0 8px 24px -8px light-dark(rgb(16 18 27 / 0.12), rgb(0 0 0 / 0.5)), 0 2px 4px -2px light-dark(rgb(16 18 27 / 0.1), rgb(0 0 0 / 0.5));
--fc-shadow-modal: 0 24px 48px -12px light-dark(rgb(16 18 27 / 0.22), rgb(0 0 0 / 0.6)), 0 2px 6px -1px light-dark(rgb(16 18 27 / 0.08), rgb(0 0 0 / 0.5));
/* size */
--fc-size-target-min: 1.5rem;
--fc-size-target-comfortable: 2.5rem;
--fc-size-content-max: 75ch;
--fc-size-measure: 65ch;
/* space */
--fc-space-0: 0;
--fc-space-1: 0.25rem;
--fc-space-2: 0.5rem;
--fc-space-3: 0.75rem;
--fc-space-4: 1rem;
--fc-space-5: 1.25rem;
--fc-space-6: 1.5rem;
--fc-space-8: 2rem;
--fc-space-10: 2.5rem;
--fc-space-12: 3rem;
--fc-space-16: 4rem;
--fc-space-20: 5rem;
--fc-space-24: 6rem;
--fc-space-32: 8rem;
--fc-space-px: 0.0625rem;
--fc-space-0-5: 0.125rem;
--fc-space-1-5: 0.375rem;
/* term */
--fc-term-bg: light-dark(#050608, #000000);
--fc-term-bg-raised: light-dark(#0e0f13, #0e0f13);
--fc-term-fg: light-dark(#f3f4f7, #e6e8ec);
--fc-term-fg-dim: light-dark(#9598a0, #9598a0);
--fc-term-cursor: light-dark(#f3f4f7, #f3f4f7);
--fc-term-ansi-black: light-dark(#b6b7ba, #b6b7ba);
--fc-term-ansi-red: light-dark(#ff7f72, #ff7f72);
--fc-term-ansi-green: light-dark(#67d283, #67d283);
--fc-term-ansi-yellow: light-dark(#e3ad4b, #e3ad4b);
--fc-term-ansi-blue: light-dark(#60b9ff, #60b9ff);
--fc-term-ansi-magenta: light-dark(#f985f0, #f985f0);
--fc-term-ansi-cyan: light-dark(#51cade, #51cade);
--fc-term-ansi-white: light-dark(#b6b7ba, #b6b7ba);
--fc-term-ansi-bright-black: light-dark(#dcdee2, #dcdee2);
--fc-term-ansi-bright-red: light-dark(#ffa393, #ffa393);
--fc-term-ansi-bright-green: light-dark(#88fca4, #88fca4);
--fc-term-ansi-bright-yellow: light-dark(#ffd368, #ffd368);
--fc-term-ansi-bright-blue: light-dark(#81e1ff, #81e1ff);
--fc-term-ansi-bright-magenta: light-dark(#ffa9ff, #ffa9ff);
--fc-term-ansi-bright-cyan: light-dark(#6ef4ff, #6ef4ff);
--fc-term-ansi-bright-white: light-dark(#dddedf, #dddedf);
/* tracking */
--fc-tracking-normal: 0;
--fc-tracking-tight: -0.01em;
--fc-tracking-wide: 0.04em;
/* type */
--fc-type-body: var(--fc-font-weight-regular) var(--fc-font-size-md)/var(--fc-line-height-normal) var(--fc-font-family-sans);
--fc-type-body-small: var(--fc-font-weight-regular) var(--fc-font-size-sm)/var(--fc-line-height-normal) var(--fc-font-family-sans);
--fc-type-title: var(--fc-font-weight-semibold) var(--fc-font-size-2xl)/var(--fc-line-height-tight) var(--fc-font-family-sans);
--fc-type-heading-1: var(--fc-font-weight-semibold) var(--fc-font-size-4xl)/var(--fc-line-height-tight) var(--fc-font-family-sans);
--fc-type-heading-2: var(--fc-font-weight-semibold) var(--fc-font-size-3xl)/var(--fc-line-height-tight) var(--fc-font-family-sans);
--fc-type-heading-3: var(--fc-font-weight-semibold) var(--fc-font-size-2xl)/var(--fc-line-height-tight) var(--fc-font-family-sans);
--fc-type-heading-4: var(--fc-font-weight-semibold) var(--fc-font-size-xl)/var(--fc-line-height-tight) var(--fc-font-family-sans);
--fc-type-heading-5: var(--fc-font-weight-semibold) var(--fc-font-size-lg)/var(--fc-line-height-tight) var(--fc-font-family-sans);
--fc-type-heading-6: var(--fc-font-weight-semibold) var(--fc-font-size-md)/var(--fc-line-height-tight) var(--fc-font-family-sans);
--fc-type-code: var(--fc-font-weight-regular) var(--fc-font-size-sm)/var(--fc-line-height-snug) var(--fc-font-family-mono);
--fc-type-label: var(--fc-font-weight-medium) var(--fc-font-size-sm)/var(--fc-line-height-snug) var(--fc-font-family-sans);
--fc-type-body-tracking: var(--fc-tracking-normal);
--fc-type-body-small-tracking: var(--fc-tracking-normal);
--fc-type-title-tracking: var(--fc-tracking-normal);
--fc-type-heading-1-tracking: var(--fc-tracking-normal);
--fc-type-heading-2-tracking: var(--fc-tracking-normal);
--fc-type-heading-3-tracking: var(--fc-tracking-normal);
--fc-type-heading-4-tracking: var(--fc-tracking-normal);
--fc-type-heading-5-tracking: var(--fc-tracking-normal);
--fc-type-heading-6-tracking: var(--fc-tracking-normal);
--fc-type-code-tracking: var(--fc-tracking-normal);
--fc-type-label-tracking: var(--fc-tracking-normal);
/* z */
--fc-z-0: 0;
--fc-z-base: 0;
--fc-z-raised: 10;
--fc-z-sticky: 100;
--fc-z-dropdown: 200;
--fc-z-overlay: 300;
--fc-z-modal: 400;
--fc-z-popover: 500;
--fc-z-toast: 600;
}
/*
* Schemes. "light dark" tells the UA to pick based on the user's OS/browser
* setting -- this is the default and requires no JavaScript. An author
* override (a theme switcher in the UI) only ever changes these two
* declarations; because every themed token is a light-dark() pair, that alone
* re-themes the whole system.
*/
:root {
color-scheme: light dark;
}
[data-fc-theme="system"] { color-scheme: light dark; }
[data-fc-theme="light"] {
color-scheme: light;
}
[data-fc-theme="dark"] {
color-scheme: dark;
}
/*
* Forced colors (Windows High Contrast). The OS overrides component colors
* here, so the tokens that matter are the focus ones: a focus ring drawn from
* a suppressed token would disappear entirely.
*/
@media (forced-colors: active) {
:root {
--fc-color-focus-ring: Highlight;
--fc-color-focus-ring-offset: Canvas;
}
}
}