diff --git a/CLAUDE.md b/CLAUDE.md index 3217670..c8aecdb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -295,7 +295,7 @@ That file is the point. The left panel's rules lived in `css/lpanel.css`, the ri `togglePanelPin(side)` is the same shape of fix: it was two copies differing in three string literals. `toggleLPanelPin`/`toggleRPanelPin` survive as one-line wrappers only because `bridge.js` and the `onclick=` attributes know those names. -**A tab is drawn by breaking the header's rule, not by painting over it.** The selected tab has rounded top corners, a hairline outline with no bottom edge, and the panel body's own `--bg2`; `.panel-tabs` carries `margin-bottom: -1px` so that fill lands *on* `.panel-header`'s `border-bottom` and covers exactly the pixel under the tab. The line separates chrome from content everywhere except under the tab you are reading, where the two are the same thing — which is the whole metaphor, and it is why the accent moved to the tab's *top* edge: under the tab is where that hairline is, and a second rule there would be drawing back what the shape just removed. +**A tab is a seam, drawn by the tab rather than by overlapping the header's rule.** The selected tab has rounded top corners, a hairline outline, and the panel body's own `--bg2`; the strip sits flush against `.panel-header`'s `border-bottom` and the active tab's own `border-bottom: 1px solid var(--bg2)` (with `margin-bottom: -1px`) covers that rule's pixel so the tab and the panel body meet with nothing between them. The line separates chrome from content everywhere except under the tab you are reading, where the two are the same thing — which is the whole metaphor, and it is why the accent rides the tab's *top* edge: under the tab is where the seam border is, and a second rule there would be drawing back what the shape just removed. Two things that shape depends on. **The header keeps `--bg2`** — see the note over `header` in [css/layout.css](css/layout.css): the app header and both panels are meant to read as one surface, and recessing this strip would cut it horizontally the way a hairline there would cut it vertically. So the contrast comes from the other side: **`.panel-tab:not(.active)` is recessed toward `--bg`**, which is the ground under everything and darker than `--bg2` in every theme, so it deepens whichever way a palette runs. Without that, an outline on a header and a body of identical paint is a curve drawn on flat colour. And **the border is `transparent` on every tab** rather than present only on the selected one, or the label shifts a pixel each way as selection moves. diff --git a/css/chrome-polish.css b/css/chrome-polish.css new file mode 100644 index 0000000..94e5f60 --- /dev/null +++ b/css/chrome-polish.css @@ -0,0 +1,79 @@ +/* Header and sidebar finishing pass. */ + +/* One subtle toolbar plane; hover stays on the target rather than lighting the + whole header as the pointer crosses it. There is deliberately no header + rule: the canvas well is the only boundary beneath the header. */ +.hdr-toolbar { + border: 1px solid transparent; + background: color-mix(in srgb, var(--surface) 62%, transparent); + box-shadow: inset 0 1px 0 color-mix(in srgb, var(--text) 4%, transparent); +} + +.hdr-toolbar:focus-within { + border-color: color-mix(in srgb, var(--accent) 34%, var(--border)); +} + +.hdr-machine { + box-shadow: inset 0 1px 0 color-mix(in srgb, var(--text) 4%, transparent); +} + +.hdr-machine:hover, +.hdr-machine:focus-within { + box-shadow: inset 0 1px 0 color-mix(in srgb, var(--text) 6%, transparent), + 0 0 0 2px color-mix(in srgb, var(--accent) 34%, transparent); +} + +/* The tab seam (flush strip, active tab covering the header rule with its own + border-bottom) lives in css/panels.css — one declaration for both panels, + so this finishing pass does not carry a second copy of the metaphor. */ + +.panel-tab:not(.active) { background: color-mix(in srgb, var(--bg) 46%, transparent); } +.panel-tab:hover:not(.active) { background: color-mix(in srgb, var(--surface) 86%, transparent); } + +/* Compact, reliably targetable section controls with a restrained focus cue. */ +.lp-section-header, +.rp-section-header { + min-height: 34px; + box-sizing: border-box; + position: relative; +} + +.lp-section-header:focus-visible, +.rp-section-header:focus-visible { + /* The accent bar is the focus cue; the ring is only an edge polish, and it + is mixed at accent 34% rather than var(--focus-ring) because that token + is near-transparent (alpha .08–.14) — invisible against --bg2, which is + the ground these headers sit on. */ + box-shadow: inset 3px 0 0 var(--accent), inset 0 0 0 1px color-mix(in srgb, var(--accent) 34%, var(--border)); +} + +.lp-section-header:hover, +.rp-section-header:hover { background: color-mix(in srgb, var(--surface) 78%, transparent); } + +.lpanel-content, +.rpanel-content { + scrollbar-gutter: stable; + scrollbar-color: var(--sb-thumb) transparent; +} + +.lpanel-content::-webkit-scrollbar, +.rpanel-content::-webkit-scrollbar { width: 8px; } + +.lpanel-content::-webkit-scrollbar-thumb, +.rpanel-content::-webkit-scrollbar-thumb { + background: var(--sb-thumb); + border: 2px solid var(--bg2); + border-radius: 999px; +} + +.lpanel-content::-webkit-scrollbar-thumb:hover, +.rpanel-content::-webkit-scrollbar-thumb:hover { background: var(--sb-thumb-hover); } + +@media (max-width: 900px) { + .hdr-toolbar { border-color: transparent; box-shadow: none; } + .panel-header { background: var(--bg2); } +} + +@media (prefers-reduced-motion: reduce) { + .hdr-machine, .hdr-toolbar, .panel-tab, .lp-section-header, .rp-section-header { transition: none; } +} diff --git a/css/layout.css b/css/layout.css index c98aba0..8d29c3e 100644 --- a/css/layout.css +++ b/css/layout.css @@ -29,6 +29,25 @@ header { flex-shrink: 0; } +/* ─── THE HEADER'S THREE COLUMNS ─── + The left cluster is at least as wide as the left panel and the right cluster + at least as wide as the right panel, so what is left between them -- the tab + strip -- lines up with the canvas well underneath it. + + That is a geometry fix, not a taste one. The workspace tabs open downward + into the well, and the well's top corners are rounded. Left to find its own + width, the left cluster could end up narrower than the panel beside it, and + then the strip began a few pixels to the *left* of the canvas -- so the + first tab's bottom flare, which curves outward and down, landed inside the + well's top-left corner, which curves inward and down. Two arcs of opposite + curvature meeting at a point: a cusp, reading as a 1/x. No amount of tuning + either curve fixes that, because the two are correct separately; what was + wrong was that they were ever in the same place. Aligned, the strip only + ever meets the flat part of the well's top edge. + + The width is read from --lpanel-width / --rpanel-width, which js/ui.js sets + on the root element in setPanelWidth(), so this follows a panel being + dragged wider with no measuring and no JS of its own. */ .hdr-left { display: flex; align-items: center; @@ -36,6 +55,15 @@ header { align-self: stretch; flex-shrink: 0; gap: 10px; + min-width: var(--lpanel-width, 256px); +} + +/* An unpinned or hidden panel leaves the flow -- the canvas runs out to the + window edge behind it -- so there is nothing for the cluster to span, and + reserving the width would only push the strip off-centre. */ +body:has(.lpanel.unpinned) .hdr-left, +body:has(.lpanel.hidden) .hdr-left { + min-width: 0; } .hdr-right { @@ -43,6 +71,17 @@ header { align-items: center; gap: 6px; flex-shrink: 0; + /* The same span on the other side, and the same expression panels.css uses + for the panel itself -- a bare --rpanel-width would over-reserve on a + narrow window, where the panel is capped at 42vw. The toolbar stays at the + trailing edge; what the min-width buys is the empty run to its left. */ + min-width: min(var(--rpanel-width, 288px), 42vw); + justify-content: flex-end; +} + +body:has(.rpanel.unpinned) .hdr-right, +body:has(.rpanel.hidden) .hdr-right { + min-width: 0; } /* Related actions sit tight together; `.hdr-sep` separates the clusters. */ @@ -53,19 +92,103 @@ header { flex-shrink: 0; } -#hdr-canvas-actions, -.hdr-history-actions, -.hdr-file-actions { +/* ─── THE MACHINE (model picker + build/edit, joined) ─── + The same shape as `.hdr-split`: one control, a wide half that says what the + machine is and a narrow half that acts on it, with the ground on the wrapper + so the pair reads as one thing rather than as two boxes standing next to + each other. The one difference is that the ground is here at rest — the + picker carries a resting fill of its own, and stripping that to match the + split button's hover-only treatment would make the header's one piece of + standing information look like a button that had not been noticed yet. + + So the border, the fill and the hover glow move up to the wrapper, and both + halves give up their box and keep their type. The picker's glow was a full + ring around itself; on a joined control that ringed half of it. */ +.hdr-machine { + display: flex; + align-items: center; + flex-shrink: 0; + /* Matches the toolbar beside it: a 30px control, 2px of wrapper padding and + a 1px border each side. The pill used to sit at 32px against a 36px + toolbar and a 36px tab strip — the same header reading as three sizes. */ + height: 36px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 6px; + transition: border-color var(--transition-fast), box-shadow var(--transition-fast); +} + +.hdr-machine:hover, +.hdr-machine:focus-within { + border-color: var(--accent); + box-shadow: 0 0 12px rgba(0, 0, 0, .2), 0 0 0 1px var(--accent-border); +} + +/* Inner radii are one less than the wrapper's, the border sitting between. */ +.hdr-machine .model-picker-btn { + height: 34px; + border: 0; + background: transparent; + border-radius: 5px 0 0 5px; +} + +.hdr-machine .model-picker-btn:hover { + background: var(--surface2); + border-color: transparent; + box-shadow: none; +} + +/* The seam is this half's own inline-start border, so it lands exactly on the + boundary rather than a pixel to one side of it, and it is present at rest: + these are two targets at all times, and the picker is wide enough that a + reader aiming for the button needs to see where it starts. */ +/* More specific than `.hbtn` below: otherwise its general 5px radius wins in + the cascade and turns this half of the joined control into its own pill. */ +.hdr-machine .hdr-machine-act { + width: 34px; + height: 34px; + padding: 0; + justify-content: center; + gap: 0; + border: 0; + border-inline-start: 1px solid var(--border); + border-radius: 0 5px 5px 0; + color: var(--text2); +} + +.hdr-machine-act:hover { + background: var(--surface2); + color: var(--accent); + border-color: transparent; + border-inline-start-color: var(--border); +} + +/* ─── THE TOOLBAR ─── + One ground with rules inside it, where there used to be three grounds with + rules *between* them. Two devices doing one job is what made the right third + of the header read as three unrelated widgets: a browser or an editor uses a + shared ground or a set of rules, and mostly neither. The ground says "these + are the app's controls"; the rules say where one kind ends and the next + begins. Nothing here clips, because `.hdr-more` anchors two absolutely + positioned popovers inside it. */ +.hdr-toolbar { + display: flex; + align-items: center; + gap: 1px; padding: 2px; - border-radius: 7px; + border-radius: 8px; background: color-mix(in srgb, var(--surface) 54%, transparent); + flex-shrink: 0; } -.canvas-danger-sep { - height: 16px; - margin: 0 3px; +/* Inside a ground the rule only has to be legible, not structural, so it is + shorter than the free-standing separator and sits on tighter margins. */ +.hdr-toolbar > .hdr-sep { + height: 18px; + margin: 0 4px; } + /* ─── LOGO ─── */ /* Centred, not baseline. The mark is taller than the wordmark and so sets this box's height; under baseline alignment the text — the only baseline item — @@ -130,45 +253,11 @@ header { padding-left: 2px; } -/* ─── TOOLS PICKER (auxiliary views) ─── - Algorithms / Grammar / Reference are not peers of Build, so they live behind a - single control rather than in a tab row. */ -.tools-picker { - position: relative; - flex-shrink: 0; -} - -.tools-btn .chevron { - width: 11px; - height: 11px; - fill: currentColor; - opacity: .65; - transition: transform var(--transition-fast); -} - -.tools-picker.open .tools-btn { - background: var(--surface2); - color: var(--text); - border-color: var(--border2); -} - -.tools-picker.open .chevron { - transform: rotate(180deg); -} - -/* Lit while an auxiliary view is open, so there is always a visible cue that - the canvas is underneath rather than gone. */ -.tools-btn.view-active { - background: var(--accent-soft); - color: var(--accent); - border-color: var(--accent-border); -} - -/* Anchored to their trigger rather than the viewport, so they override the +/* ─── HEADER MENU (More, and the Explore group inside it) ─── + Anchored to its trigger rather than the viewport, so it overrides the position:fixed that .ctx uses for cursor-placed context menus. The .ctx- - qualified selectors are needed because modals.css loads after this file and + qualified selector is needed because modals.css loads after this file and its `.ctx { display: none }` would otherwise win at equal specificity. */ -.ctx.tools-menu, .ctx.hdr-more-menu { position: absolute; top: calc(100% + 6px); @@ -178,16 +267,39 @@ header { z-index: 950; } -.ctx.tools-menu.open, .ctx.hdr-more-menu.open { display: block; } -.tools-menu .ctx-i.active { +.hdr-more-menu .ctx-i.active { color: var(--accent); background: var(--accent-soft); } +/* Lit while an auxiliary view is open, so there is always a visible cue that + the canvas is underneath rather than gone -- and, now that the views are + rows in this menu, a cue pointing at the control they came out of. */ +.hbtn.view-active { + background: var(--accent-soft); + color: var(--accent); + border-color: var(--accent-border); +} + +/* A group heading. `.ctx-i` is a row you can press; this is neither, so it + takes the app's small-caps instrumentation face rather than the sentence + face the rows use, and it is sized to sit under the menu's own padding + without opening a gap above the first row it labels. */ +.ctx-group-label { + padding: 6px 12px 3px; + font-family: var(--mono); + font-size: .58rem; + font-weight: 600; + letter-spacing: .12em; + text-transform: uppercase; + color: var(--text3); + user-select: none; +} + /* ─── HEADER OVERFLOW ─── */ .hdr-more { position: relative; @@ -238,6 +350,32 @@ header { border-color: var(--border2); } +/* Every other pressable surface in the app gets this exact ring — + `.lp-row-btn`, `.panel-tab`, `.modal-close`, the canvas tool rail — so a + keyboard user tabbing across the header met the one cluster that did not + opt in. `.hbtn` sets no `outline-color`, so the browser's default fell + through to `outline-color: auto`, which resolves to `currentColor`: the + button's own resting `--text3`, a tone chosen to look quiet, not to mark a + focus target. Against `--bg2` that measures under 3:1 — a focus ring is + exactly the thing contrast guidelines hold to that floor, and this was the + one place in the header where a Tab press produced something close to + invisible. */ +.hbtn:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 1px; +} + +/* The picker pill and the split button already answer "where is focus" at + the wrapper — `:focus-within` lights the whole ground, exactly as `:hover` + does two rules up. A second ring on the child inside that glow would be + two answers to one question, so the child's own outline stands down here + and only here. */ +.hdr-machine .hbtn:focus-visible, +.hdr-split .hbtn:focus-visible, +.model-picker-btn:focus-visible { + outline: none; +} + .hbtn.active { background: var(--surface2); color: var(--accent); @@ -260,12 +398,130 @@ header { gap: 0; } -.hbtn.warn:hover { +/* :focus-visible rides alongside :hover, the pairing `.sm-tool[aria-label^= + "Delete"]` already uses for the app's other irreversible action — a reader + who tabs to Clear Canvas instead of pointing at it is owed the same red + warning the mouse gets, not a neutral button that turns out to erase the + workspace. */ +.hbtn.warn:hover, +.hbtn.warn:focus-visible { background: var(--red-soft); color: var(--red); border-color: var(--red-border); } +/* ─── SPLIT BUTTON (an act, and its variants) ─── + Word's arrangement, and it is here for the reason Word has it: Save and + "Save as" are one verb with two endings, so the caret has to read as + belonging to the button beside it rather than as a second button that + happens to sit next to it. + + The ground belongs to the *split*, not to either half, and that is the whole + of what makes it read as one control. Written the other way — two `.hbtn`s + keeping their own hover borders with a hairline drawn between them — the + halves stay two boxes: every hover paints a full outline around one of them, + so the seam reads as that button's own edge with the other button parked + against it, which is what it looked like. + + So the halves give up their box and keep everything else `.hbtn` gives them + (type, sizing, icon metrics, transitions), and the seam is the caret's own + inline-start border, coloured in only while there is a ground for it to + divide. At rest there are no boxes for a line to sit between, and a stray + hairline between two flat icons is what that was. */ +.hdr-split { + display: flex; + align-items: center; + flex-shrink: 0; + border-radius: 6px; + transition: background var(--transition-fast), box-shadow var(--transition-fast); +} + +/* An inset shadow rather than a border: this sits in `.hdr-file-actions` + beside 30px icon buttons, and a real border would make the split 2px taller + than the one next to it. */ +.hdr-split:hover, +.hdr-split:focus-within, +.hdr-split.open { + background: var(--surface); + box-shadow: inset 0 0 0 1px var(--border2); +} + +.hdr-split > .hbtn, +.hdr-split > .hbtn:hover { + background: transparent; + border-color: transparent; +} + +.hdr-split > .hbtn:first-child { + border-start-start-radius: 6px; + border-end-start-radius: 6px; + border-start-end-radius: 0; + border-end-end-radius: 0; +} + +.hdr-split > .hbtn:last-child { + border-start-start-radius: 0; + border-end-start-radius: 0; + border-start-end-radius: 6px; + border-end-end-radius: 6px; +} + +/* Only the half under the pointer lifts, so which of the two a click will land + on is never in question. */ +.hdr-split > .hbtn:hover { + background: var(--surface2); + color: var(--text2); +} + +/* Both halves are drawn in, and the numbers here are about the *glyphs* rather + than about the boxes. A 30px icon-only button centres a 14px glyph, so it + carries 8px of air on each side; a caret box adds its own. Flush boxes were + therefore 13px of empty header between the floppy and the chevron — the two + sat correctly adjacent and read as two icons that happened to be near each + other, which is the thing this control exists not to look like. Pulled in, + the pair sits about as close as the model picker's label and chevron, which + is the header's own idiom for "this opens something". + + The caret is narrower than the act as well as narrower than an icon button: + it is a modifier on the verb to its left, not a peer of it, and a caret at + the same 30px would say otherwise. */ +.hdr-split > .hdr-split-main { + width: 26px; +} + +.hdr-split-caret { + width: 16px; + padding: 0; + justify-content: center; + gap: 0; +} + +.hdr-split-caret svg { + width: 9px; + height: 9px; + transition: transform var(--transition-fast); +} + +/* The seam. */ +.hdr-split:hover > .hdr-split-caret, +.hdr-split:focus-within > .hdr-split-caret, +.hdr-split.open > .hdr-split-caret { + border-inline-start-color: var(--border2); +} + +/* Held down while its tray is up, the way `.hdr-more.open > .hbtn` is. Its + chevron turns too, the same 180° the model picker's does a few pixels to + the left — the header's other disclosure control, opening the same way, so + a reader who has learned one has already learned the other. */ +.hdr-split.open > .hdr-split-caret { + background: var(--surface2); + color: var(--text); +} + +.hdr-split.open > .hdr-split-caret svg { + transform: rotate(180deg); +} + /* ─── CUSTOM TITLEBAR (Electron only) ─── The Electron window is frameless (electron/main.js), so the header doubles as the titlebar: most of it is a drag region, with every clickable cluster @@ -359,10 +615,6 @@ header { align-items: center; gap: 1px; background: transparent; - /* Inset rather than flush: at height:100% the strip filled the header's - 47px content box, putting its own 1px bottom border immediately above the - header's, and the two stacked into one thick-looking line. Centring it - leaves clear space so only the header's border reads as the edge. */ height: 36px; align-self: center; border: 1px solid var(--border); @@ -378,9 +630,6 @@ header { transition: box-shadow var(--transition-fast); } -/* Jump-to-tab dropdown trigger — hidden until updateTabOverflowShadows() - detects the strip can't fit every tab; on mobile that kicks in with far - fewer tabs than on desktop, so the same mechanism covers both. */ .tab-overflow-btn { display: none; align-items: center; @@ -956,7 +1205,12 @@ header { .model-picker-menu { position: absolute; top: calc(100% + 8px); - left: 0; + /* Right-anchored, because the control is in the right cluster now. A 360px + menu hung off the left edge of a button that close to the window would run + out of it. css/mobile.css pins it to both edges below 900px and so wins + there regardless. */ + right: 0; + left: auto; /* Wide enough for the longest full name ("Two-Way Nondeterministic Finite Automaton") plus its short-code chip, on one line. */ width: 360px; @@ -1008,6 +1262,12 @@ header { color: var(--text2); } +.model-item:focus-visible { + outline: none; + background: var(--surface3); + box-shadow: inset 2px 0 0 var(--accent); +} + .model-item:hover:not(.disabled) { background: var(--surface); color: var(--text); @@ -1102,12 +1362,6 @@ header { } -.hdr-tools { - display: flex; - gap: 3px; - margin-left: auto; -} - @media (max-width: 1100px) { /* Keep the header stable: only the document strip scrolls. Secondary canvas, view, load, and export actions remain available from the existing More menu. */ @@ -1117,10 +1371,22 @@ header { overflow: visible; } + /* No column spans: the panels are overlays at this width, so there is nothing + under the header to line up with, and reserving their widths would squeeze + the strip out of a header that is already shedding controls. */ + .hdr-left, + .hdr-right { + min-width: 0; + } + + /* The toolbar's rules go with the group they fenced: the canvas actions are + the toolbar's first child, so leaving its rule in place would leave the + run starting with one. What is left is four icons, which is a short + enough run not to need fencing. */ #hdr-canvas-actions, - .tools-picker, .hdr-file-actions .hdr-load-action, .hdr-file-actions .hdr-export-action, + .hdr-toolbar > .hdr-sep, header > .hdr-sep, .hdr-right > .hdr-sep { display: none; @@ -1130,6 +1396,15 @@ header { display: block; } + /* With the caret gone the split is one button again, so it has to be shaped + like one: `:last-child` is structural and still matches the hidden caret, + so the main half would otherwise keep two square corners. Same selector as + the rule it is undoing, or the four longhands there outrank a shorthand. */ + .hdr-split > .hbtn:first-child { + border-start-end-radius: 6px; + border-end-end-radius: 6px; + } + .tab-bar-wrap { min-width: 96px; } @@ -1175,9 +1450,7 @@ header { gap: 3px; } - #hdr-canvas-actions, - .hdr-history-actions, - .hdr-file-actions { + .hdr-toolbar { padding: 0; background: transparent; } diff --git a/css/modals.css b/css/modals.css index 2942204..ea14f8d 100644 --- a/css/modals.css +++ b/css/modals.css @@ -3087,6 +3087,18 @@ button.sm-stat:disabled { background: var(--surface3); } +/* Menu rows are divs, so focus has to be drawn by the app rather than by the + browser's default outline suppression in resets. Inset accent bar — the + same cue the app's own list rows and section headers use — plus the hover + ground so a keyboard user sees the row they are on. +.ctx-i[tabindex]:focus-visible is the selector on purpose: rows built without + a tabindex (canvas ctx menus driven by pointer) keep their old look. */ +.ctx-i[tabindex]:focus-visible { + background: var(--surface3); + outline: none; + box-shadow: inset 2px 0 0 var(--accent); +} + /* display:flex above outranks the user-agent's [hidden] rule, so a hidden menu item would otherwise still be laid out. Needed by #updates-btn, which ships hidden and is revealed only on desktop builds that can actually update. */ diff --git a/css/panels.css b/css/panels.css index 4c38937..ccc11c6 100644 --- a/css/panels.css +++ b/css/panels.css @@ -733,11 +733,12 @@ body.panel-resizing .rpanel { gap: 2px; min-width: 0; min-height: 36px; - /* One pixel taller than the header's content box, so the selected tab's own - fill lands *on* the header's bottom rule and breaks it. That break is the - whole tab metaphor: the rule separates chrome from content everywhere - except under the tab you are reading, where the two are the same thing. */ - margin-bottom: -1px; + /* The strip sits flush against the header's bottom rule, and the selected + tab's own border-bottom (see .panel-tab.active) covers that rule's pixel + so the two meet with nothing between them — the seam is drawn by the tab + rather than by overlapping the header's rule, which keeps the strip's + height stable as selection moves between tabs of differing fill. */ + margin-bottom: 0; } .panel-tab { @@ -780,10 +781,10 @@ body.panel-resizing .rpanel { } /* The accent rides the tab's top edge rather than sitting under it as a rule. - Under the tab is where the header's hairline is, and the selected tab's job - is to *break* that line, not to draw a second one over it. It needs no radius - of its own: .panel-tab clips to its own rounded corners, so the cap picks up - the curve it is sitting inside. */ + Under the tab is where the tab's own seam border is, and a second rule there + would draw back what the shape just removed. It needs no radius of its own: + .panel-tab clips to its own rounded corners, so the cap picks up the curve + it is sitting inside. */ .panel-tab::after { content: ''; position: absolute; @@ -821,12 +822,14 @@ body.panel-resizing .rpanel { } /* Continuous with the panel below it: same fill, an outline that stops at the - header rule, and the extra pixel from .panel-tabs' negative margin covering - that rule so the two meet with nothing between them. */ + header rule, and the tab's own bottom border in --bg2 covering that rule so + the two meet with nothing between them. */ .panel-tab.active { color: var(--text); background: var(--bg2); border-color: var(--border); + border-bottom: 1px solid var(--bg2); + margin-bottom: -1px; } .panel-tab.active:hover { diff --git a/css/variables.css b/css/variables.css index e58134e..05908de 100644 --- a/css/variables.css +++ b/css/variables.css @@ -36,13 +36,13 @@ belongs to the canvas, and what each panel shows at its inner corners is the concave outside of it. mobile.css zeroes it where the panels stop being rails and the well stops having chrome beside it. */ - --canvas-radius: 8px; + --canvas-radius: 14px; /* The well had chrome on three sides only — header above, a sidebar either side, and nothing under it, which left the bottom corners curving into a window edge instead of into a frame. This is the sill that closes it: the canvas stops short by this much and the panels carry --bg2 across the gap, so the frame runs all the way round. */ - --canvas-sill: 10px; + --canvas-sill: 14px; --overlay: rgba(2, 8, 20, .68); --overlay-blur: 3px; --overlay-ring: rgba(255, 255, 255, .025); diff --git a/css/views.css b/css/views.css index 4df477a..1d84a27 100644 --- a/css/views.css +++ b/css/views.css @@ -324,12 +324,19 @@ overflow: hidden; background: var(--bg); cursor: default; - /* The well. overflow:hidden was already here, so the radius clips the grid - and glow pseudos and the SVG along with it. The ring is an inset shadow - rather than a border so it follows the curve without taking layout width - off a flex item that the camera measures. */ + /* The well. `overflow: hidden` clips the grid, glow pseudos and SVG to the + radius. The surrounding panel surface supplies the concave corners, so + there is no seam or border to draw between the rails and the canvas. + + All four corners, and the top two were once squared off on the reasoning + that the workspace tabs open down into this well, so its top edge is the + floor they stand on and a floor should not curve away at its ends. That + reasoning was wrong about what the corner is. The uncovered quarter is not + a gap in the floor — it is the concave corner each panel appears to have, + which is the one thing giving the panels a shape where they meet the + canvas, and the tabs are in the middle of the strip, nowhere near it. The + artifact under the tabs was the grid, and it is fixed where the grid is. */ border-radius: var(--canvas-radius); - box-shadow: inset 0 0 0 1px var(--border); /* Stretch alignment subtracts the margin, so this shortens the canvas rather than overflowing #v-build — and the panels either side keep their full height, which is what turns the gap into a sill instead of a letterbox. */ @@ -356,6 +363,17 @@ --cam-z is set by applyCamera() in js/canvas.js; it is unregistered, so it substitutes textually into the calc and the fallback covers first paint. */ opacity: calc(.35 * clamp(0, (var(--cam-z, 1) * 40 - 12) / 12, 1)); + /* Clean under the tab strip. The grid's vertical lines start at the well's + top edge, which is flush with the header, so they used to drop straight + out of the gaps between the workspace tabs — two of them + hanging off each tab, which reads as something drawn wrong rather than as + a background. A browser's content area is clean where the tabs land, for + the same reason. The ramp is short and only at the top: the grid is a + placement reference and losing sixteen pixels of it costs nothing, while a + hard cut-off would just be a horizontal line in place of the vertical + ones. */ + -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 16px); + mask-image: linear-gradient(to bottom, transparent 0, #000 16px); } .canvas-area::after { diff --git a/index.html b/index.html index b07bebe..48a8ecf 100644 --- a/index.html +++ b/index.html @@ -33,6 +33,7 @@ + @@ -107,175 +108,231 @@
-
- -
- +
+ +
+
+ +
+ +
+
+ +
-
- -
- - -
- -
- -
- - -
- -