Spaces:
Running
Running
| /* catalog.css β the work catalog and the draggable rail. | |
| =========================================================================== | |
| A separate stylesheet rather than more of app.css: these are two new, | |
| self-contained surfaces, and keeping them out of an 8,600-line file makes | |
| them reviewable. Every colour here is one of app.css's existing tokens, so | |
| both themes and the light/dark switch work with nothing added. | |
| =========================================================================== */ | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββ draggable rail ββ */ | |
| .rail-split { | |
| position: fixed; | |
| top: var(--bench-strip-h, 56px); | |
| bottom: 0; | |
| /* Centred ON the boundary, so the grab area straddles the line rather | |
| than sitting to one side of it and feeling offset. */ | |
| left: calc(var(--bench-rail, 392px) - 5px); | |
| width: 11px; | |
| z-index: 36; | |
| cursor: col-resize; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| background: transparent; | |
| /* No transition on `left`: it tracks the pointer, and easing a drag makes | |
| the handle lag behind the cursor. */ | |
| } | |
| .rail-split[hidden] { display: none; } | |
| /* Visible AT REST, and this is the whole point of the control. | |
| The first version started at opacity 0 and only appeared on hover. Measured | |
| in the browser: `getComputedStyle(.rail-split-grip).opacity === "0"` with | |
| the split open and the pointer elsewhere β so the boundary between the | |
| conversation and the bench looked exactly like the fixed 1px border it used | |
| to be, and nothing on screen said it could move. A handle you have to | |
| already know about is not a handle. | |
| So it shows: a short grip on the seam, in --line-bold β one step up from | |
| the .cockpit border-right it sits on, present enough to read as a grab | |
| point, quiet enough not to become a third vertical rule in the layout. It | |
| grows and takes ink on hover, focus and drag, which is where the affordance | |
| should be loud. */ | |
| .rail-split-grip { | |
| width: 3px; | |
| height: 34px; | |
| border-radius: 3px; | |
| background: var(--line-bold); | |
| opacity: 1; | |
| transition: opacity var(--t-fast, .12s) var(--ease, ease), | |
| height var(--t-fast, .12s) var(--ease, ease), | |
| background var(--t-fast, .12s) var(--ease, ease); | |
| } | |
| .rail-split:hover .rail-split-grip, | |
| .rail-split:focus-visible .rail-split-grip, | |
| body.is-railsplitting .rail-split-grip { | |
| height: 46px; | |
| background: var(--ink-soft); | |
| } | |
| @media (prefers-reduced-motion: reduce) { .rail-split-grip { transition: none; } } | |
| .rail-split:focus-visible { | |
| outline: 2px solid var(--ink-strong); | |
| outline-offset: -2px; | |
| } | |
| /* While dragging, kill text selection and stop the pointer being captured by | |
| anything underneath β including the Mol* canvas, which otherwise starts | |
| rotating the model the moment the cursor crosses it. */ | |
| body.is-railsplitting { user-select: none; cursor: col-resize; } | |
| body.is-railsplitting .workspace, | |
| body.is-railsplitting .cockpit { pointer-events: none; } | |
| @media (max-width: 900px) { .rail-split { display: none; } } | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ catalog ββ */ | |
| .cat { | |
| position: fixed; inset: 0; z-index: 90; | |
| display: flex; align-items: center; justify-content: center; | |
| padding: 32px 20px; | |
| } | |
| .cat[hidden] { display: none; } | |
| body.cat-open { overflow: hidden; } | |
| .cat-backdrop { | |
| position: absolute; inset: 0; | |
| background: rgba(6, 8, 12, 0.55); | |
| backdrop-filter: blur(2px); | |
| animation: catFade 160ms var(--ease, ease); | |
| } | |
| @keyframes catFade { from { opacity: 0; } to { opacity: 1; } } | |
| .cat-panel { | |
| position: relative; | |
| width: min(1080px, 100%); | |
| max-height: min(820px, 100%); | |
| display: flex; flex-direction: column; | |
| background: var(--bg-card); | |
| border: 1px solid var(--line-strong); | |
| border-radius: var(--r-4, 6px); | |
| box-shadow: 0 24px 64px rgba(0, 0, 0, .34); | |
| overflow: hidden; | |
| animation: catRise 200ms var(--ease, ease); | |
| } | |
| @keyframes catRise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } } | |
| @media (prefers-reduced-motion: reduce) { | |
| .cat-backdrop, .cat-panel { animation: none; } | |
| } | |
| .cat-head { | |
| display: flex; align-items: center; gap: 12px; | |
| padding: 16px 18px 12px; | |
| border-bottom: 1px solid var(--line); | |
| flex: 0 0 auto; | |
| } | |
| .cat-title { margin: 0; font-size: 16px; font-weight: 600; letter-spacing: -0.01em; color: var(--ink-strong); } | |
| .cat-x { | |
| margin-left: auto; background: none; border: none; cursor: pointer; | |
| font-size: 22px; line-height: 1; color: var(--ink-faint); padding: 2px 8px; | |
| font-family: inherit; | |
| } | |
| .cat-x:hover { color: var(--ink-strong); } | |
| .cat-controls { | |
| display: flex; align-items: center; gap: 12px; flex-wrap: wrap; | |
| padding: 12px 18px; border-bottom: 1px solid var(--line); flex: 0 0 auto; | |
| } | |
| .cat-q { | |
| flex: 1 1 260px; min-width: 0; | |
| padding: 8px 12px; font-size: 13.5px; font-family: inherit; | |
| color: var(--ink); background: var(--bg-app); | |
| border: 1px solid var(--line-strong); border-radius: 999px; | |
| } | |
| .cat-q::placeholder { color: var(--ink-faint); } | |
| .cat-q:focus { outline: none; border-color: var(--ink-soft); } | |
| .cat-seg { display: flex; gap: 2px; flex: 0 0 auto; } | |
| .cat-segb { | |
| padding: 6px 13px; font-size: 12px; font-family: inherit; cursor: pointer; | |
| background: none; border: 1px solid transparent; border-radius: 999px; | |
| color: var(--ink-soft); | |
| } | |
| .cat-segb:hover { color: var(--ink-strong); } | |
| .cat-segb.is-on { | |
| color: var(--ink-strong); border-color: var(--line-strong); background: var(--bg-app); | |
| } | |
| .cat-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 4px 18px 20px; } | |
| .cat-body:focus { outline: none; } | |
| .cat-sec { | |
| margin: 18px 0 10px; font-family: var(--font-mono); font-size: 10px; | |
| letter-spacing: 0.13em; text-transform: uppercase; color: var(--ink-faint); | |
| font-weight: 500; display: flex; align-items: baseline; gap: 7px; | |
| } | |
| .cat-sec span { color: var(--ink-disabled); } | |
| .cat-grid { | |
| display: grid; gap: 10px; | |
| grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); | |
| /* Equal-height rows: chip counts differ per run, and ragged card bottoms | |
| make a grid read as broken rather than varied. */ | |
| grid-auto-rows: 1fr; | |
| } | |
| .cat-empty { color: var(--ink-faint); font-size: 13.5px; padding: 40px 4px; text-align: center; } | |
| /* ββ card βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .cat-card { | |
| display: grid; | |
| grid-template-columns: 104px 1fr; | |
| /* 1fr on the content row, auto on the actions row: with equal-height grid | |
| rows this pins Rename/Delete to the bottom edge of every card, so the | |
| buttons line up across the row however many chips a run carries. */ | |
| grid-template-rows: 1fr auto; | |
| grid-template-areas: "thumb main" "acts acts"; | |
| gap: 12px; | |
| padding: 12px; | |
| border: 1px solid var(--line); | |
| border-radius: var(--r-4, 6px); | |
| background: var(--bg-app); | |
| position: relative; | |
| transition: border-color var(--t-fast, .12s) var(--ease, ease); | |
| } | |
| .cat-card:hover { border-color: var(--line-strong); } | |
| .cat-card.is-sel { border-color: var(--ink-soft); } | |
| .cat-card.is-current { border-color: var(--ink-soft); } | |
| .cat-card.is-current::before { | |
| content: "Open now"; position: absolute; top: -1px; right: 10px; | |
| font-family: var(--font-mono); font-size: 9px; letter-spacing: 0.1em; | |
| text-transform: uppercase; color: var(--bg-app); background: var(--ink-soft); | |
| padding: 1px 6px; border-radius: 0 0 3px 3px; | |
| } | |
| .cat-card.is-busy { opacity: .55; pointer-events: none; } | |
| /* ββ the fold βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .cat-thumb { | |
| grid-area: thumb; | |
| position: relative; | |
| width: 104px; height: 84px; | |
| border-radius: 4px; | |
| background: var(--bg-subtle); | |
| border: 1px solid var(--line); | |
| display: flex; align-items: center; justify-content: center; | |
| overflow: hidden; flex: none; | |
| } | |
| .cat-canvas { width: 100%; height: 100%; display: block; } | |
| .cat-thumb-msg { | |
| position: absolute; inset: 0; | |
| display: flex; align-items: center; justify-content: center; | |
| text-align: center; padding: 6px; | |
| font-family: var(--font-mono); font-size: 8.5px; line-height: 1.35; | |
| letter-spacing: 0.04em; color: var(--ink-disabled); | |
| } | |
| /* The accession is the receipt: it says WHICH model this is, so the picture | |
| is checkable rather than decorative. */ | |
| .cat-acc { | |
| position: absolute; left: 0; right: 0; bottom: 0; | |
| font-family: var(--font-mono); font-size: 8px; letter-spacing: 0.06em; | |
| color: var(--ink-faint); background: color-mix(in srgb, var(--bg-subtle) 84%, transparent); | |
| padding: 1px 4px; text-align: center; | |
| } | |
| .cat-thumb[data-sc="idle"] .cat-canvas, | |
| .cat-thumb[data-sc="loading"] .cat-canvas { opacity: 0; } | |
| .cat-thumb[data-sc="ok"] .cat-thumb-msg { display: none; } | |
| .cat-thumb[data-sc="loading"] .cat-thumb-msg::after { content: "Foldingβ¦"; } | |
| .cat-thumb[data-sc="error"] .cat-thumb-msg::after { content: "Structure unavailable"; } | |
| .cat-thumb[data-sc="ok"] .cat-acc { display: block; } | |
| .cat-thumb[data-sc="none"] .cat-acc { display: none; } | |
| /* A construct has no fold to show, so its tile carries a mark instead of a | |
| picture. Dropping the border is the point: a 1px frame around a small glyph | |
| on an empty ground is the shape of a broken image, and that is what it was | |
| being read as. Without the frame it reads as an icon beside a title, which | |
| is what it is. */ | |
| .cat-thumb--cx { | |
| color: var(--ink-faint); | |
| border-color: transparent; | |
| background: var(--bg-subtle); | |
| } | |
| .cat-thumb--cx svg { width: 34px; height: 34px; } | |
| .cat-card--cx:hover .cat-thumb--cx { color: var(--ink-soft); } | |
| /* ββ body βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .cat-main { grid-area: main; min-width: 0; display: flex; flex-direction: column; gap: 5px; } | |
| .cat-open { | |
| background: none; border: none; padding: 0; text-align: left; cursor: pointer; | |
| font-family: inherit; color: var(--ink-strong); min-width: 0; | |
| } | |
| .cat-name { | |
| display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; | |
| overflow: hidden; font-size: 13.5px; font-weight: 600; line-height: 1.35; | |
| letter-spacing: -0.005em; | |
| } | |
| .cat-open:hover .cat-name { text-decoration: underline; text-underline-offset: 2px; } | |
| .cat-open:focus-visible { outline: 2px solid var(--ink-strong); outline-offset: 2px; border-radius: 2px; } | |
| .cat-flag { | |
| align-self: flex-start; | |
| font-family: var(--font-mono); font-size: 9px; letter-spacing: 0.09em; | |
| text-transform: uppercase; color: var(--warning, var(--ink-strong)); | |
| border: 1px solid currentColor; border-radius: 3px; padding: 1px 5px; | |
| } | |
| .cat-chips { display: flex; flex-wrap: wrap; gap: 4px; } | |
| .cat-chip { | |
| font-family: var(--font-mono); font-size: 9.5px; letter-spacing: 0.04em; | |
| padding: 2px 6px; border-radius: 3px; | |
| background: var(--bg-subtle); color: var(--ink-soft); border: 1px solid var(--line); | |
| } | |
| .cat-chip.is-key { color: var(--ink-strong); border-color: var(--line-strong); } | |
| .cat-chip.is-soft { color: var(--ink-faint); background: none; } | |
| .cat-meta { font-family: var(--font-mono); font-size: 10px; color: var(--ink-faint); letter-spacing: 0.02em; } | |
| .cat-note { margin: 2px 0 0; font-size: 11.5px; color: var(--danger); } | |
| /* ββ actions ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .cat-acts { | |
| grid-area: acts; | |
| display: flex; gap: 6px; justify-content: flex-end; | |
| padding-top: 8px; margin-top: 2px; border-top: 1px solid var(--line); | |
| } | |
| .cat-act { | |
| font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.06em; | |
| padding: 4px 10px; cursor: pointer; | |
| background: none; color: var(--ink-soft); | |
| border: 1px solid var(--line-strong); border-radius: 999px; | |
| } | |
| .cat-act:hover { color: var(--ink-strong); border-color: var(--ink-soft); } | |
| .cat-act.is-danger { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 40%, var(--line)); } | |
| .cat-act.is-danger:hover { border-color: var(--danger); } | |
| .cat-act:focus-visible { outline: 2px solid var(--ink-strong); outline-offset: 1px; } | |
| .cat-rename { display: flex; gap: 6px; align-items: center; } | |
| .cat-rename-i { | |
| flex: 1 1 auto; min-width: 0; padding: 5px 9px; font-size: 12.5px; | |
| font-family: inherit; color: var(--ink); background: var(--bg-card); | |
| border: 1px solid var(--line-strong); border-radius: 4px; | |
| } | |
| .cat-rename-i:focus { outline: none; border-color: var(--ink-soft); } | |
| .cat-confirm { | |
| grid-column: 1 / -1; | |
| display: flex; align-items: center; gap: 8px; flex-wrap: wrap; | |
| margin-top: 8px; padding: 9px 10px; | |
| border: 1px solid color-mix(in srgb, var(--danger) 34%, var(--line)); | |
| border-radius: 4px; | |
| font-size: 12px; color: var(--ink); | |
| } | |
| .cat-confirm span { flex: 1 1 180px; min-width: 0; } | |
| @media (max-width: 620px) { | |
| .cat { padding: 0; } | |
| .cat-panel { max-height: 100%; height: 100%; border-radius: 0; border: none; } | |
| .cat-grid { grid-template-columns: 1fr; } | |
| } | |