@layer theme { /* ---------- panels / cards (cbi) ---------- */ .cbi-section, .cbi-map > .cbi-section-node, .panel { background: var(--fs-panel); border: var(--fs-hairline); border-radius: var(--fs-radius-lg); } .cbi-section { padding: var(--fs-card-pad); margin-bottom: var(--fs-card-gap); } /* bare only; ink from the heading ramp. No line-height — base already sets 1. */ legend { font-size: var(--fs-type-lg); font-weight: var(--fs-weight-bold); color: var(--fs-text); } /* overview (luci-mod-status) DOM: * .cbi-section > .cbi-title>h3 + div > table.table (.tr/.td) */ /* section title + collapse control */ .cbi-section > .cbi-title { margin-bottom: var(--fs-space-3); } .cbi-section > .cbi-title h3, .cbi-section > h3:first-child, .cbi-section > legend { margin: 0; padding: 0; border: 0; } /* bare title (no wrapper) sat flush below — same 12px; excludes .cbi-title h3, already gapped */ .cbi-section > h3:first-child, .cbi-section > legend { margin-bottom: var(--fs-space-3); } /* a heading FOLLOWING a table had no air; separates more from what precedes than introduces */ .cbi-section .table + h3 { margin-top: var(--fs-space-5); } .cbi-section h3 + .table { margin-top: var(--fs-space-3); } /* [id]/.fs-dt = data table; no id = key/value, no frame/clip; nested tables too */ .cbi-section .table:not([id]):not(.fs-dt), .cbi-section-node .table:not([id]):not(.fs-dt), .cbi-modal .table, .table .table, .cbi-value .table { border: 0; border-radius: 0; overflow: visible; background: none; } .cbi-section .table { margin: 0; width: 100%; } /* moved from base with the generic field box (theme/60-inputs.css) */ .table.cbi-section-table input[type="password"], .table.cbi-section-table input[type="text"], .table.cbi-section-table textarea, .table.cbi-section-table select, .table.cbi-section-table .cbi-select { width: 100%; min-width: auto; } .table.cbi-section-table .td.cbi-section-table-cell select, .table.cbi-section-table .td.cbi-section-table-cell .cbi-select { width: inherit; } /* filter placeholder sliced mid-glyph on a long title (issue #12); ellipsis changes the * failure instead. No !important — the inline 30px flag width still stands. */ .table.cbi-section-table .cbi-section-filter { padding: var(--fs-space-1) var(--fs-space-1-5); font-size: var(--fs-type-field-xs); text-overflow: ellipsis; } /* width:100% cannot shrink below min-content; the inverse rule further down restores the * floor on a data table — read that block before touching either. */ .cbi-section .table .td, .cbi-section .table .th { padding-block: var(--fs-space-2-5); padding-inline: 0 var(--fs-space-4); border: 0; border-bottom: var(--fs-hairline); line-height: 1.45; vertical-align: middle; text-align: start; } /* hugs the far edge, beating text-align:start above (form.TableSection's own action cells; * hand-built ones stay left) */ .cbi-section .table .td.cbi-section-table-cell.cbi-section-actions { text-align: end; } .cbi-section .table .tr:last-child .td { border-bottom: 0; } /* case is the app's: uppercase broke LuCI's own casing (issue #39); weight/size/ink alone * still read as a heading. */ .cbi-section .table .th { color: var(--fs-eyebrow-color); font-weight: var(--fs-eyebrow-weight); font-size: var(--fs-type-xs); letter-spacing: var(--fs-eyebrow-tracking); } /* --- DATA tables ([id] or JS-tagged .fs-dt) --- `:is()` at ONE specificity: split, a table * known only by the tag (Status -> Routing) lost to key/value's default. The padding, frame, corner * and placeholder rules key on `:is(table, div).fs-dt` (0,1,1) as v0.14.14's `table.fs-dt`, plus * the bare div: `.table.fs-dt .td` (0,3,0) beat an app's `.table td:nth-child(1)` (0,2,1), * filemanager padding-left 5 -> 12px (docs/css.md). The rules that were `.table.fs-dt` stay so. */ .cbi-section .table:is([id], .fs-dt) .td, .cbi-section .table:is([id], .fs-dt) .th, :is(table, div).fs-dt .td, :is(table, div).fs-dt .th { padding: var(--fs-space-2-5) var(--fs-space-3); } .cbi-section .table:is([id], .fs-dt) .td { font-family: var(--fs-font-mono); font-size: var(--fs-type); color: var(--fs-text); } /* the min-content FLOOR belongs here: break-word (unlike base's -anywhere) does not lower * min-content, so an overflow stays visible to fit.overflows(). :not(.fs-stacked) is the * fitter's own measuring state. */ .table.fs-dt:not(.fs-stacked) .td { overflow-wrap: break-word; } /* the ONE column the fitter allows to shred rather than card the table (fs-select.js); * (0,5,0) beats the floor above on specificity, not source order */ .table.fs-dt:not(.fs-stacked) .td.fs-td-break { overflow-wrap: anywhere; } /* processes.js's inline style= cannot be outranked by a layer — !important is the sanctioned * fight against it. A CARD IS NOT EXEMPT: an unlayered app rule breaks a token that would fit. */ .table.fs-dt .td, .table.fs-dt .td :not(button):not(.btn):not(input):not(select):not(textarea) { word-break: normal !important; } /* Whether a table cards, breaks one column or scrolls is MEASURED (fs-select.js), at any width. */ /* sticks each .th, not the row — a row's fill does not travel with a sticky cell here. 0 in * the sidebar (.fs-main scrolls itself), --fs-bar-live where the document does. */ :where(:root[data-layout="sidebar"]:not([data-narrow]) #view, #modal_overlay) .table.fs-dt:not(.fs-stacked) .tr.table-titles .th { position: sticky; top: 0; z-index: 1; background: var(--fs-panel2); } :where(:root[data-layout="top"] #view, :root[data-narrow] #view) .table.fs-dt:not(.fs-stacked) .tr.table-titles .th { position: sticky; top: var(--fs-bar-live, var(--fs-bar-h)); z-index: 1; background: var(--fs-panel2); } /* fitTables() forces a layout with marks off, and anchoring can re-pick a node inside it * (18 jumps/1500 frames measured). */ .table.fs-dt { overflow-anchor: none; } .cbi-section .table:is([id], .fs-dt) .tr:not(.table-titles):not(.placeholder):hover .td { background: var(--fs-panel2); } .cbi-section .table:is([id], .fs-dt) .tr.table-titles .th { border-bottom-color: var(--fs-border); } /* contour (border-collapse:separate, or the radius is silently dropped); #view too — * Startup renders .table.fs-dt with no section */ .cbi-section .table[id], #view .table[id], .cbi-section .table.fs-dt, .cbi-section table.fs-dt, :is(table, div).fs-dt { border-collapse: separate; border-spacing: 0; border: var(--fs-hairline); border-radius: var(--fs-radius-lg); /* EXPLICIT: the generic .table rule below still says overflow:hidden, clipped otherwise */ overflow: visible; } /* frame is overflow:visible, so nothing clips the corner cells — round the four OUTER ones. * `:is(.tr.table-titles, .thead)` covers both header shapes; without .thead a real * kept square top corners. */ .cbi-section .table[id] :is(.tr.table-titles, .thead) .th:first-child, #view .table[id] :is(.tr.table-titles, .thead) .th:first-child, :is(table, div).fs-dt :is(.tr.table-titles, .thead) .th:first-child { border-start-start-radius: var(--fs-radius-lg); } .cbi-section .table[id] :is(.tr.table-titles, .thead) .th:last-child, #view .table[id] :is(.tr.table-titles, .thead) .th:last-child, :is(table, div).fs-dt :is(.tr.table-titles, .thead) .th:last-child { border-start-end-radius: var(--fs-radius-lg); } .cbi-section .table[id] .tr:last-child .td:first-child, #view .table[id] .tr:last-child .td:first-child, :is(table, div).fs-dt .tr:last-child .td:first-child { border-end-start-radius: var(--fs-radius-lg); } .cbi-section .table[id] .tr:last-child .td:last-child, #view .table[id] .tr:last-child .td:last-child, :is(table, div).fs-dt .tr:last-child .td:last-child { border-end-end-radius: var(--fs-radius-lg); } /* empty table: L.ui.Table adds .tr.placeholder; spanning it is base's job, framed flavour left here */ .cbi-section .table[id] .tr.placeholder .td, #view .table[id] .tr.placeholder .td, :is(table, div).fs-dt .tr.placeholder .td { padding-block: var(--fs-space-2); /* this layer's own cell padding made the plate 4px tall */ text-align: start; color: var(--fs-dim); font-family: var(--fs-font-sans); border-radius: 0 0 var(--fs-radius-lg) var(--fs-radius-lg); } /* --- KEY/VALUE: "no id" is not enough — GridSection also renders no-id .cbi-section-table, * excluded along with .fs-dt and the legend's inline table-layout. Shape, not a class, says * data table: any .th or a 3rd .td rules a table out — a real key/value row is exactly 2 .td * and no .th. docs/css.md, "The key/value discriminator". */ .cbi-section .table:not([id]):not(.cbi-section-table):not(.fs-dt):not(:has(.th)):not(:has(.td:nth-child(3))):not([style*="table-layout"]) .td:first-child { color: var(--fs-dim); font-weight: var(--fs-weight); width: 40%; white-space: nowrap; } .cbi-section .table:not([id]):not(.cbi-section-table):not(.fs-dt):not(:has(.th)):not(:has(.td:nth-child(3))):not([style*="table-layout"]) .td:last-child:not(:first-child) { font-family: var(--fs-font-mono); color: var(--fs-text); text-align: end; } /* below ~560px the 40%/nowrap column starves the value — stacked below (560, not the config * table's 960px). :first-child:first-child doubles the pseudo-class ON PURPOSE, to outrank. */ @container fs-content (max-width: 560px) { /* the row's own .td/.th padding above spent a FULL padding per half; moved to the ROW */ .cbi-section .table:not([id]):not(.cbi-section-table):not(.fs-dt):not(:has(.th)):not(:has(.td:nth-child(3))):not([style*="table-layout"]) .tr { display: block; padding: var(--fs-space-1) 0 var(--fs-space-1-5); border: 0; border-bottom: var(--fs-hairline); } .cbi-section .table:not([id]):not(.cbi-section-table):not(.fs-dt):not(:has(.th)):not(:has(.td:nth-child(3))):not([style*="table-layout"]) .tr:last-child { border-bottom: 0; } .cbi-section .table:not([id]):not(.cbi-section-table):not(.fs-dt):not(:has(.th)):not(:has(.td:nth-child(3))):not([style*="table-layout"]) .td:first-child:first-child { display: block; width: auto; white-space: normal; padding: 0; border: 0; font-size: var(--fs-type-xs); font-weight: var(--fs-eyebrow-weight); letter-spacing: var(--fs-eyebrow-tracking); color: var(--fs-eyebrow-color); text-transform: uppercase; } .cbi-section .table:not([id]):not(.cbi-section-table):not(.fs-dt):not(:has(.th)):not(:has(.td:nth-child(3))):not([style*="table-layout"]) .td:last-child:last-child:not(:first-child) { display: block; width: auto; text-align: start; padding: 0; border: 0; } } /* --- the realtime graph legend: the one no-id .table with inline table-layout:fixed, six * equal columns overflowing on a phone — folded into label/value pairs. */ @media (max-width: 767px) { .cbi-section .table:not([id]):not(.cbi-section-table):not(.fs-dt)[style*="table-layout"] .tr { display: flex; flex-wrap: wrap; } .cbi-section .table:not([id]):not(.cbi-section-table):not(.fs-dt)[style*="table-layout"] .td { flex: 1 1 50%; min-width: 0; width: auto; } } /* remove double card when a section nests a table-only include */ .cbi-section .cbi-section { border: 0; box-shadow: none; padding: 0; margin: 0; background: transparent; } /* generic links/labels inside cards; the MAC badge keeps base's flat 4px, square against the card corners (issue #5.4) */ .cbi-section .ifacebox, .cbi-section .zonebadge, .network-status-table .ifacebox-body .ifacebadge { border-radius: var(--fs-radius); } .zonebadge input { width: 6em; } /* moved from base with the generic field box */ /* head rounds WITH the box (issue #7); max() since Rounding goes to 0, an invalid subtraction */ .cbi-section .ifacebox > .ifacebox-head { --fs-head-radius: max(0px, calc(var(--fs-radius) - 1px)); border-radius: var(--fs-head-radius) var(--fs-head-radius) 0 0; } /* inactive tab panes: base's height:0/overflow:hidden still drew empty padding/border bars * (issue #10). visibility not display:none — stock JS reads a graph's width once in a rAF. */ [data-tab-title]:not([data-tab-active="true"]) { visibility: hidden; position: relative; height: 0; overflow: hidden; padding: 0; margin: 0; border: 0; } /* 6%, not 12%: bold --fs-danger on a tint OF --fs-danger eats its own contrast (12% measured * 4.4:1); at 6% every palette clears 4.5 (4.87/4.95/5.28/6.09) */ .cbi-section-error { border-color: var(--fs-danger); background: color-mix(in srgb, var(--fs-danger) 6%, var(--fs-panel)); color: var(--fs-text); } .cbi-section-error ul li { color: var(--fs-danger); } /* denser data tables (dhcp static leases, section-tables) */ .cbi-section-table .th, .cbi-section-table .td, /* the GridSection name CELL on 24.10, a ::before from data-title where form.js has no real * cell — named here too or the column lines up with nothing */ .table.cbi-section-table .th, .table.cbi-section-table .td, .table.cbi-section-table .tr::before { padding-block: var(--fs-space-2); padding-inline: 0 var(--fs-space-3); } /* zero inline-start padding is right only INSIDE a card; a self-framed table glued its header text * to its own corner without this (issue #7) */ .table.cbi-section-table:not(.cbi-section *):not(.cbi-section-node *) .th, .table.cbi-section-table:not(.cbi-section *):not(.cbi-section-node *) .td { padding: var(--fs-space-2-5) var(--fs-space-3); } .cbi-section-table .tr.cbi-section-table-titles .th, .cbi-section-table .tr.table-titles .th { font-size: var(--fs-type-xs); letter-spacing: var(--fs-eyebrow-tracking); line-height: 1.25; } /* standalone data tables -> rounded card; inside a CBI section the section already IS the * card, so the frame is stripped there (above) */ .table { border: var(--fs-hairline); border-radius: var(--fs-radius-lg); overflow: hidden; background: var(--fs-panel); } /* separators on the CELLS: the frame needs border-collapse:separate, a row's own border * paints nothing. :not(:last-child) keeps a trailing one off the rounded corner. */ .table .tr.table-titles > .th, .table .tr.cbi-section-table-titles > .th, .table .tr.cbi-section-table-titles::before, .table .tr:not(.table-titles):not(.cbi-section-table-titles):not(:last-child) > .td, .table .tr.cbi-section-table-row:not(:last-child)::before { border-bottom: var(--fs-hairline); } /* per-CELL border, so a foreign `position: relative; top:` (dashboard) drags one cell's * border out of line — `top` AND `bottom` cancels either without touching `relative` itself */ :is(#view, #modal_overlay) .fs-dt .td { top: auto; bottom: auto; } /* descendant combinator so a real 's inserted is covered too */ .table .tr.table-titles, .table .tr.cbi-section-table-titles { background: transparent; /* drop the base panel2 header bar — match the dashboard's faint-label + underline look */ } .table .tr.table-titles .th, .table .tr.cbi-section-table-titles .th, .table .tr.cbi-section-table-titles::before { color: var(--fs-eyebrow-color); font-size: var(--fs-type-xs); font-weight: var(--fs-eyebrow-weight); letter-spacing: var(--fs-eyebrow-tracking); background: transparent; } /* row NAME reads the same on both releases (real cell / ::before) — a LABEL, sans face */ .table.cbi-section-table .td.cbi-section-table-titles, .table.cbi-section-table .tr.cbi-section-table-row::before { font-family: var(--fs-font-sans); font-weight: var(--fs-weight-bold); color: var(--fs-text); } .table .tr:not(.table-titles):not(.cbi-section-table-titles) { transition: background var(--fs-dur); } /* row hover ONLY on real data/config tables (an untagged plain .table lit its whole block, * issue #5.3; a placeholder row lit up too, issue #5.2/#15) */ .table.cbi-section-table .tr.cbi-section-table-row:not(.placeholder):hover { background: var(--fs-panel2); } /* .fs-dt: fs-select.js on a header-row #view .table that is NOT .cbi-section-table — * replacing a live :has() selector re-evaluated on every mutation. */ /* One card-stack copy, no @container: fs-select.js already measures OVERFLOW and sets * .fs-stacked. The config table keeps its own @container — its widgets bake in a width. */ /* a poll REPLACES a data table with a fresh, unmarked copy that briefly lays out full-width, * throwing the reader 612px out and back — waits OUTSIDE the layout until fs-select.js * answers, guarded on :root[data-fs-fit]. */ :root[data-fs-fit] #view .table.fs-dt:not(.fs-fitted), :root[data-fs-fit] #modal_overlay .table.fs-dt:not(.fs-fitted) { display: none; } .table.fs-stacked { display: block; width: 100%; } /* .fs-drop-xs is the same drop one rung earlier, on an overflowed table before carding */ .table.fs-stacked .td.hide-sm, .table.fs-stacked .th.hide-sm, .table.fs-stacked .td.hide-xs, .table.fs-stacked .th.hide-xs, .table.fs-drop-xs .td.hide-sm, .table.fs-drop-xs .th.hide-sm, .table.fs-drop-xs .td.hide-xs, .table.fs-drop-xs .th.hide-xs, .table.fs-stacked .tr.table-titles, .table.fs-stacked .thead, .table.fs-stacked .tr.cbi-section-table-titles, .table.fs-stacked .tr.cbi-section-table-descr { display: none; } /* stretch (default), not flex-start: a 2-line cell's border-bottom broke out of line with a * 1-line neighbour's (52px) */ /* a with no .tr IS the row. Two rules, chosen instead of :is(): a browser without :has() drops a whole list, * losing display:flex on every card. Keep both in step by hand. */ .table.fs-stacked .tr { display: flex; flex-wrap: wrap; } .table.fs-stacked tfoot:not(:has(> .tr)), .table.fs-stacked .tfoot:not(:has(> .tr)) { display: flex; flex-wrap: wrap; } .table.fs-stacked .td { display: block; min-width: 0; width: auto; padding-block: var(--fs-space-1-5); padding-inline: 0 var(--fs-space-2-5); border: 0; text-align: start; overflow-wrap: anywhere; white-space: normal; } /* the column label, lifted above its value. A copy sits in theme/65-dropdown.css: the config * table needs it under its own @container guard, which cannot share a block with a class. Keep * the two identical by hand. */ .table.fs-stacked .td[data-title]::before { content: attr(data-title); display: block; margin-bottom: var(--fs-space-1); font-size: var(--fs-type-xs); font-weight: var(--fs-eyebrow-weight); letter-spacing: var(--fs-eyebrow-tracking); color: var(--fs-eyebrow-color); } /* a footer cell needs [data-title] in its OWN selector to win the caption rule's (0,4,0) */ .table.fs-stacked .td.cbi-section-actions::before, .table.fs-stacked .td:not([data-title])::before, .table.fs-stacked .td[data-title=""]::before, .table.fs-stacked tfoot .td[data-title]::before, .table.fs-stacked .tfoot .td[data-title]::before { content: none; } /* row buttons are not a label/value pair — full width below the pairs. Copy in theme/65-dropdown.css. */ .table.fs-stacked .td.cbi-section-actions { display: flex; flex: 1 1 100%; justify-content: flex-start; flex-wrap: wrap; gap: var(--fs-space-2); padding-top: var(--fs-space-2); } /* LuCI groups buttons in ONE inner
, so this flex-wrap never fires and .fs-main clipped * the tail button. Copy in theme/65-dropdown.css. */ .table.fs-stacked .td.cbi-section-actions > :not(.cbi-dropdown) { flex: 1 1 auto; display: flex; flex-wrap: wrap; gap: var(--fs-space-2); } /* width: 0 — the label's nowrap min-content (288px for a Russian item at 360px) sizes the table-row-group * past the card (td right 442 vs 315); basis `content` still sizes the item to its label (it was 266px, a whole card, with `1 1 auto`). Copy in theme/65-dropdown.css. */ .table.fs-stacked .td.cbi-section-actions > .cbi-dropdown { flex: 0 1 content; width: 0; min-width: 0; } /* nothing in a half-width cell may force the row wider than the viewport — .fs-main clips, * it does not scroll to an over-wide child */ .table.fs-stacked .td > *, .table.fs-stacked .td .ifacebadge { min-width: 0; max-width: 100%; } /* a
is still a table-footer-group in a display:block table, drawing beside the cards * instead of under them; `:has(> .tr)` tells the two shapes upstream writes it in apart. */ .table.fs-stacked tfoot:has(> .tr), .table.fs-stacked .tfoot:has(> .tr) { display: block; } .table.fs-stacked tfoot .td:empty, .table.fs-stacked .tfoot .td:empty { display: none; } /* --- DATA table flavour (L.ui.Table, tagged .fs-dt) --- the frame stays overflow:visible so a * ComboButton's open list can leave the card; zebra and hover paint on the .tr, so the two outer * rows carry the frame's inner radius (its radius less the 1px border) instead of a clip. */ .table.fs-dt.fs-stacked :is(.tr:first-child, :is(.tr.table-titles, .thead) + .tr) { border-start-start-radius: calc(var(--fs-radius-lg) - 1px); border-start-end-radius: calc(var(--fs-radius-lg) - 1px); } .table.fs-dt.fs-stacked .tr:last-child { border-end-start-radius: calc(var(--fs-radius-lg) - 1px); border-end-end-radius: calc(var(--fs-radius-lg) - 1px); } /* :not(.placeholder) — the empty-state plate is out of flow; its row only reserves height, * and padding subtracted from that left text hanging below the frame (Wireless) */ .table.fs-dt.fs-stacked .tr:not(.placeholder) { padding: var(--fs-space-2-5) var(--fs-space-4) var(--fs-space-3); } /* half the row makes the card; col-N cells are excluded by the second :not(), not outranked. Third :not() loses on purpose to the meter below. */ .table.fs-stacked .td:not(.cbi-section-actions):not([class*="col-"]):not(:has(.cbi-progressbar)) { flex: 1 1 50% !important; } /* a first cell that is one token (fs-select.js): "/dev/mtdblock6" is 110px, the half-split left * 94-106px at 360-390 and `anywhere` cut it mid-word. fit-content of one token is the token, * capped at the row, so a 64-char hash still breaks inside the card; a phrase keeps its half. */ .table.fs-stacked .tr > .td.fs-td-token:first-child:not(.cbi-section-actions):not([class*="col-"]) { min-width: fit-content; } /* a meter takes the whole line — its own title prints over the cell's caption otherwise */ .table.fs-stacked .td:has(.cbi-progressbar) { flex: 1 1 100% !important; } /* column weights (1-10): a share of a CARDED row, unguarded — `flex` only applies once a * `.td` becomes a flex item, so one copy serves every tier and is inert elsewhere. */ .table .td.col-1, .table .th.col-1 { flex: 1 1 30px; } .table .td.col-2, .table .th.col-2 { flex: 2 2 60px; } .table .td.col-3, .table .th.col-3 { flex: 3 3 90px; } .table .td.col-4, .table .th.col-4 { flex: 4 4 120px; } .table .td.col-5, .table .th.col-5 { flex: 5 5 150px; } .table .td.col-6, .table .th.col-6 { flex: 6 6 180px; } .table .td.col-7, .table .th.col-7 { flex: 7 7 210px; } .table .td.col-8, .table .th.col-8 { flex: 8 8 240px; } .table .td.col-9, .table .th.col-9 { flex: 9 9 270px; } .table .td.col-10, .table .th.col-10 { flex: 10 10 300px; } /* general table cell padding — compact but readable (Startup/Processes/etc) */ .table .td { padding-top: var(--fs-space-2); padding-bottom: var(--fs-space-2); } /* last resort: no header row, never carded (no data-title to print). .fs-xscroll: same, * MEASURED to overflow; refused to a table holding a widget (clips a popup inside). */ #view table:not(.table):not(.cbi-section-table):not(.fs-dt), #modal_overlay table:not(.table):not(.cbi-section-table):not(.fs-dt), #view .table.fs-xscroll, #modal_overlay .table.fs-xscroll { display: block; max-width: 100%; overflow-x: auto; overscroll-behavior-x: contain; /* the gesture stops here instead of dragging the page */ scrollbar-width: thin; /* the scrollbar IS the affordance; a shadow trick shows through zebra rows */ } /* a header-less table holding a control stacks instead of scrolling — a scroll container * would clip the popup inside; nothing here creates one, so a dropdown still escapes. */ .table.fs-rowstack, .table.fs-rowstack .tr, .table.fs-rowstack .td, .table.fs-rowstack .th { display: block; width: auto; } /* the cells were table-cells a moment ago (side padding); as blocks they need the vertical * rhythm instead, or the controls sit flush against each other */ .table.fs-rowstack .td { padding-inline: 0; padding-block: var(--fs-space-1); } /* pins the row's NAME while scrolling — its own background (a row's fill does not travel * with a sticky cell) and a hairline so it reads as an edge, not text over text. */ #view .table.fs-xscroll .tr > :first-child, #modal_overlay .table.fs-xscroll .tr > :first-child, #view .table.fs-xscroll tr > :first-child, #modal_overlay .table.fs-xscroll tr > :first-child { position: sticky; inset-inline-start: 0; z-index: 2; background: var(--fs-panel); border-inline-end: var(--fs-hairline); } /* a keyboard user tabs to the scroll box (fs-select.js gives it tabindex+name) — outline, not * box-shadow, so forced-colors keeps it */ #view .table.fs-xscroll:focus-visible, #modal_overlay .table.fs-xscroll:focus-visible { outline: 2px solid var(--fs-accent); outline-offset: 2px; } }