Something went wrong. Try again.
A collaborative coding-agent orchestrator for atproto radl.app
Something went wrong. Try again.
CSS
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725/* The design system, ported from the approved comp (docs/mock/phase6-goals.html). * * It is deliberately global rather than component-scoped: this is a system — tokens, a type scale, * a badge, a button, a rule — shared by every view, and Svelte's per-component scoping would mean * either duplicating those rules or wrapping each in a component that exists only to carry CSS. * Components in `lib/components` own structure and behaviour; this file owns how the product looks. * DESIGN.md is regenerated from what ships here at 6.8 (phase6-ui-plan §5). * * The rules that survive from the comp's direction contract, and must keep surviving: * - ONE accent, ink-indigo, on actions and selection only. * - Sage / red / amber are spent exclusively on verdicts and on "stopped on you". Drift is * information, not an alarm, so it stays neutral. * - Mono means machine identifier. English words are never mono. * - No eyebrows. Flat at rest. */
/* ─── tokens ─────────────────────────────────────────────────────────────── */:root { color-scheme: light; --desk: oklch(0.921 0.008 258); --desk-edge: oklch(0.878 0.010 258); --rail: oklch(0.963 0.005 258); --paper: oklch(0.996 0.001 258); --sunk: oklch(0.972 0.004 258); --line: oklch(0.902 0.006 258); --line-soft: oklch(0.938 0.005 258); --ink: oklch(0.255 0.016 262); --ink-2: oklch(0.475 0.014 262); --stroke: oklch(0.640 0.012 262); --accent: oklch(0.475 0.150 264); --accent-deep: oklch(0.410 0.150 264); --accent-mark: oklch(0.545 0.165 264); --accent-wash: oklch(0.955 0.028 264); --ok: oklch(0.470 0.115 152); --ok-mark: oklch(0.560 0.135 152); --ok-wash: oklch(0.958 0.032 152); --warn: oklch(0.475 0.108 68); --warn-mark: oklch(0.660 0.145 72); --warn-wash: oklch(0.958 0.044 78); --bad: oklch(0.475 0.165 27); --bad-mark: oklch(0.575 0.190 27); --bad-wash: oklch(0.958 0.036 27); --on-accent: oklch(1 0 0); --raised: oklch(1 0 0); --sel: oklch(0.918 0.008 258); --hover: oklch(0.972 0.004 258); --teal: oklch(0.560 0.100 200); --plum: oklch(0.520 0.130 330);
/* The rail's width, and the only token here that a reader edits: the grip on the rail's edge writes it back (`rail.svelte.ts`), and `app.html` applies the remembered one before first paint. It is a variable rather than a number in `.app` because the grid needs it where both columns are laid out — the rail cannot widen itself without the pane knowing. */ --rail-w: 246px;
/* The size of text somebody types. Three, because the comp draws three: a field, a field holding a machine identifier, and the quick-find box.
They are variables rather than the numbers themselves because iOS Safari zooms the page when a focused control computes under 16px, and the floor at the foot of this sheet has to be able to raise every one of them at once. Written as literals they could only be raised by out-ranking each rule that sets one — a fight `.fedit-where .ti` wins against `input.ti` and an inline `style` wins against everything. A custom property is not in that fight: the rule stays where it is and the value under it changes. So no typed control states its own size in pixels — not in this sheet, not in a component's `style`, not in the editor theme (`lib/editor.ts`). */ --field-size: 13.5px; --field-ident-size: 12.5px; --field-find-size: 13px;
--ease-out: cubic-bezier(0.22, 1, 0.36, 1); --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, "Helvetica Neue", sans-serif; --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, "Cascadia Mono", monospace;}
:root[data-theme="dark"] { color-scheme: dark; --desk: oklch(0.128 0.012 262); --desk-edge: oklch(0.345 0.014 262); --on-accent: oklch(0.155 0.030 264); --rail: oklch(0.213 0.013 262); --paper: oklch(0.243 0.012 262); --sunk: oklch(0.215 0.012 262); --line: oklch(0.320 0.012 262); --line-soft: oklch(0.288 0.012 262); --ink: oklch(0.935 0.006 262); --ink-2: oklch(0.735 0.012 262); --stroke: oklch(0.520 0.014 262); --accent: oklch(0.680 0.150 264); --accent-deep: oklch(0.760 0.140 264); --accent-mark: oklch(0.680 0.150 264); --accent-wash: oklch(0.272 0.052 264); --ok: oklch(0.760 0.130 152); --ok-mark: oklch(0.680 0.135 152); --ok-wash: oklch(0.310 0.050 152); --warn: oklch(0.800 0.120 78); --warn-mark: oklch(0.760 0.140 75); --warn-wash: oklch(0.320 0.050 70); --bad: oklch(0.740 0.140 27); --bad-mark: oklch(0.680 0.170 27); --bad-wash: oklch(0.318 0.055 27); --raised: oklch(0.278 0.012 262); /* above --paper: raised things get lighter */ --sel: oklch(0.300 0.016 262); --hover: oklch(0.272 0.012 262); --teal: oklch(0.700 0.090 200); --plum: oklch(0.700 0.110 330);}
* { box-sizing: border-box; }html, body { height: 100%; }body { margin: 0; font: 400 14px/1.45 var(--sans); color: var(--ink); /* Only ever seen around the picker, and behind `?frame`. Everywhere else the app covers it. */ background: radial-gradient(120% 90% at 50% 0%, var(--desk) 0%, var(--desk-edge) 100%); -webkit-font-smoothing: antialiased; display: grid; place-items: center; padding: 0;}button, input, textarea, select { font: inherit; color: inherit; }a { color: var(--accent); text-underline-offset: 2px; }/* The rail's grip is the one control that says for itself whether focus is worth a ring: it takes focus when it is dragged, which `:focus-visible` may read as keyboard-driven. See `.rail-resize`. */:where(button, a, [tabindex]):focus-visible,.rail-resize[data-ring] { outline: 2px solid var(--accent-mark); outline-offset: 2px; border-radius: 5px;}
/* ─── window ─────────────────────────────────────────────────────────────── *//* The app fills the tab. The comp drew it as a window on a desk, and that is the right way to *photograph* a product and the wrong way to live in one: a browser tab is already a window, and framing it a second time spends 26px of every edge saying so a second time. The framing is kept, exactly, behind `?frame` — `app.html` sets the flag before first paint, so a screenshot run gets the comp's desk and shadow without anybody having to work inside them. */.app { width: 100%; height: 100dvh; display: grid; grid-template-columns: var(--rail-w) minmax(0, 1fr); background: var(--paper); overflow: hidden;}/* While the edge is being dragged the whole window is the drag: the cursor does not change under the pointer as it crosses into the pane, and nothing selects the text it passes over. */.app.resizing { cursor: col-resize; user-select: none; }:root[data-frame] body { padding: 26px; }:root[data-frame] .app { width: min(1220px, 100%); height: min(860px, calc(100dvh - 52px));}/* The picker keeps the framing whether or not the app has it: it is a card on a desk rather than a window filling one, and there is no rail to put beside it. */.app.solo,:root[data-frame] .app { border: 1px solid var(--desk-edge); border-radius: 13px; box-shadow: 0 1px 1px oklch(0 0 0 / 0.05), 0 18px 34px -14px oklch(0.20 0.03 262 / 0.30), 0 44px 90px -30px oklch(0.20 0.03 262 / 0.30);}:root[data-theme="dark"] .app.solo,:root[data-theme="dark"][data-frame] .app { box-shadow: 0 24px 60px -18px oklch(0 0 0 / 0.65); }
/* ─── rail ───────────────────────────────────────────────────────────────── */.rail { position: relative; background: var(--rail); border-right: 1px solid var(--line); display: flex; flex-direction: column; min-height: 0;}/* The grip. A 9px target straddling the hairline — wide enough to hit without aiming, and invisible until it is pointed at, because the edge is already drawn and a second line there would be the app advertising a control instead of having one. Under the pointer (or the focus ring) the hairline thickens into the accent, which is the whole animation. */.rail-resize { position: absolute; top: 0; right: -4px; bottom: 0; width: 9px; z-index: 20; cursor: col-resize; touch-action: none;}.rail-resize::after { content: ""; position: absolute; inset: 0 4px; background: var(--accent-mark); opacity: 0; transition: opacity 120ms ease;}/* Dragging the edge focuses the grip, so that the arrow keys are reachable afterwards without tabbing the length of the rail — but that focus is the tail of a press, not a reader asking where they are, and a ring left down the edge of a rail somebody has just dragged is noise. So the UA's ring is declined here and `data-ring` (set by `RailResizer` when focus came from the keyboard) is what draws it, in the rule above. The hairline follows the same signal. */.rail-resize:not([data-ring]):focus-visible { outline: 0; }.rail-resize:hover::after,.rail-resize[data-ring]::after,.app.resizing .rail-resize::after { opacity: 1; }.rail-top { padding: 16px 12px 10px; display: flex; align-items: center; gap: 8px; width: 100%; border: 0; background: none; text-align: left; cursor: pointer; color: inherit; border-radius: 8px;}.rail-top:hover { background: var(--hover); }.rail-top:focus-visible { box-shadow: 0 0 0 3px var(--accent-wash); }.mark { width: 22px; height: 22px; flex: none; color: var(--accent); }.rail-identity { flex: 1; min-width: 0; }.space-name, .space-sub { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.space-name { font-weight: 600; font-size: 14px; letter-spacing: -0.005em; }.space-sub { font-size: 11.5px; color: var(--ink-2); margin-top: 1px; }.rail-switch { flex: none; color: var(--stroke); stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round;}.rail-top:hover .rail-switch, .rail-top:focus-visible .rail-switch { color: var(--ink-2); }.rail-scroll { overflow-y: auto; padding: 4px 8px 12px; flex: 1; min-height: 0; }
.nav { display: flex; flex-direction: column; gap: 1px; list-style: none; margin: 0; padding: 0; }.nav-item { display: flex; align-items: center; gap: 9px; width: 100%; padding: 5px 8px; border: 0; background: none; border-radius: 6px; text-align: left; cursor: pointer; color: var(--ink); font-size: 13.5px; line-height: 1.3; text-decoration: none; transition: background 120ms ease;}.nav-item:hover { background: var(--hover); }.nav-item[aria-current="page"] { background: var(--sel); font-weight: 600; }.nav-item .glyph { width: 17px; height: 17px; flex: none; display: grid; place-items: center; }.nav-item .label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.nav-item .count { font-size: 11.5px; font-variant-numeric: tabular-nums; color: var(--ink-2); font-weight: 500;}.nav-item[aria-current="page"] .count { color: var(--ink); }
.rail-rule { height: 1px; background: var(--line); margin: 12px 8px; border: 0; }.rail-group { margin-top: 2px; }.disclose { display: flex; align-items: center; gap: 6px; width: 100%; padding: 5px 8px 5px 4px; border: 0; background: none; border-radius: 6px; cursor: pointer; text-align: left; color: var(--ink-2); font-size: 11.5px; font-weight: 600;}.disclose:hover { background: var(--hover); }.disclose svg { transition: transform 200ms var(--ease-out); }.disclose[aria-expanded="false"] svg { transform: rotate(-90deg); }.subnav { padding-left: 6px; }/* A section heading in the rail. Quieter than a project's disclosure, because there is nothing to disclose — the views under it are the whole of it. */.rail-head { padding: 5px 8px 5px 4px; color: var(--ink-2); font: 600 11.5px/1 var(--sans); letter-spacing: 0.01em;}/* Delete sits beside its view rather than inside the link, for the same reason a goal row's label chips do: a button inside an anchor is invalid markup. It is revealed on hover or focus and is always reachable by keyboard — hidden, not absent. */.view-item { display: flex; align-items: center; }.view-item .nav-item { flex: 1; min-width: 0; }.view-x { flex: none; width: 20px; height: 20px; margin-right: 4px; border: 0; border-radius: 5px; background: transparent; color: var(--ink-2); cursor: pointer; font: 600 13px/1 var(--sans); opacity: 0; transition: opacity 110ms ease;}.view-item:hover .view-x, .view-x:focus-visible { opacity: 1; }.view-x:hover { background: var(--hover); color: var(--bad); }.view-x:disabled { cursor: default; }
/* ─── pane ───────────────────────────────────────────────────────────────── */.pane { position: relative; min-width: 0; min-height: 0; /* grid item: without this the column never scrolls, it just clips */ display: flex; flex-direction: column; background: var(--paper);}.pane-bar { display: flex; align-items: center; gap: 10px; padding: 12px 20px 12px 22px; min-height: 50px; }.crumb { display: flex; align-items: center; gap: 6px; font-size: 12.5px; color: var(--ink-2); min-width: 0; }/* Each part carries its own separator, so hiding the tail on a narrow viewport takes the slash with it rather than leaving one dangling. */.crumb .part { display: inline-flex; align-items: center; gap: 6px; min-width: 0; }.crumb .part > span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.crumb a, .crumb button { border: 0; background: none; padding: 2px 4px; margin: -2px -4px; border-radius: 5px; cursor: pointer; color: inherit; text-decoration: none;}.crumb a:hover, .crumb button:hover { color: var(--ink); background: var(--hover); }.bar-spacer { flex: 1; }.find { display: flex; align-items: center; gap: 6px; background: var(--sunk); border: 1px solid transparent; border-radius: 7px; padding: 4px 9px; width: 190px; transition: border-color 140ms ease, background 140ms ease;}.find:focus-within { border-color: var(--accent-mark); background: var(--paper); }/* Qualified, because `.icon-btn` sets its own `display` further down and would otherwise out-rank a bare `.find-btn` by order. The narrow rule that brings it back is qualified for the same reason. */.icon-btn.find-btn { display: none; }.find input { border: 0; background: none; outline: 0; width: 100%; font-size: var(--field-find-size); }.find input::placeholder { color: var(--ink-2); }.chip-mock { font: 600 10.5px/1 var(--sans); letter-spacing: 0.01em; color: var(--warn); background: var(--warn-wash); padding: 4px 7px; border-radius: 999px; white-space: nowrap;}/* Freshness, not status. A live space is polled; this says when that last worked, and turns quiet amber when it did not — the view is stale, never wrong. Clicking it polls now. */.chip-sync { display: inline-flex; align-items: center; gap: 6px; border: 0; background: none; cursor: pointer; border-radius: 999px; padding: 4px 8px; font-size: 11.5px; color: var(--ink-2); white-space: nowrap; font-variant-numeric: tabular-nums;}.chip-sync:hover { background: var(--hover); color: var(--ink); }.chip-sync .dot { width: 6px; height: 6px; border-radius: 999px; background: var(--ok-mark); flex: none; }/* The dot alone, at the size of the bar's other icon buttons so a thumb has the same target. */.sync { position: relative; display: none; }.chip-sync.mini { width: 28px; height: 28px; padding: 0; justify-content: center; border-radius: 6px; }.sync-pop { position: absolute; top: calc(100% + 8px); right: 0; z-index: 30; width: 216px; padding: 12px; background: var(--paper); border: 1px solid var(--line); border-radius: 11px; box-shadow: 0 2px 6px oklch(0.25 0.03 262 / 0.10), 0 18px 40px -10px oklch(0.25 0.03 262 / 0.34); text-align: left;}.sync-pop[hidden] { display: none; }.sync-pop .sync-state { font-size: 13px; font-weight: 600; color: var(--ink); }.sync-pop .hint { margin: 3px 0 10px; font-size: 12px; line-height: 1.5; color: var(--ink-2); overflow-wrap: anywhere;}.chip-sync.busy .dot { animation: pulse 1.4s ease-in-out infinite; }.chip-sync.stale { color: var(--warn); }.chip-sync.stale .dot { background: var(--warn-mark); }@keyframes pulse { 50% { opacity: 0.25; } }
.icon-btn { border: 0; background: none; cursor: pointer; color: var(--ink-2); width: 28px; height: 28px; border-radius: 6px; display: grid; place-items: center;}.icon-btn:hover { background: var(--hover); color: var(--ink); }
.scroll { flex: 1; overflow-y: auto; padding: 0 22px 120px; min-height: 0; scroll-behavior: smooth; }.sheet { max-width: 880px; }/* The sheet stays hard against the rail at every width, framed or not. Centring the measure in a pane that has grown to the display was tried and is wrong: the pane bar's crumb cannot centre with it — the sync chip and the account are pinned to the far corner — so the page acquires two left edges, one for the breadcrumb and one for the title under it. */
/* ─── titles ─────────────────────────────────────────────────────────────── */.page-head { padding: 6px 0 16px; }.page-title { display: flex; align-items: center; gap: 11px; }.page-title > svg, .page-title > span > svg { width: 22px; height: 22px; flex: none; }.page-title > span { display: grid; place-items: center; flex: none; }.page-title h1 { margin: 0; font-size: 26px; font-weight: 600; line-height: 1.18; letter-spacing: -0.019em; }.page-meta { margin: 7px 0 0; font-size: 13px; color: var(--ink-2); display: flex; flex-wrap: wrap; gap: 4px 12px; align-items: center;}/* A control on the meta row may have something to say — the label editor's ceiling notice, a write error. The row is a flex line, so the sentence takes a line of its own instead of standing between two facts as if it were one. */.page-meta .compose-note, .page-meta .write-error { flex-basis: 100%; margin: 4px 0 0; }/* A goal's body is a record: its line breaks are the author's, so they are kept. */.notes { margin: 12px 0 0; max-width: 66ch; font-size: 14.5px; line-height: 1.58; color: var(--ink-2); white-space: pre-wrap; }/* Copy we wrote, not text a record carries — it wraps to the measure like ordinary prose. */.notes.copy { white-space: normal; }
.sec-head { display: flex; align-items: center; gap: 10px; margin: 30px 0 6px; padding-bottom: 5px; border-bottom: 1px solid var(--line-soft); font-size: 12px; font-weight: 700; color: var(--ink-2);}.sec-head .n { font-weight: 500; font-variant-numeric: tabular-nums; }.sec-head + .sec-head { margin-top: 30px; }/* Inside a drawer the rule tracks its content measure — a rule longer than the block under it is the ragged edge Things never has. */.drawer-pad .sec-head { max-width: 64ch; }
/* A foreign board's vocabulary is useful for narrowing, not a Radial workflow to decorate. These stay neutral and compact; selection is the ordinary accent, and every state remains written out. */.status-filter { position: relative; width: max-content; margin: 9px 0 12px; }.status-filter-trigger { display: inline-flex; align-items: center; gap: 7px; min-height: 30px; border: 1px solid var(--line); border-radius: 7px; padding: 5px 9px; background: var(--paper); color: var(--ink); cursor: pointer; font: 600 12px/1.2 var(--sans);}.status-filter-trigger:hover { background: var(--hover); border-color: var(--stroke); }.status-filter-trigger.active { border-color: color-mix(in oklch, var(--accent) 35%, var(--line)); }.status-filter-value { color: var(--ink-2); font-weight: 500; }.status-filter-count { min-width: 18px; padding: 2px 5px; border-radius: 999px; background: var(--accent-wash); color: var(--accent); font-size: 10.5px; text-align: center;}.status-filter-chevron { width: 6px; height: 6px; margin: -3px 2px 0 1px; border-right: 1.5px solid var(--stroke); border-bottom: 1.5px solid var(--stroke); transform: rotate(45deg);}.status-filter-menu { position: absolute; z-index: 30; top: calc(100% + 5px); left: 0; width: 250px; padding: 7px; border: 1px solid var(--line); border-radius: 10px; background: var(--raised); box-shadow: 0 12px 30px color-mix(in oklch, var(--ink) 14%, transparent);}.status-filter-menu[hidden] { display: none; }.status-filter-menu input { width: 100%; padding: 7px 8px; }.status-filter-options { max-height: 260px; overflow-y: auto; margin: 6px -2px 0; }.status-filter-options button { display: grid; grid-template-columns: 18px 1fr auto; align-items: center; gap: 7px; width: 100%; padding: 6px; border: 0; border-radius: 6px; background: none; color: var(--ink); cursor: pointer; text-align: left; font: 500 12px/1.25 var(--sans);}.status-filter-options button:hover { background: var(--hover); }.status-filter-options button.active { color: var(--accent); font-weight: 600; }.status-filter-check { display: grid; place-items: center; width: 16px; height: 16px; border: 1px solid var(--line); border-radius: 4px; color: var(--on-accent); font-size: 11px;}.status-filter-options button.active .status-filter-check { border-color: var(--accent); background: var(--accent); }.status-filter-options button.active .status-filter-check::after { content: ''; width: 7px; height: 3px; margin-top: -2px; border-left: 1.5px solid var(--on-accent); border-bottom: 1.5px solid var(--on-accent); transform: rotate(-45deg);}.status-filter-option-count { color: var(--ink-2); font-variant-numeric: tabular-nums; font-weight: 400; }.status-filter-empty { margin: 12px 7px; color: var(--ink-2); font-size: 12px; }.status-filter-foot { display: flex; justify-content: space-between; align-items: center; margin: 6px -7px -7px; padding: 7px 9px; border-top: 1px solid var(--line-soft); color: var(--ink-2); font-size: 11.5px;}.status-filter-foot button { border: 0; padding: 2px; background: none; color: var(--accent); cursor: pointer; font: 600 11.5px/1.2 var(--sans); }.empty.compact { margin-top: 6px; }
/* ─── rows (goal list) ───────────────────────────────────────────────────── */.list { list-style: none; margin: 0; padding: 0; }.goal-row { display: flex; align-items: center; gap: 10px; width: 100%; padding: 8px 10px; margin: 0 -10px; border: 0; background: none; border-radius: 8px; cursor: pointer; text-align: left; color: var(--ink); font-size: 14px; text-decoration: none; transition: background 110ms ease;}.goal-row:hover { background: var(--hover); }/* The tail is written before the name so that below 560px it can float into the name's text (the mobile block); `order` puts it back on the end of the flex line here. */.goal-row .gtitle { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.goal-row.done .gtitle { color: var(--ink-2); }/* The tail is written before the name so that below 560px it can float into the name's text (the mobile block); `order` puts it back on the end of the flex line here. */.goal-row .tail { order: 1; display: flex; align-items: center; gap: 8px; flex: none; }.pie { color: var(--accent); flex: none; }.goal-row.done .pie { color: var(--ok-mark); }.when { font-size: 11.5px; color: var(--ink-2); font-variant-numeric: tabular-nums; white-space: nowrap; }
/* ─── state circle ───────────────────────────────────────────────────────── */.dot { width: 18px; height: 18px; flex: none; display: block; }.dot circle.ring { fill: none; stroke-width: 1.6; }.st-open .ring { stroke: var(--stroke); }.st-assigned .ring { stroke: var(--accent-mark); }.st-assigned .core { fill: var(--accent-wash); }.st-claimed .ring { stroke: var(--accent-mark); stroke-dasharray: 3 3.2; transform-origin: 9px 9px; animation: spin 9s linear infinite; }.st-claimed .core { fill: var(--accent-wash); }.st-awaiting .ring { stroke: var(--warn-mark); stroke-width: 2; }.st-awaiting .core { fill: var(--warn-mark); }.st-landed .ring { stroke: var(--ink); }.st-landed .tick { stroke: var(--ink); }.st-judged .disc { fill: var(--ink); }.st-merged .disc { fill: var(--ok-mark); }.st-retracted .ring { stroke: var(--stroke); }.st-retracted .slash { stroke: var(--stroke); stroke-width: 1.6; }.tick { stroke: var(--paper); stroke-width: 1.9; fill: none; stroke-linecap: round; stroke-linejoin: round; }@keyframes spin { to { transform: rotate(360deg); } }
/* ─── unit rows (goal detail) ────────────────────────────────────────────── */.unit { border-radius: 10px; margin: 0 -12px; position: relative; transition: background 220ms var(--ease-out), box-shadow 220ms var(--ease-out); }.unit + .unit .row::before { content: ""; position: absolute; left: 12px; right: 12px; top: 0; border-top: 1px solid var(--line-soft);}.unit.open { background: var(--paper); box-shadow: 0 0 0 1px var(--line), 0 10px 26px -12px oklch(0.25 0.03 262 / 0.30); z-index: 2; }.unit.open .row::before, .unit.open + .unit .row::before { border-color: transparent; }:root[data-theme="dark"] .unit.open { background: var(--raised); box-shadow: 0 0 0 1px var(--line), 0 14px 30px -14px oklch(0 0 0 / 0.6); }
.row { display: flex; align-items: center; gap: 11px; width: 100%; padding: 9px 12px; border: 0; background: none; cursor: pointer; text-align: left; color: var(--ink); border-radius: 10px; position: relative; font-size: 14px; transition: background 110ms ease;}.unit:not(.open) .row:hover { background: var(--hover); }.row .name { font-weight: 500; white-space: nowrap; }.row.dead .name, .row.dead .sub { color: var(--ink-2); }.row .sub { color: var(--ink-2); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }/* An artifact's own short title, as opposed to a line derived from its body or a word about what an agent is doing. It is the record's name, so it reads as text rather than as metadata — and it still ellipsises in the same cell, because a valid 200-character title must not widen the row. */.row .sub.titled { color: var(--ink); }/* Written before the summary so that below 560px it can float into it; see `.goal-row .tail`. */.row .tail { order: 1; display: flex; align-items: center; gap: 7px; flex: none; }.vchip { font: 600 11px/1 var(--mono); color: var(--ink-2); background: var(--sunk); border-radius: 4px; padding: 3px 5px; letter-spacing: 0.01em;}.unit.open .vchip { background: var(--hover); }/* Mono means machine identifier. These are English, so they are not mono. */.vchip.word { font-family: var(--sans); font-size: 10.5px; letter-spacing: 0.004em; }
/* ─── badges ─────────────────────────────────────────────────────────────── */.badge { display: inline-flex; align-items: center; gap: 4px; font: 600 11.5px/1 var(--sans); letter-spacing: 0.004em; padding: 3.5px 8px 3.5px 6px; border-radius: 999px; white-space: nowrap;}.badge .g { font-size: 10px; line-height: 1; }.badge .narrow { display: none; }.b-ok { color: var(--ok); background: var(--ok-wash); }.b-bad { color: var(--bad); background: var(--bad-wash); }.b-warn { color: var(--warn); background: var(--warn-wash); }.b-flat { color: var(--ink-2); background: var(--sunk); }.b-accent { color: var(--accent); background: var(--accent-wash); }.unit.open .b-flat { background: var(--hover); }
/* ─── labels ───────────────────────────────────────────────────────────────── A label's colour is a pure function of its text (`labels.ts:labelHue`), handed in as `--lh`, so two members looking at the same board see the same chips with nothing stored and nothing on-protocol to govern it. Chroma and lightness are FIXED — only the hue varies — which is what keeps twenty arbitrary strings from producing twenty different contrast ratios. Colour is the third channel here as everywhere: a chip always carries its word. */.lchip { display: inline-flex; align-items: center; gap: 4px; max-width: 15em; font: 600 11px/1 var(--sans); letter-spacing: 0.008em; padding: 3px 8px; border-radius: 999px; white-space: nowrap; color: oklch(0.42 0.09 var(--lh)); background: oklch(0.955 0.035 var(--lh)); border: 1px solid transparent;}.lchip > .t { overflow: hidden; text-overflow: ellipsis; }.lchip > .n { font-weight: 500; opacity: 0.7; font-variant-numeric: tabular-nums; }:root[data-theme='dark'] .lchip { color: oklch(0.84 0.075 var(--lh)); background: oklch(0.30 0.055 var(--lh));}/* A chip that filters. The pressed state is a border and a weight, never colour alone — the hue is already spoken for by which label it is. */button.lchip { cursor: pointer; }button.lchip:hover { border-color: oklch(0.72 0.10 var(--lh)); }button.lchip[aria-pressed='true'] { border-color: oklch(0.55 0.12 var(--lh)); font-weight: 700;}/* The tick stands in space the chip already had. A chip that GREW when it was pressed reflowed the line it was on, which on a full one moved the list underneath — the same reason the bar's second line is reserved rather than conditional. */button.lchip::after { content: '✓'; font-size: 9px; visibility: hidden; }button.lchip[aria-pressed='true']::after { visibility: visible; }.lchip .x { display: inline-grid; place-items: center; width: 13px; height: 13px; margin-right: -3px; border: 0; padding: 0; border-radius: 999px; cursor: pointer; font: 700 11px/1 var(--sans); color: inherit; background: transparent; opacity: 0.55;}.lchip .x:hover { opacity: 1; background: oklch(0.72 0.10 var(--lh) / 0.28); }.lchips { display: inline-flex; align-items: center; gap: 5px; min-width: 0; flex-wrap: wrap; }/* On a goal row the chips are a second line under the title, aligned to it — the pie's width plus the row's gap — so they read as belonging to the name rather than to the list. */.row-labels { padding: 0 0 8px 32px; margin-top: -4px; }
/* Adding a label (`LabelEditor.svelte`). The affordance stands in the row of chips it adds to; the field it opens does NOT — it is a small panel placed from that chip against the visual viewport, out of flow, because a field and a button appearing in a goal's meta row pushed every fact after them along the line and, on a full one, wrapped it and moved the description underneath. A control a reader is pointing at may not move what they were reaching for. */.labeladd { display: inline-flex; }.labeladd-pop { position: fixed; z-index: 30; width: min(288px, calc(100vw - 32px)); display: flex; align-items: center; gap: 7px; padding: 8px; border: 1px solid var(--line); border-radius: 11px; background: var(--raised); box-shadow: 0 12px 30px color-mix(in oklch, var(--ink) 14%, transparent); opacity: 0; pointer-events: none;}/* Hidden until it has been measured — with OPACITY, never `visibility`. An element whose computed visibility is not `visible` is not a focusable area, so `field.focus()` inside a panel hidden that way is refused: the caret never lands, and on a phone the soft keyboard never comes up, which is also the thing that shrinks the visual viewport this panel is placed against. `pointer-events` keeps the one unmeasured frame from catching a press at the top left of the window. */.labeladd-pop.placed { opacity: 1; pointer-events: auto; }.labeladd-pop[hidden] { display: none; }.labeladd-pop input.ti { width: 100%; min-width: 0; }/* Chrome draws the datalist's own arrow inside the field, and it is left at the browser's size: the arrow sat off centre because a field shrunk to chip size was given one shrunk to match. Here it is only centred and toned down — it is the one thing saying the space's vocabulary is behind the field, so it is not removed either. */.labeladd-pop input.ti::-webkit-calendar-picker-indicator { align-self: center; opacity: 0.5; cursor: pointer;}.labeladd-pop input.ti::-webkit-calendar-picker-indicator:hover { opacity: 0.85; }/* The chip that adds a chip: the same pill as the labels beside it, dashed because it is an affordance rather than something this goal carries. */button.lchip-add { display: inline-flex; align-items: center; gap: 4px; white-space: nowrap; font: 600 11px/1 var(--sans); letter-spacing: 0.008em; padding: 3px 9px; border-radius: 999px; cursor: pointer; color: var(--ink-2); background: transparent; border: 1px dashed var(--stroke); transition: color 130ms ease, border-color 130ms ease, background 130ms ease;}button.lchip-add:hover,button.lchip-add[aria-expanded='true'] { color: var(--accent); border-color: var(--accent); background: var(--accent-wash);}
/* ─── filter bar ───────────────────────────────────────────────────────────── Above a goal list, and only ever narrowing what is already on screen — nothing here writes, and nothing here is on-protocol until somebody names it (`views.ts`).
No rule under it. The list below opens with a `.sec-head`, which is a hairline with a word on it, and a second hairline four pixels above that one drew a box around the controls rather than separating them from anything. *//* Little padding under it, because the reserved line below usually IS the padding: a row held open to its full height with nothing on it, and then a step of padding under that, put a band of empty space between the chips and the list they narrow. */.filters { display: flex; flex-direction: column; gap: 7px; padding: 9px 2px 2px; }.filters .frow { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }/* The second line is drawn whether or not anything stands on it: "Clear" and "Save as view…" arrive the instant a chip is pressed, and a bar that grew a line under the reader's pointer would move the list they were reaching for. It is blank until then, which is what the reservation costs, and its height is the naming field's — the tallest thing that ever stands here, taken from the type token so the touch floor raises the reservation along with the text. */.filters .frow.fend { min-height: calc(var(--field-size) * 1.5 + 12px); justify-content: flex-end; }.filters .fend .ti { padding: 5px 9px; }.filters .seg { display: inline-flex; border: 1px solid var(--line); border-radius: 7px; overflow: hidden; }.filters .seg button { border: 0; background: transparent; cursor: pointer; color: var(--ink-2); font: 600 11.5px/1 var(--sans); padding: 5px 9px;}.filters .seg button + button { border-left: 1px solid var(--line); }.filters .seg button[aria-pressed='true'] { background: var(--accent-wash); color: var(--accent); }/* A reader cannot enter grouping where there is no vocabulary, but can always leave it after carrying that per-tab preference onto an unlabelled list. */.filters .seg button:disabled { opacity: 0.45; cursor: default; }/* A remark the bar owes the reader — why there is nothing here to filter by. Not scoped to `.filters`: it is one voice in one size wherever a control has to explain its own absence. */.fnote { margin: 0; color: var(--ink-2); font: 500 11.5px/1.4 var(--sans); }
/* A label's own section inside a goal list (`GoalGroups.svelte`), lighter than `.sec-head` on purpose: the heading above already said what the list is, and this says which part of it. Plain text and not the label's own chip — the pressable pills are all in the bar a few pixels up, and the same word painted twice in two weights read as two controls for one thing. */.grp-head { display: flex; align-items: baseline; gap: 7px; margin: 18px 0 5px; padding: 0 1px; font: 700 11.5px/1.3 var(--sans); letter-spacing: 0.01em; color: var(--ink-2);}.grp-head .n { font-weight: 500; opacity: 0.7; font-variant-numeric: tabular-nums; }/* The first section opens right under whatever introduced the list — a `.sec-head`. Only the gaps BETWEEN sections are the full step. */.sec-head + .grp-head { margin-top: 9px; }
/* ─── actor discs ────────────────────────────────────────────────────────── */.who { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }.disc { width: 17px; height: 17px; flex: none; display: grid; place-items: center; font: 700 9px/1 var(--sans); color: #fff; letter-spacing: 0.02em;}.disc.human { border-radius: 999px; }.disc.agent { border-radius: 5px; }/* A published profile picture, in the disc's own box and shape. The derived fill stays behind it, so a transparent or slow image degrades to the colour it would have had. */.disc > img { width: 100%; height: 100%; object-fit: cover; border-radius: inherit; display: block; }
/* What a row leads with when there is nobody to draw a disc for. A disc is an atproto-identity affordance — its colour and its initials are derived from a DID — so a GitHub issue, whose filer is a login and not an identity this app can check, gets a neutral mark instead of a disc that would claim more than is known. Drawn in the disc's own box so the row's gutter is the same width either way. */.mark { width: 17px; height: 17px; flex: none; display: grid; place-items: center; font-size: 11px; line-height: 1; color: var(--stroke);}
/* ─── drawer ─────────────────────────────────────────────────────────────── */.drawer { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 400ms var(--ease-out); }.unit.open .drawer { grid-template-rows: 1fr; }.drawer-in { overflow: hidden; min-height: 0; }.drawer-pad { padding: 2px 12px 14px 41px; }
.kv { display: flex; flex-wrap: wrap; gap: 4px 14px; font-size: 12.5px; color: var(--ink-2); margin: 0 0 12px; align-items: center; }.kv b { font-weight: 600; color: var(--ink); }
.brief { border-left: 1px solid var(--line); padding: 1px 0 1px 12px; margin: 0 0 14px; font-size: 13.5px; line-height: 1.55; color: var(--ink-2); max-width: 64ch; white-space: pre-wrap;}/* The artifact's own title, over the version being read. Set above the body's own headings — it names the whole record, and the body's `# ` heading (where it has one) is a heading inside it. It wraps rather than ellipsising: there is room for a line here, unlike in a row. */.atitle { font-size: 15.5px; font-weight: 600; letter-spacing: -0.004em; line-height: 1.35; margin: 0 0 8px; max-width: 66ch; color: var(--ink);}.body { font-size: 13.5px; line-height: 1.62; max-width: 66ch; margin: 0 0 14px; color: var(--ink); }.body p { margin: 0 0 0.9em; }.body p:last-child { margin-bottom: 0; }.artifact-review { position: relative; max-width: 66ch; }.artifact-prose { outline: none; }.artifact-prose ::selection { background: color-mix(in srgb, var(--accent) 24%, transparent); }.inline-review { position: absolute; z-index: 8; width: min(272px, calc(100vw - 32px)); padding: 11px; transform: translateX(-50%); border: 1px solid var(--line); border-radius: 10px; background: var(--raised); box-shadow: 0 10px 28px rgba(20, 26, 31, 0.16);}.inline-review-head, .inline-review-actions { display: flex; align-items: center; justify-content: space-between; gap: 8px;}.inline-review-head { margin-bottom: 8px; font-size: 12.5px; }.inline-review .iconbtn { width: 24px; height: 24px; }.inline-review blockquote { max-height: 72px; margin: 0 0 9px; padding: 6px 8px; overflow: auto; border-left: 1px solid var(--accent); color: var(--ink-2); font-size: 11.5px; line-height: 1.45;}.inline-review .ta { width: 100%; min-height: 64px; margin-bottom: 9px; resize: vertical; }.inline-review-actions .sl { width: auto; }.body .bh { font-size: 13px; font-weight: 700; letter-spacing: 0.001em; margin: 21px 0 7px; color: var(--ink); }/* A body's own `#` title sits above its sections; below `##` the levels stop growing apart and only the weight carries the step, so a deep outline never shouts. */.body .bh[data-level='1'] { font-size: 14px; margin-top: 24px; }.body .bh[data-level='4'], .body .bh[data-level='5'], .body .bh[data-level='6'] { font-weight: 600; color: var(--ink-2); margin-top: 17px;}.body > .bh:first-child { margin-top: 2px; }.body .plain { white-space: pre-wrap; }.body code, code.inl { font: 12.5px/1.4 var(--mono); background: var(--sunk); padding: 1px 4px; border-radius: 4px; }.unit.open .body code, .unit.open code.inl { background: var(--hover); }.body strong { font-weight: 600; color: var(--ink); }.body em { font-style: italic; }.body del { text-decoration: line-through; color: var(--ink-2); }.body .lk { color: var(--accent); text-decoration: none; border-bottom: 1px solid var(--line); }.body .lk:hover { border-bottom-color: var(--accent); }/* An image is shown at the prose measure and never wider, so a screenshot pasted into a plan sits in the column the text is read in rather than stretching the pane. */.body .ig { display: block; max-width: 100%; height: auto; margin: 0.9em 0; border: 1px solid var(--line-soft); border-radius: 7px; background: var(--sunk);}/* One whose address we will not fetch from — a relative path, or a scheme that is not http(s) — is named instead, so the reader knows the body meant to show something here. */.body .im { color: var(--ink-2); }.body .im span { margin-right: 4px; color: var(--stroke); }
/* A fenced block is a quotation of code: the mono column runs the measure, and a long line scrolls inside the block rather than widening the pane. */.body .cb { margin: 0 0 0.9em; padding: 9px 11px; background: var(--sunk); border: 1px solid var(--line-soft); border-radius: 7px; overflow-x: auto;}.body .cb code { display: block; font: 12px/1.55 var(--mono); background: none; padding: 0; border-radius: 0; white-space: pre; }.unit.open .body .cb { background: var(--hover); }
.body .bl { margin: 0 0 0.9em; padding-left: 19px; }.body .bl li { margin: 0 0 0.2em; }.body .bl li::marker { color: var(--stroke); }.body .bl .bl { margin: 0.2em 0 0; }.body .bl p { margin: 0 0 0.4em; }.body .bl li.task { list-style: none; margin-left: -19px; padding-left: 19px; text-indent: -19px; }.body .bl .tk { display: inline-block; width: 19px; text-indent: 0; color: var(--stroke); }.body .bq { margin: 0 0 0.9em; padding-left: 12px; border-left: 1px solid var(--line); color: var(--ink-2);}.body .br { border: 0; border-top: 1px solid var(--line-soft); margin: 18px 0; }
/* A table is the one block allowed past the measure — its own scroller, so a wide one never widens the pane the prose is read in. */.body .tw { margin: 0 0 0.9em; overflow-x: auto; }.body .tb { border-collapse: collapse; font-size: 13px; }.body .tb th, .body .tb td { text-align: left; padding: 5px 12px 5px 0; vertical-align: top; border-bottom: 1px solid var(--line-soft); white-space: nowrap;}.body .tb th { font-weight: 600; color: var(--ink-2); font-size: 11.5px; letter-spacing: 0.02em; text-transform: uppercase; }.body .tb tr:last-child td { border-bottom: 0; }
.crit { list-style: none; margin: 0 0 14px; padding: 0; max-width: 64ch; }.crit li { display: flex; gap: 8px; font-size: 13.5px; line-height: 1.5; padding: 3px 0; color: var(--ink); }.crit .m { color: var(--stroke); flex: none; margin-top: 1px; }
.rail-v { display: flex; align-items: center; gap: 6px; margin: 0 0 14px; flex-wrap: wrap; }.vbtn { border: 1px solid var(--line); background: none; color: var(--ink-2); border-radius: 6px; padding: 3px 9px; cursor: pointer; font: 600 11.5px/1.4 var(--mono); transition: all 130ms ease;}.vbtn:hover { border-color: var(--stroke); color: var(--ink); }.vbtn[aria-pressed="true"] { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }.rail-v .arrow { color: var(--stroke); font-size: 11px; }
.finding { display: grid; grid-template-columns: auto 1fr; gap: 3px 9px; padding: 9px 0; border-top: 1px solid var(--line-soft); font-size: 13px; max-width: 64ch;}.finding:first-of-type { border-top: 0; }.finding .sev { font: 600 10.5px/1.5 var(--sans); letter-spacing: 0.01em; grid-row: 1; }.finding .loc { grid-column: 2; font: 11.5px/1.4 var(--mono); color: var(--ink-2); }/* The body is prose, rendered by the same `Prose` every other body gets — so it may be a list, a fenced block or a screenshot. It sits in the finding's column at the finding's type scale, and its own measure is dropped: the row already runs at 64ch, and a second, wider one inside it would let an image out of the column the severity and the location are read in. */.finding .body { grid-column: 2; margin: 0; font-size: 13px; line-height: 1.5; /* `min-width: 0` because a grid track's automatic minimum is its content's, and a fenced block that scrolls inside itself only scrolls once the column is allowed to be narrower than it. */ max-width: none; min-width: 0;}.sev-error { color: var(--bad); }.sev-warning { color: var(--warn); }.sev-info { color: var(--ink-2); }
.checks { display: flex; flex-direction: column; gap: 0; margin: 0 0 14px; max-width: 64ch; }.check { display: flex; align-items: center; gap: 9px; padding: 6px 0; border-top: 1px solid var(--line-soft); font-size: 13px; }.check:first-child { border-top: 0; }.check .nm { flex: 1; font-family: var(--mono); font-size: 12.5px; }
/* ─── actions ────────────────────────────────────────────────────────────── */.acts { display: flex; flex-wrap: wrap; align-items: center; gap: 7px; padding-top: 12px; border-top: 1px solid var(--line-soft); }.btn { border: 1px solid var(--line); background: var(--paper); color: var(--ink); border-radius: 7px; padding: 6px 12px; cursor: pointer; font-size: 13px; font-weight: 500; transition: background 130ms ease, border-color 130ms ease, transform 130ms var(--ease-out);}/* A link that is a button — "Settings" on a project row. It has to look like the buttons beside it, not like prose, so the anchor defaults are undone rather than worked around. */a.btn { text-decoration: none; color: var(--ink); display: inline-block; }.btn:hover { background: var(--hover); border-color: var(--stroke); }.btn:active { transform: translateY(0.5px); }.btn:disabled { color: var(--ink-2); background: var(--sunk); border-color: var(--line-soft); cursor: default; }.btn:disabled:hover { transform: none; border-color: var(--line-soft); }.btn-p { background: var(--accent); border-color: var(--accent); color: var(--on-accent); font-weight: 600; }.btn-p:hover { background: var(--accent-deep); border-color: var(--accent-deep); }.btn-ok { color: var(--ok); border-color: color-mix(in oklch, var(--ok) 32%, var(--line)); }.btn-ok:hover { background: var(--ok-wash); border-color: var(--ok); }.btn-bad { color: var(--bad); border-color: color-mix(in oklch, var(--bad) 28%, var(--line)); }.btn-bad:hover { background: var(--bad-wash); border-color: var(--bad); }.acts .gap { width: 1px; height: 20px; background: var(--line); margin: 0 3px; }/* A picker standing in a row of buttons — the goal ending's disposition. The same control as every other `select.sl`, sized to its content instead of to the row, exactly like `.fedit-where`'s. */.acts label { font-size: 11.5px; font-weight: 600; color: var(--ink-2); }.acts select.sl { width: auto; }
/* ─── the ask bar ────────────────────────────────────────────────────────── *//* The registry's request buttons, standing on the goal they are asked against (`RequestBar.svelte`). A plain row of controls with no box, no rule and no fill: the ask is the reader's next move, not a call to action shouted at them, and one accent-filled button here would be the UI ranking the registry's types on their behalf. It occupies the compose card's own slot, so pressing a button swaps the row for the card in place rather than moving the page under the reader. */.askbar { display: flex; flex-wrap: wrap; align-items: center; gap: 7px; margin: 2px 0 16px; }.askbar .asklead { font-size: 12px; font-weight: 600; color: var(--ink-2); margin-right: 1px; }.askbtn { display: inline-flex; align-items: center; gap: 7px; }.askbtn .g { display: grid; place-items: center; color: var(--stroke); }.askbtn:hover .g, .askbtn:focus-visible .g { color: var(--accent); }
.softwarn { display: flex; align-items: flex-start; gap: 8px; font-size: 12.5px; line-height: 1.45; color: var(--warn); background: var(--warn-wash); border-radius: 7px; padding: 7px 10px; margin: 12px 0 0; max-width: 62ch;}.softwarn .g { flex: none; margin-top: 1px; }/* Drift is information, not an alarm. Amber in this system means "stopped on you"; nothing about a drifting document is stopped, so it stays neutral. */.softnote { display: flex; align-items: flex-start; gap: 9px; font-size: 12.5px; line-height: 1.5; color: var(--ink-2); background: var(--sunk); border-radius: 7px; padding: 9px 11px; margin: 14px 0 0; max-width: 62ch;}.unit.open .softnote { background: var(--hover); }.softnote .g { flex: none; margin-top: 1px; font-weight: 700; color: var(--ink); }.softnote b { color: var(--ink); font-weight: 600; }.grouphint { margin: 7px 0 8px; font-size: 12.5px; line-height: 1.5; color: var(--ink-2); max-width: 74ch; }.linkish { border: 0; background: none; padding: 0; margin: 0; cursor: pointer; color: var(--accent); font: inherit; text-decoration: underline; text-underline-offset: 2px; text-decoration-thickness: 1px;}.linkish:hover { color: var(--accent-deep); }
/* ─── uri chip ───────────────────────────────────────────────────────────── */.uri { font: 11.5px/1.3 var(--mono); color: var(--ink-2); background: var(--sunk); border: 0; border-radius: 4px; padding: 3px 6px; cursor: copy; transition: all 130ms ease; white-space: nowrap;}.uri:hover { color: var(--ink); background: var(--hover); }.uri.copied { color: var(--ok); background: var(--ok-wash); }.unit.open .uri { background: var(--hover); }
/* ─── thread ─────────────────────────────────────────────────────────────── */.msg { display: grid; grid-template-columns: auto 1fr; gap: 3px 10px; padding: 11px 0; border-top: 1px solid var(--line-soft); }.msg:first-of-type { border-top: 0; }.msg .disc { grid-row: span 2; margin-top: 2px; }.msg .mh { display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; font-size: 12.5px; }.msg .mh b { font-size: 13px; font-weight: 600; }.msg .mb { font-size: 13.5px; line-height: 1.55; max-width: 64ch; white-space: pre-wrap; margin: 2px 0 0; }.msg.question { background: var(--warn-wash); border-radius: 8px; padding: 11px 12px; margin: 8px -12px; border-top: 0; }/* A reply sits one level in, behind a hairline. One level and no further: a deeper chain flattens onto the same rail and says who it was answering, because a conversation drawn as a tree is one nobody reads (DESIGN.md, Measure). */.replies { margin-left: 15px; padding-left: 14px; border-left: 1px solid var(--line-soft); }.replies .msg:first-of-type { border-top: 1px solid var(--line-soft); }/* The two things a person can do to a message. Quiet by default — they are always there, on every row, and a thread of buttons is a thread nobody reads. */.msg-acts { grid-column: 2; display: flex; gap: 12px; margin-top: 5px; font-size: 12px; }.msg-acts .linkish { font-size: 12px; }/* The "ask an agent to reply" card opens inside the row. Like everything else below the header it belongs beside the disc, not under it: auto-placement would drop it into column 1, whose `auto` track then grows to the width of a form and pushes the message itself off to the right. */.msg .newreq { grid-column: 2; }/* A reply somebody owes: the same disappearing-request behaviour a review request has, in the thread rather than on a row. */.msg .pending, .msg-acts + .pending { grid-column: 2; display: flex; align-items: center; gap: 7px; margin-top: 6px; font-size: 12.5px; color: var(--ink-2);}.msg .pending .dot { width: 13px; height: 13px; flex: none; }.msg .pending b { color: var(--ink); font-weight: 600; }
/* ─── community (comments from outside the space) ────────────────────────── *//* Apart from the thread on purpose. Everything above is a record this space's own fold admitted; everything here is text a stranger wrote that the tab went and fetched. A guest row is inset behind its own rail and reads a shade quieter, so the difference between "somebody here said this" and "a stranger said this" is visible before any badge is read. The other two lists of outside words — a userinput board, and issues on the project's forge — are pages of their own rather than a section beside a thread, so they are drawn as unit rows with the stranger's prose quoted in `.brief` and take nothing from here. */.community { margin-left: 2px; padding-left: 14px; border-left: 2px solid var(--line-soft); }/* Profile resolution gives a guest the same disc-and-prose geometry as a thread message. Keeping the body in column two makes it begin under the author's name, never under their avatar. */.community .guest { color: var(--ink-2); }.community .guest .mb { grid-column: 2; }.community .guest .msg-acts { flex-wrap: wrap; align-items: baseline; }/* Blessed: this one is in every turn bundle on the goal, which is worth the accent rail. */.community .guest.blessed { border-left: 2px solid var(--accent); margin-left: -16px; padding-left: 14px; }.community .guest .hint { font-size: 12.5px; color: var(--warn); }
/* ─── writing ────────────────────────────────────────────────────────────── *//* Every field a human types a record into: the goal composer's two, the thread box, the request brief, and the assignee picker. One set of rules rather than one per card, because they are one control in three shapes. */textarea.ta, input.ti, select.sl { width: 100%; border: 1px solid var(--line); background: var(--paper); color: var(--ink); border-radius: 8px; padding: 8px 10px; font-family: inherit; font-size: var(--field-size); line-height: 1.5; resize: vertical; outline: 0; transition: border-color 130ms ease, box-shadow 130ms ease;}/* A field whose value is a machine identifier — a git remote, a branch, a check command, a path — so mono, per the direction contract at the head of this file, and a step down with it. It is a class rather than the `style=` attribute six of these carried, because an inline declaration is the one place a size can be written that the touch floor cannot reach. */input.ti.ident { font-family: var(--mono); font-size: var(--field-ident-size); }textarea.ta { min-height: 60px; }textarea.ta:focus, input.ti:focus, select.sl:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-wash); }textarea.ta::placeholder, input.ti::placeholder { color: var(--ink-2); }textarea.ta:disabled, input.ti:disabled, select.sl:disabled { background: var(--sunk); color: var(--ink-2); }/* The picker holds identities, not prose: a chevron rather than a caret, and the platform's own menu below it. `appearance: none` would mean drawing and theming a listbox to gain nothing. */select.sl { resize: none; cursor: pointer; padding-right: 8px; }
/* ─── the markdown editor ────────────────────────────────────────────────── *//* Every field that holds markdown (`MarkdownEditor.svelte`). It is deliberately the same object as `.ta` above — same border, same radius, same focus ring — because a field that highlights what you type is still a field, and drawing it as a code editor would make a goal's body look like a different kind of thing from its title. What the highlighting adds is confirmation, not chrome.
The editor and the textarea it degrades to are both in the DOM, so both are styled here and the hidden one takes no space. */.md { position: relative; }.md > [hidden] { display: none; }/* Both halves take the field size — the editor from its own theme (`lib/editor.ts`), the textarea from here — because the fallback is the same object one browser down, and a box that changes size depending on whether CodeMirror loaded is not the same object. */.md .md-host, .md textarea.md-plain { width: 100%; border: 1px solid var(--line); background: var(--paper); color: var(--ink); border-radius: 8px; outline: 0; transition: border-color 130ms ease, box-shadow 130ms ease; min-height: var(--md-min, 60px); max-height: var(--md-max, 40vh); font-size: var(--field-size); line-height: 1.5;}/* CodeMirror draws its own padding (`editor.ts`), so the host is only the box. */.md .md-host { display: grid; overflow: hidden; }.md .md-host .cm-editor { max-height: var(--md-max, 40vh); }.md .md-host:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-wash); }/* The fallback keeps the grip a plain field has; the editor grows with its content instead. */.md textarea.md-plain { resize: vertical; overflow-y: auto; }
/* Inside a `.compose`, which owns the border and the one focus ring around the field and its bar. */.md-flush .md-host, .md-flush textarea.md-plain { border: 0; border-radius: 10px 10px 0 0; background: transparent; min-height: var(--md-min, 0);}.md-flush .md-host:focus-within { box-shadow: none; }.md-flush textarea.md-plain { resize: none; padding: 9px 11px 2px; }
/* Adding an image. A link rather than a button: it is one more thing you can do with the box, not an action on the record, and the buttons that write the record are already below it. */.md-bar { display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 8px; padding: 5px 2px 0; font-size: 12px;}.md-flush .md-bar { padding: 0 11px 4px; }.md-bar .linkish { font-size: 12px; }.md-hint { color: var(--ink-2); }/* Off screen rather than `display:none`: a hidden input cannot be clicked programmatically in every browser, and this one is driven by the link beside it. */.md-file { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }/* ─── the composer ───────────────────────────────────────────────────────── *//* The box and what you can do with what is in it are one object, so they share one border and one focus ring: the field is the card, and the bar along its bottom edge holds the actions. What was here before put the button beside the box and the picker and its button on two more lines under it, which drew a three-storey form for a one-line message and left "Post" floating against nothing. The box grows with its content (`MarkdownEditor.svelte`, `.md-flush` below) and has no resize grip — a drag handle to read your own sentence is not a feature, and it is the one diagonal scribble on a page otherwise made of hairlines. */.compose { margin-top: 14px; background: var(--paper); border: 1px solid var(--line); border-radius: 10px; transition: border-color 130ms ease, box-shadow 130ms ease;}.compose:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-wash); }/* A guest signs outside the space, so the composer says which public identity will author the record. Keep that identity on the field's own inset, then tighten the editor below it: its normal top padding is for a composer where the editor is the first child. */.compose-who { padding: 8px 10px 0; font-size: 12.5px; color: var(--ink); }.compose-who + .md-flush .cm-content { padding-top: 2px; }.compose-who + .md-flush textarea.md-plain { padding-top: 2px; }/* The field inside carries none of that: one ring around the pair, not two nested ones. */.compose textarea.ta { border: 0; border-radius: 10px 10px 0 0; background: transparent; resize: none; overflow-y: auto; min-height: 0; max-height: 40vh; padding: 9px 11px 2px;}.compose textarea.ta:focus { box-shadow: none; }.compose textarea.ta:disabled { background: transparent; }.compose-bar { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 7px; padding: 7px 8px 8px; }
/* The ask, as one button. Pressing `@planner` posts the message and writes the request that asks that agent to reply; the chevron beside it is where "that agent" is changed, and it is only there when the space has more than one agent to change it to. It was a full-width picker on its own line above a button called "Post & ask an agent" — two controls that had to be read together and gave no sign that they were one, so the only way to find out what the picker did was to press the other thing. */.askpick { position: relative; display: inline-flex; align-items: stretch; }.askpick .askdo { display: inline-flex; align-items: center; gap: 6px; max-width: 15rem; }.askpick .askdo .h { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.askpick .askdo .disc { flex: none; }/* Two halves of one control: the seam between them is a single hairline, not two borders and a gap. */.askpick:has(.askmore) .askdo { border-top-right-radius: 0; border-bottom-right-radius: 0; }.askmore { border-top-left-radius: 0; border-bottom-left-radius: 0; border-left: 0; padding: 7px 8px; display: grid; place-items: center; color: var(--ink-2);}.askmore:hover { color: var(--ink); }.askmore[aria-expanded="true"] { background: var(--hover); border-color: var(--stroke); color: var(--ink); }.askmore svg { transition: transform 200ms var(--ease-out); }.askmore[aria-expanded="true"] svg { transform: rotate(180deg); }/* Upward, because this control lives at the bottom of the last card on the page. */.askmenu { position: absolute; right: 0; bottom: calc(100% + 7px); z-index: 21; min-width: 232px; max-width: min(320px, 80vw); padding: 5px; text-align: left; background: var(--paper); border: 1px solid var(--line); border-radius: 10px; box-shadow: 0 2px 6px oklch(0.25 0.03 262 / 0.10), 0 18px 40px -10px oklch(0.25 0.03 262 / 0.34); animation: pop 180ms var(--ease-out);}.askmenu[hidden] { display: none; }.askmenu .menu-item { font-size: 13px; }.askmenu .menu-item .disc { flex: none; }.askmenu .tick { flex: none; width: 12px; color: var(--accent); font-size: 12px; }.askmenu .menu-item:hover .tick, .askmenu .menu-item:focus-visible .tick { color: var(--on-accent); }/* Signed out, or looking at the fixture: the box is not there to be filled in, so it is replaced by the reason rather than shown disabled. */.compose-note { margin-top: 14px; font-size: 12.5px; line-height: 1.5; color: var(--ink-2); display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap;}.write-error { margin: 9px 0 0; font-size: 12.5px; line-height: 1.5; color: var(--bad); background: var(--bad-wash); border-radius: 7px; padding: 8px 10px; max-width: 62ch;}
/* ─── compose card ───────────────────────────────────────────────────────── */.newreq { margin: 8px -12px 16px; padding: 14px; background: var(--raised); border-radius: 10px; box-shadow: 0 0 0 1px var(--line), 0 10px 26px -12px oklch(0.25 0.03 262 / 0.30); animation: pop 300ms var(--ease-out);}.request-overlay { position: fixed; z-index: 38; margin: 0; width: min(520px, calc(100vw - 24px)); overflow: auto; overscroll-behavior: contain; opacity: 0; pointer-events: none;}/* Opacity rather than `visibility`, for the reason `.labeladd-pop` states: this card takes the caret into its first field as it opens, and a `visibility: hidden` card refuses it. The entrance is suppressed until it is placed, because an animation on `opacity` would otherwise draw the card at the top left of the window for the frame before it has been measured. */.request-overlay.placed { opacity: 1; pointer-events: auto; }.request-overlay:not(.placed) { animation-name: none; }@keyframes pop { from { opacity: 0; transform: translateY(-6px) scale(0.995); } }/* The goal composer is a modal (`.scrim .newgoal`) rather than a card in the page, but it is the same object saying the same things, so the heading and the hint under it are drawn once for both. */.newreq h3, .newgoal h3 { margin: 0 0 3px; font-size: 14px; font-weight: 600; display: flex; align-items: center; gap: 8px; }.newreq .hint, .newgoal .hint { margin: 0 0 11px; font-size: 12.5px; line-height: 1.5; color: var(--ink-2); }/* Not scoped to the card: the space composer lives inside the picker, which is already a raised surface, so it borrows these fields and none of the card's chrome. */.fields { display: grid; gap: 9px; }.fields label { display: block; font-size: 11.5px; font-weight: 600; color: var(--ink-2); margin-bottom: 4px; }.fields label .plain { font-weight: 400; }
/* ─── verdict card ───────────────────────────────────────────────────────── *//* There is no verdict *picker* here, deliberately: the card's two actions are the two verdicts, so `.btn-ok` / `.btn-bad` in its `.acts` bar are the whole of it, and the glyph on each carries which is which alongside the colour. A selector above them would put a third and fourth button on screen labelled the same as the two that write, and the inert pair is the pair a reader presses first. */
/* One finding, being written. The same three things the read-side `.finding` shows — severity, where, what — so writing one and reading one look like the same object. The locator row is flex rather than a grid so it folds on a narrow viewport without a breakpoint of its own. */.fedit { display: grid; gap: 7px; padding: 9px 0; border-top: 1px solid var(--line-soft); max-width: 64ch;}.fedit:first-of-type { border-top: 0; }.fedit-where { display: flex; flex-wrap: wrap; align-items: center; gap: 7px; }.fedit-where select.sl { width: auto; }.fedit-where .where { flex: 1 1 14ch; min-width: 14ch; }.fedit-where .line { width: 76px; text-align: right; }/* Machine identifiers, so mono — a path and a line number are exactly that. Every `.ti` in the row, rather than `.ident` on each: the row holds nothing else. */.fedit-where .ti { font-family: var(--mono); font-size: var(--field-ident-size); }.fedit-where .btn { margin-left: auto; padding: 6px 10px; color: var(--ink-2); }.fedit .ta { min-height: 52px; }
/* ─── admin rows ─────────────────────────────────────────────────────────── *//* A member, a registry entry, a project, a check, an auto-review switch: the same object five times over — a name, what the space knows about it, and at most one thing you can do to it. One row, so the space page reads as one list of what this space is rather than five settings panels. */.arow { display: flex; align-items: center; gap: 10px; padding: 9px 12px; margin: 0 -12px; border-radius: 8px; border-top: 1px solid var(--line-soft); font-size: 13.5px;}.arow:first-child { border-top: 0; }.arow:hover { background: var(--hover); }.arow .nm { font-weight: 500; white-space: nowrap; flex: none; }.arow a.nm { color: var(--ink); text-decoration: none; }.arow a.nm:hover { color: var(--accent); }.arow .meta { flex: 1; min-width: 0; color: var(--ink-2); font-size: 12.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;}.arow .tail { display: flex; align-items: center; gap: 7px; flex: none; }.arow .tail .btn { padding: 4px 10px; font-size: 12px; }/* A key and its value: the label reads as prose and the value is mono, because everything on the left of these rows is a word and everything on the right is a machine identifier. */.arow .key { min-width: 10em; }.arow .meta code.inl { font-size: 12.5px; }/* Removed, or shadowed by an earlier record: still on the page, because the space is still carrying it, and reading as spent rather than as current. */.arow.dim .nm, .arow.dim code.nm { color: var(--ink-2); }
/* An auto-review switch. Accent means selection, and "on" is a selection, so the chosen state fills the way a chosen version button does — and the word carries it as well as the fill. */.toggle { min-width: 56px; text-align: center; font-variant-numeric: tabular-nums; }.toggle[aria-pressed="true"] { background: var(--accent); border-color: var(--accent); color: var(--on-accent); font-weight: 600;}.toggle[aria-pressed="true"]:hover { background: var(--accent-deep); border-color: var(--accent-deep); }
/* ─── account ────────────────────────────────────────────────────────────── *//* Reading needs no account, so this is quiet until it has something to say. Signed in it is the human's own disc; signed out it is one word. */.acct { position: relative; display: flex; align-items: center; }.acct-btn { display: flex; align-items: center; gap: 6px; max-width: 150px; border: 0; background: none; border-radius: 7px; padding: 3px 6px; cursor: pointer; color: var(--ink-2); font-size: 12.5px;}.acct-btn:hover { background: var(--hover); color: var(--ink); }.acct-btn .who-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.acct-pop { position: absolute; top: calc(100% + 8px); right: 0; z-index: 30; width: 292px; padding: 13px; background: var(--paper); border: 1px solid var(--line); border-radius: 11px; box-shadow: 0 2px 6px oklch(0.25 0.03 262 / 0.10), 0 18px 40px -10px oklch(0.25 0.03 262 / 0.34); text-align: left;}.acct-pop[hidden] { display: none; }.acct-pop h3 { margin: 0 0 3px; font-size: 13.5px; font-weight: 600; }.acct-pop .hint { margin: 0 0 10px; font-size: 12px; line-height: 1.5; color: var(--ink-2); }.signin-warning { margin: 10px 0 12px; padding: 9px 10px; border: 1px solid var(--warn-mark); border-radius: 8px; background: var(--warn-wash); color: var(--ink); font-size: 11.5px; line-height: 1.45;}.signin-warning p { margin: 0; }.signin-warning p + p { margin-top: 5px; }.signin-recent { display: grid; gap: 4px; margin: 11px 0; }.signin-identity { display: flex; align-items: center; justify-content: space-between; gap: 10px; width: 100%; padding: 7px 8px; border: 0; border-radius: 7px; background: var(--sunk); color: var(--ink); cursor: pointer; font: 12px var(--mono); text-align: left;}.signin-identity span:first-child { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.signin-identity:hover, .signin-identity:focus-visible { background: var(--hover); outline: 0; }.signin-identity:disabled { color: var(--ink-2); cursor: default; }.signin-or { display: flex; align-items: center; gap: 8px; margin: 12px 0 9px; color: var(--ink-2); font-size: 10.5px; }.signin-or::before, .signin-or::after { content: ''; height: 1px; flex: 1; background: var(--line-soft); }.signin-or span { flex: none; }/* Where the field's suggestions come from, said on both sign-in surfaces before anything is typed: the remembered ones are this browser's, the rest are a public directory's, and asking one means the prefix leaves the tab (`handle-suggest.svelte.ts`). */.signin-source { margin: 8px 0 0; font-size: 11.5px; line-height: 1.45; color: var(--ink-2); }/* The list under the sign-in field. Drawn by the app rather than left to a `<datalist>`, because a directory's answers arrive after the keystroke that asked for them and the browser's own popup is closed by then — which is why the fully logged out screen, the one with nothing remembered to show in the meantime, never appeared to have a typeahead at all (`SignInField.svelte`). Over the content rather than in it: the button below must not move while somebody is typing. */.signin-combo { position: relative; }.signin-list { position: absolute; z-index: 40; left: 0; right: 0; top: calc(100% + 4px); display: grid; gap: 1px; padding: 4px; max-height: 188px; overflow: auto; background: var(--paper); border: 1px solid var(--line); border-radius: 9px; box-shadow: 0 2px 6px oklch(0.25 0.03 262 / 0.10), 0 14px 30px -10px oklch(0.25 0.03 262 / 0.30);}.signin-list[hidden] { display: none; }/* A row is a face and a handle: the picture the directory named for it, or the disc every other identity in the app falls back to (`Disc.svelte`). The face never shrinks and the handle is what gives, so a long domain truncates rather than squeezing the thing somebody recognises. */.signin-option { width: 100%; padding: 6px 8px; border: 0; border-radius: 6px; background: none; cursor: pointer; color: var(--ink); text-align: left; font-family: var(--mono); font-size: var(--field-ident-size); line-height: 1.4; display: flex; align-items: center; gap: 8px; overflow: hidden;}.signin-face { flex: none; display: flex; }.signin-handle { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.signin-option:hover { background: var(--hover); }.signin-option.on { background: var(--accent); color: var(--on-accent); }.acct-pop .acts { padding-top: 11px; }.acct-who { display: flex; align-items: center; gap: 8px; margin-bottom: 9px; }.acct-who .nm { font-size: 13.5px; font-weight: 600; }
/* ─── magic plus ─────────────────────────────────────────────────────────── */.plus { position: absolute; right: 24px; bottom: 24px; z-index: 20; width: 46px; height: 46px; border-radius: 999px; border: 0; background: var(--accent); color: var(--on-accent); cursor: pointer; display: grid; place-items: center; box-shadow: 0 2px 4px oklch(0.25 0.03 262 / 0.20), 0 10px 22px -6px oklch(0.35 0.12 264 / 0.45); transition: transform 200ms var(--ease-out), background 160ms ease, box-shadow 200ms var(--ease-out);}.plus:hover { background: var(--accent-deep); transform: translateY(-1.5px); box-shadow: 0 3px 6px oklch(0.25 0.03 262 / 0.22), 0 16px 30px -8px oklch(0.35 0.12 264 / 0.5); }.plus:disabled { background: var(--sunk); color: var(--ink-2); cursor: default; box-shadow: none; }.plus:disabled:hover { background: var(--sunk); transform: none; box-shadow: none; }.plus[hidden] { display: none; }.plus svg { transition: transform 300ms var(--ease-out); }.plus[aria-expanded="true"] svg { transform: rotate(135deg); }
.menu { position: absolute; right: 24px; bottom: 82px; z-index: 21; width: 306px; padding: 6px; background: var(--paper); border: 1px solid var(--line); border-radius: 11px; box-shadow: 0 2px 6px oklch(0.25 0.03 262 / 0.10), 0 18px 40px -10px oklch(0.25 0.03 262 / 0.34); transform-origin: bottom right; transition: opacity 180ms ease, transform 220ms var(--ease-out), visibility 220ms;}.menu[hidden] { display: block; visibility: hidden; opacity: 0; transform: scale(0.94) translateY(6px); pointer-events: none; }.menu-head { font-size: 11px; font-weight: 700; color: var(--ink-2); padding: 7px 9px 5px; }.menu-item { display: flex; align-items: center; gap: 10px; width: 100%; border: 0; background: none; border-radius: 7px; padding: 7px 9px; cursor: pointer; text-align: left; color: var(--ink); font-size: 13.5px;}.menu-item:hover, .menu-item:focus-visible { background: var(--accent); color: var(--on-accent); outline: 0; }.menu-item:hover .mdesc, .menu-item:focus-visible .mdesc { color: color-mix(in oklch, var(--on-accent) 74%, transparent); }.menu-item:hover .glyph, .menu-item:focus-visible .glyph { color: var(--on-accent); }.menu-item .mtxt { flex: 1; min-width: 0; }.menu-item .mname { display: block; font-weight: 500; }.menu-item .mdesc { display: block; font-size: 11.5px; line-height: 1.35; color: var(--ink-2); margin-top: 2px; }.menu-item .glyph { color: var(--stroke); flex: none; display: grid; place-items: center; width: 17px; }.menu-rule { height: 1px; background: var(--line-soft); margin: 5px 8px; }
/* ─── space picker ───────────────────────────────────────────────────────── *//* The first screen, and the only one that exists before there is a space. It is the same paper pane, without a rail to put beside it — so it sits on the measure rather than filling the width. *//* No space means no rail, and nothing to fill 860px with. The card sizes to what it holds, and carries its own margin because the body stopped having padding when the frame went away. */.app.solo { grid-template-columns: minmax(0, 1fr); width: min(620px, calc(100% - 48px)); height: auto; margin: 24px 0;}.app.solo .scroll { padding: 0; overflow: visible; }.app.solo .sheet { max-width: none; }.app.solo .picker { max-width: none; }.picker { max-width: 52ch; padding: 34px 30px 30px; }.picker.overlay { padding: 0; }.picker-head { display: flex; align-items: flex-start; gap: 13px; }.picker-head .mark { width: 26px; height: 26px; color: var(--accent); margin-top: 3px; }.picker h1 { margin: 0; font-size: 24px; font-weight: 600; letter-spacing: -0.018em; }.picker-sub { margin: 7px 0 0; font-size: 13.5px; line-height: 1.55; color: var(--ink-2); }.picker-sub code { font: 12px var(--mono); background: var(--sunk); border-radius: 4px; padding: 1px 4px; }.picker form { margin-top: 22px; }.field { display: block; }.field-label { display: block; font-size: 11.5px; font-weight: 600; color: var(--ink-2); margin-bottom: 5px; }/* A handle, a DID, a PDS address, a space URI: identifiers, the same as `.ident` above. Long-hand rather than the `font` shorthand it used to be, so the size is one declaration the floor's token reaches rather than a component of a shorthand. */.field input { width: 100%; background: var(--sunk); border: 1px solid transparent; border-radius: 8px; padding: 9px 11px; outline: 0; font-family: var(--mono); font-size: var(--field-ident-size); line-height: 1.4; transition: border-color 140ms ease, background 140ms ease;}.field input:focus { border-color: var(--accent-mark); background: var(--paper); }.field input::placeholder { color: var(--ink-2); }.field input:disabled { color: var(--ink-2); }.picker-acts { display: flex; align-items: center; gap: 10px; margin-top: 11px; flex-wrap: wrap; }/* A button never gives up its own width to the sentence beside it. */.picker-acts .btn { flex: none; }.picker-note { font-size: 12px; color: var(--ink-2); }.picker-error { margin: 12px 0 0; font-size: 12.5px; line-height: 1.5; color: var(--bad); background: var(--bad-wash); border-radius: 7px; padding: 8px 10px;}.picker-sec { margin: 26px 0 6px; font-size: 11px; font-weight: 700; color: var(--ink-2); }.picker-list { list-style: none; margin: 0; padding: 0; }.picker-list li { display: flex; align-items: center; gap: 4px; }/* A space in a list: the mark, its name, and one line about it. A flex row rather than two stacked lines, because with sign-in leading the screen this list is the content and has to read as a list of objects — not as text that happens to be clickable. */.picker-item { flex: 1; min-width: 0; display: flex; align-items: center; gap: 10px; text-align: left; border: 0; background: none; border-radius: 8px; padding: 8px 9px; cursor: pointer; color: var(--ink);}.picker-item:hover { background: var(--hover); }.picker-item .mark { width: 17px; height: 17px; flex: none; color: var(--accent); }.picker-item .txt { min-width: 0; display: grid; gap: 2px; }.picker-name { font-size: 13.5px; font-weight: 500; }.picker-uri { font: 11.5px var(--mono); color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }/* A space's own description is prose the human wrote, so it is never mono — that is reserved for the URI it falls back to. */.picker-desc { font-size: 12.5px; color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.picker-foot { margin-top: 20px; display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }/* Signed in, on the picker. The pane bar's chip does this over an open space; here there is no pane bar yet, so who-you-are is a row rather than a popover — and the way out sits on it. */.picker-who { margin-top: 20px; padding: 10px 12px; border-radius: 9px; background: var(--sunk); display: flex; align-items: center; gap: 10px;}.picker-who .nm { font-size: 13.5px; font-weight: 600; }.picker-who .btn { margin-left: auto; flex: none; padding: 5px 11px; font-size: 12.5px; }
.scrim { position: fixed; inset: 0; z-index: 50; display: grid; place-items: center; padding: 24px; background: oklch(0.2 0.03 262 / 0.34); overflow-y: auto;}/* Two cards on the one scrim: the space picker, and the goal composer. The composer is here rather than in a page because the ⊕ offers a goal on every view of a space — and because a card injected into a goal list shoves every row under it down as it opens, which was the visible bug. Off the flow, nothing behind it moves. */.scrim .picker, .scrim .newgoal { width: min(560px, 100%); max-width: none; background: var(--paper); border: 1px solid var(--line); border-radius: 13px; padding: 24px 26px 26px; box-shadow: 0 2px 6px oklch(0.25 0.03 262 / 0.10), 0 24px 60px -14px oklch(0.25 0.03 262 / 0.4);}
/* ─── keyboard help ──────────────────────────────────────────────────────── *//* The map, drawn (`keys.ts` holds the table it draws). It is the same paper card on the same scrim as the picker, one layer above it, because it is opened over whatever is already up — including the picker. The keycaps are sans, not mono: `Esc` and `Home` are labels printed on a key, and mono in this system means a machine identifier. */.keys-scrim { z-index: 60; }.keys-card { width: min(520px, 100%); background: var(--paper); border: 1px solid var(--line); border-radius: 13px; padding: 20px 24px 24px; box-shadow: 0 2px 6px oklch(0.25 0.03 262 / 0.10), 0 24px 60px -14px oklch(0.25 0.03 262 / 0.4); text-align: left;}.keys-head { display: flex; align-items: center; gap: 12px; }.keys-head h2 { margin: 0; font-size: 17px; font-weight: 600; letter-spacing: -0.012em; }.keys-head .btn { margin-left: auto; flex: none; padding: 5px 11px; font-size: 12.5px; }.keys-group { margin: 20px 0 0; padding-bottom: 5px; border-bottom: 1px solid var(--line-soft); font-size: 12px; font-weight: 700; color: var(--ink-2);}.keys-when { margin: 7px 0 0; font-size: 12px; line-height: 1.5; color: var(--ink-2); }.keys-list { margin: 11px 0 0; display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 9px 14px; align-items: baseline;}.keys-list dt { display: flex; align-items: center; gap: 5px; }.keys-list dd { margin: 0; font-size: 13px; line-height: 1.5; color: var(--ink-2); }.keys-list kbd { font: 500 12px/1 var(--sans); color: var(--ink); white-space: nowrap; background: var(--sunk); border: 1px solid var(--line); border-bottom-color: var(--stroke); border-radius: 5px; padding: 4px 6px;}.keys-or { font-size: 11px; color: var(--ink-2); }
/* ─── skeleton ───────────────────────────────────────────────────────────── *//* The shape of a space, while it is being read. Every bar is the same neutral the app already uses for a sunk surface — a skeleton that shimmers in the accent would be the loading state advertising itself, and this is meant to be over before it is noticed. */.skeleton .rail-top, .skeleton .nav-item { pointer-events: none; }.skel { display: block; background: var(--sunk); border-radius: 5px; height: 11px; flex: none; animation: shimmer 1.7s ease-in-out infinite;}.skel.disc-skel { width: 22px; height: 22px; border-radius: 6px; flex: none; }.skel.glyph-skel { width: 17px; height: 17px; border-radius: 5px; flex: none; }.skel.dot-skel { width: 18px; height: 18px; border-radius: 999px; flex: none; }.skel-row { display: flex; align-items: center; gap: 11px; padding: 13px 12px; border-top: 1px solid var(--line-soft);}.skel-row:first-of-type { border-top: 0; }@keyframes shimmer { 50% { opacity: 0.42; } }
/* ─── error surfaces ─────────────────────────────────────────────────────── *//* A view that threw, and the space's own diagnostics. Both are quiet: one is rare, the other is almost always empty, and neither is news. */.broke { max-width: 62ch; padding: 26px 0; }.broke h2 { margin: 0 0 8px; font-size: 16px; font-weight: 600; }.broke p { margin: 0 0 10px; font-size: 13.5px; line-height: 1.55; color: var(--ink-2); }.broke pre { margin: 0 0 14px; padding: 9px 11px; background: var(--sunk); border-radius: 7px; font: 12px/1.5 var(--mono); color: var(--ink-2); white-space: pre-wrap; overflow-wrap: anywhere;}
.diag { margin: 34px 0 0; border-top: 1px solid var(--line-soft); padding-top: 10px; }/* The same disclosure tucked under the list it belongs to, rather than standing at the foot of a pane: no rule above it and the spacing of a list that continues. Used for the retired devices in a private space, which are a quieter part of the section above them and not a footer of their own. The chrome and the quietness are unchanged — this only removes a separator that would have read as the end of the section. */.diag.tucked { margin: 6px 0 0; border-top: 0; padding-top: 0; }/* The foot of the pane, where more than one of these disclosures stands together: the space's diagnostics (absent whenever nothing is wrong) and the build stamp (never absent). The separator and the gap above belong to the group, so the stamp does not start a second footer under the diagnostics — and the group still looks like the footer it always was when it holds only one. */.foot { margin: 34px 0 0; border-top: 1px solid var(--line-soft); padding-top: 10px; }.foot .diag { margin: 0; border-top: 0; padding-top: 0; }.diag > summary .n { font-weight: 500; font-variant-numeric: tabular-nums; }.diag > summary { display: flex; align-items: center; gap: 8px; cursor: pointer; list-style: none; font-size: 11.5px; color: var(--ink-2); padding: 4px 0;}.diag > summary::-webkit-details-marker { display: none; }.diag > summary::before { content: "›"; font-size: 13px; line-height: 1; color: var(--stroke); transition: transform 180ms var(--ease-out);}.diag[open] > summary::before { transform: rotate(90deg); }.diag > summary:hover { color: var(--ink); }.diag-body { padding: 6px 0 4px 14px; max-width: 74ch; }.diag-group { margin: 0 0 12px; }.diag-reason { font-size: 12.5px; color: var(--ink); margin: 0 0 4px; }.diag-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 3px; }.diag-list li { font: 11.5px/1.45 var(--mono); color: var(--ink-2); overflow-wrap: anywhere; }/* A line that is a sentence about an identifier rather than one identifier. */.diag-list.mixed li { font: 12.5px/1.6 var(--sans); }.diag code.inl { font-size: 11.5px; background: none; padding: 0; }.diag-note { font-size: 12.5px; line-height: 1.5; color: var(--ink-2); margin: 0 0 12px; max-width: 66ch; }
/* ─── private mode ───────────────────────────────────────────────────────── *//* The disclosure (`private-mode.ts`), the device rows, and what a replica holds. Deliberately quiet: none of it is an alarm, and the one thing on the page that IS irreversible — revoking — says so in its own words rather than in red. */.disclose { list-style: none; margin: 10px 0 12px; padding: 0; display: grid; gap: 8px; max-width: 70ch; }.disclose li { font-size: 12.5px; line-height: 1.5; color: var(--ink-2); }.disclose li strong { color: var(--ink); font-weight: 600; }.disclose-ack { display: flex; align-items: flex-start; gap: 8px; margin: 0 0 12px; max-width: 70ch; }.disclose-ack input { margin: 2px 0 0; flex: none; }.disclose-ack span { font-size: 12.5px; line-height: 1.5; color: var(--ink); }/* One replica's counts. A definition list would be semantically right and would wrap badly; these are pairs of a word and a number, so they read as one line that folds. */.replica { display: flex; flex-wrap: wrap; gap: 4px 16px; margin: 8px 0 0; padding: 0; list-style: none; }.replica li { font-size: 12.5px; color: var(--ink-2); }.replica li b { font-weight: 500; color: var(--ink); font-variant-numeric: tabular-nums; }/* A space in the picker that is private: the badge takes the tail of the row rather than trailing the name, so the list reads down its left edge the way it did before there were two kinds of space. */.picker-item .badge { margin-left: auto; flex: none; }/* The sentence a confirmation owes the row above it. In the list rather than beside the button because what it says is about the record, and it must not move the row it is explaining. */.arow-note { padding: 0 0 10px; }.arow-note .grouphint { margin: 0; }
/* ─── empty ──────────────────────────────────────────────────────────────── */.empty { padding: 22px 0 8px; max-width: 52ch; }.empty p { margin: 0; font-size: 13.5px; line-height: 1.55; color: var(--ink-2); }.empty p + p { margin-top: 8px; }
/* The confirmation, lifted clear of whatever is covering the bottom of the pane — a soft keyboard, usually, which the layout viewport this is positioned in knows nothing about. `Toast.svelte` measures the gap and sets `--toast-lift`; the 26px here and `TOAST_STRIP` in `ui.svelte.ts` are the same strip seen from two sides, so change one and change the other. */.toast { position: absolute; left: 50%; bottom: calc(26px + var(--toast-lift, 0px)); transform: translateX(-50%) translateY(0); background: var(--ink); color: var(--paper); font-size: 13px; font-weight: 500; padding: 8px 14px; border-radius: 999px; z-index: 40; pointer-events: none; box-shadow: 0 8px 24px -8px oklch(0.2 0.03 262 / 0.5); transition: opacity 220ms ease, transform 260ms var(--ease-out);}.toast[hidden] { display: block; opacity: 0; transform: translateX(-50%) translateY(10px); }
.railtoggle { display: none; }.rail-scrim { display: none; }
/* ─── responsive ─────────────────────────────────────────────────────────── */@media (max-width: 860px) { /* A 1220px window on a phone is a picture of a desktop app, so `?frame` gives up here too. */ body, :root[data-frame] body { padding: 0; place-items: stretch; } .app, :root[data-frame] .app { width: 100%; height: 100dvh; border-radius: 0; border: 0; box-shadow: none; grid-template-columns: minmax(0, 1fr); } /* The picker gives its card up too — at this width a card and the screen are the same rectangle. */ .app.solo { width: 100%; margin: 0; border: 0; border-radius: 0; box-shadow: none; } .rail { position: absolute; inset: 0; z-index: 30; width: 288px; border-right: 1px solid var(--line); transform: translateX(-100%); transition: transform 300ms var(--ease-out); box-shadow: 0 0 60px oklch(0.2 0.03 262 / 0.25); } /* A drawer over the pane is not a column beside it, so there is no edge between two things to drag: the rail takes its own width here and the grip goes away rather than resizing an overlay. */ .rail-resize { display: none; } .app.rail-open .rail { transform: none; } .rail-scrim { display: block; position: absolute; inset: 0; z-index: 29; border: 0; padding: 0; background: oklch(0.2 0.03 262 / 0.28); } .railtoggle { display: grid; } .scroll { padding: 0 16px 120px; } .pane-bar { padding: 10px 14px; } .find { width: 128px; } .page-title h1 { font-size: 23px; } .drawer-pad { padding-left: 12px; } .unit { margin: 0 -8px; } .goal-row { margin: 0 -8px; } .plus { right: 18px; bottom: 18px; } .menu { right: 18px; bottom: 74px; width: min(306px, calc(100vw - 36px)); }}
/* iOS Safari zooms the page when a focused control computes under 16px, and there is no way to decline the zoom that does not also take pinch zoom away from Android (`maximum-scale=1` on the viewport meta — a WCAG 1.4.4 failure, and the reason this is done in type rather than in the meta). So on a touch-primary device — keyed on the input, not on the viewport, because an iPad in landscape is wider than 860px and zooms just the same — every typed control meets the floor.
Raising the tokens rather than naming selectors is the whole design: a rule here can be out-ranked by a longer selector somewhere above it, and was — the mono identifier fields sat at 12.5px under a selector list that claimed to cover them. A value cannot be out-ranked. `test/touch-floor.test.mjs` holds the other end of it: no typed control anywhere may state a size that is not one of these. */@media (hover: none) and (pointer: coarse) { :root { --field-size: 16px; --field-ident-size: 16px; --field-find-size: 16px; }}@media (max-width: 560px) { /* A phone is an editing surface even when its browser reports an attached fine pointer (or a test harness does not emulate pointer capabilities). Keep every caret at the same 16px floor the touch-primary query establishes above: this prevents Safari focus zoom and makes the compact desktop field sizes stop leaking into phone layouts. */ :root { --field-size: 16px; --field-ident-size: 16px; --field-find-size: 16px; } /* The switcher can be much taller than the viewport once an account has several spaces. On a phone it is the screen, not a desktop card floating in the middle of one: pin it to the top of the scrolling scrim, remove the doubled outer inset, and keep the content clear of notches and the home indicator. `min-height` also leaves short states feeling intentional without trapping tall states — the scrim remains the one scroll container. */ .scrim:has(.picker) { place-items: start stretch; padding: 0; } .scrim .picker { width: 100%; min-height: 100dvh; border: 0; border-radius: 0; box-shadow: none; padding: max(22px, env(safe-area-inset-top)) max(18px, env(safe-area-inset-right)) max(24px, env(safe-area-inset-bottom)) max(18px, env(safe-area-inset-left)); } .app.solo .picker { padding: max(24px, env(safe-area-inset-top)) max(18px, env(safe-area-inset-right)) max(24px, env(safe-area-inset-bottom)) max(18px, env(safe-area-inset-left)); } .picker-head { gap: 10px; } .picker-who { align-items: flex-start; } .picker-who .btn { padding-inline: 9px; } .picker-item { padding-inline: 6px; } .picker-item .badge { font-size: 11px; padding-inline: 6px; } /* A row stops being a flex line and becomes a paragraph: the name wraps as text and the tail floats into it, so metadata takes whatever the name's last line left over and only drops to a line of its own when there is none. Nothing is shed for the width — a name that would have been ellipsised wraps instead — and the common row (a name and a date) still costs one line.
The state glyph leaves the flow entirely and is positioned into a reserved gutter. Floated it would only hold the first line, and wrapped text would run back under it. */ .goal-row { display: flow-root; position: relative; padding-left: 38px; } .goal-row > .pie, .goal-row > .dot { position: absolute; left: 10px; top: 9px; } .goal-row .gtitle { display: inline; float: none; white-space: normal; overflow: visible; text-overflow: clip; } /* A float wider than the text beside it leaves a gap no word fits into, and the name gets pushed under its own metadata. The cap keeps a readable measure on the name's first line; the tail wraps within it instead of taking the line. */ .goal-row .tail { float: right; margin: 1px 0 0 10px; max-width: 58%; flex-wrap: wrap; justify-content: flex-end; } .row { display: flow-root; padding-left: 41px; } /* Whatever the row leads with goes into the gutter, and a row leads with exactly one thing: a unit leads with its state, a piece of foreign feedback with the stranger who wrote it, a GitHub issue with the neutral mark that stands in for a filer this app cannot identify. All three are drawn in an 18px box, so all three take the same offsets — and `.disc` is `display: grid`, so a selector that named only `.dot` would leave the disc a block of its own and cost the row the extra line this whole block exists to save. */ .row > .dot, .row > .disc, .row > .mark { position: absolute; left: 12px; top: 10px; } /* The flex gap goes with the flex line; inline children state their own separation. */ .row .name, .row .vchip { margin-right: 6px; } .row .sub { display: inline; float: none; white-space: normal; overflow: visible; text-overflow: clip; } .row .tail { float: right; margin: 1px 0 0 10px; max-width: 58%; flex-wrap: wrap; justify-content: flex-end; } .crumb .full { display: none; } .chip-mock { display: none; } /* Freshness keeps the dot and sheds the sentence; the tap that a hover cannot happen on opens the rest. `.full` means the same here as it does on the crumb — the long form, shed when narrow. */ .chip-sync.full { display: none; } .sync { display: flex; } /* Quick find is a search icon until it is asked for, and then it takes the bar — the crumb and the freshness dot are what a box being typed into can afford to push aside, and both are a tap from coming back. The box is never `display: none`: `/` and the icon beside it both open it by putting focus in it, and a field that is not rendered cannot take focus. */ .icon-btn.find-btn { display: grid; } .find { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); } .find.open { position: static; width: auto; height: auto; margin: 0; padding: 4px 9px; overflow: visible; clip-path: none; flex: 1; } .pane-bar.searching .crumb, .pane-bar.searching .chip-mock, .pane-bar.searching .bar-spacer, .pane-bar.searching .sync, .pane-bar.searching .icon-btn.find-btn { display: none; } /* The sentence is hidden from the eye, not from the accessible name: `display: none` would take it out of the tree too, and the narrow form beside it is aria-hidden — a badge that shed both would say nothing at all to a screen reader, which is the one place a row cannot afford to be terse. */ .badge .wide { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } .badge .narrow { display: inline; } /* A bare badge has nothing beside its glyph, so the padding that was making room for a word on the right goes symmetrical and the pill closes up around it. */ .badge.bare { padding-inline: 6px; } /* Admin metadata may wrap rather than squeezing a handle or description into an ellipsis. The basis leaves useful text on line one and lets a larger action tail fall below as a group. */ .arow { flex-wrap: wrap; align-items: flex-start; } .arow .meta { flex-basis: 18ch; white-space: normal; text-overflow: clip; overflow-wrap: anywhere; } .arow .tail { margin-left: auto; flex-wrap: wrap; justify-content: flex-end; } .page-meta { gap: 3px 10px; font-size: 12.5px; } /* Signed in, the disc already says who you are, and the bar has quick find to fit. Signed out there is no disc, so the word stays — it is the only thing that would say what the button does. */ .acct-btn.with-disc .who-name { display: none; } .acct-pop { width: min(292px, calc(100vw - 32px)); } /* A viewport this narrow is a phone, and a phone has no keys to map. `?` still opens it for a keyboard that happens to be attached; what goes is the button advertising it. */ .keys-btn { display: none; }}
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } .st-claimed .ring { animation: none; }}