Files
support-fins/web/style.css

883 lines
29 KiB
CSS

:root {
--topbar-h: 52px; /* the top bar's height, kept by ui/topbar.js */
--bg: #14161a;
--panel: #1c1f26;
--line: #2c313b;
--text: #e7e9ee;
--dim: #8d95a5;
--accent: #4da3ff;
--btn: #2a303a; /* button fill — deliberately lighter than --panel so a
control reads as a control against the dark viewport */
--btn-hi: #333a46; /* button hover fill */
--btn-line: #454d5c; /* button edge, strong enough to see over the 3D scene */
}
* { box-sizing: border-box; }
/* An author `display` rule outranks the UA stylesheet's [hidden]{display:none},
so any flex/grid panel here would ignore its own hidden attribute. */
[hidden] { display: none !important; }
html, body {
margin: 0;
height: 100%;
overflow: hidden;
background: var(--bg);
color: var(--text);
font: 13px/1.45 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
}
#viewport { position: fixed; inset: 0; }
#viewport canvas { display: block; }
/* ---------- long-build spinner ---------- */
#spinner {
position: fixed;
left: 50%; top: 50%;
transform: translate(-50%, -50%);
z-index: 4;
display: flex;
align-items: center;
gap: 9px;
padding: 9px 14px;
border: 1px solid var(--line);
border-radius: 6px;
background: var(--panel);
color: var(--dim);
font-size: 12px;
pointer-events: none; /* informational only; never blocks the canvas */
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.4);
/* Always laid out (never display:none) so the opacity transition fires every
time; visibility keeps the idle spinner out of the a11y/hit tree. Toggling
display:none instead skipped the fade unpredictably. */
opacity: 0;
visibility: hidden;
transition: opacity 0.5s ease, visibility 0s linear 0.5s; /* fade out, then hide */
}
#spinner.show {
opacity: 1;
visibility: visible;
transition: opacity 0.5s ease, visibility 0s; /* show immediately, fade in */
}
@media (prefers-reduced-motion: reduce) {
#spinner, #spinner.show { transition: none; }
}
#spinner .ring {
width: 15px; height: 15px;
border: 2px solid var(--btn-line);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { #spinner .ring { animation-duration: 2.4s; } }
/* ---------- top bar ---------- */
#topbar {
position: fixed;
z-index: 4; /* above the empty-state scrim, or the controls read
as disabled on the one screen that must invite a click;
and above #right-rail (3), which the Export menu drops over */
top: 0; left: 0; right: 0;
display: flex;
align-items: center;
gap: 14px;
padding: 10px 14px;
background: linear-gradient(var(--bg), transparent);
pointer-events: none;
/* a window too narrow for one row wraps onto a second (and third) instead of
pushing buttons off the right edge; ui/topbar.js measures the bar into
--topbar-h so the rails and dropped panels start below it */
flex-wrap: wrap;
row-gap: 8px;
justify-content: flex-end; /* a wrapped row stays with the buttons on the right */
}
#topbar > * { pointer-events: auto; }
/* Narrower windows: shed words before anything wraps. GitHub and Ko-fi go to
icons first (their titles and aria-labels still say what they are), then the
brand text, the Build volume label and some slider width. */
@media (max-width: 1320px) {
#topbar { gap: 10px; }
#topbar .btn.gh .lbl, #topbar .btn.kofi .lbl { display: none; }
}
@media (max-width: 1140px) {
#topbar .brand-text, #topbar .field .lbl { display: none; }
#topbar #thr { width: 90px; }
}
/* wrapped onto a phone: the first row is the logo and the import controls, so the
logo stays left of them rather than floating mid-row */
@media (max-width: 600px) {
#topbar .brand { margin-right: auto; }
/* Export can land first on its wrapped row: drop its menu rightward, not off
the left edge */
#export-menu { right: auto; left: 0; }
}
#topbar .spacer { flex: 1; pointer-events: none; }
.brand {
display: inline-flex;
align-items: center;
gap: 7px;
font-weight: 650;
letter-spacing: .02em;
margin-right: 4px;
}
.brand-mark { height: 20px; width: auto; display: block; }
.btn {
display: inline-block;
padding: 5px 12px;
/* A plain button floats over the near-black viewport, so --panel/--line (both
within a few points of --bg) made it read as a faint dark smudge. Lift it
off the scene: a lighter fill, a defined edge, and a drop shadow. */
border: 1px solid var(--btn-line);
border-radius: 6px;
background: var(--btn);
color: var(--text);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45);
cursor: pointer;
user-select: none;
}
.btn:hover { border-color: var(--accent); color: var(--accent); background: var(--btn-hi); }
/* Keyboard focus was invisible: no control showed a focus ring, so tabbing
through the tool was untrackable. One visible ring for every focusable thing
-- buttons, the select, the slider, the file label -- shown only for keyboard
focus (:focus-visible) so a mouse click doesn't leave a lingering outline. */
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.btn:focus-visible {
border-color: var(--accent);
color: var(--accent);
}
/* A field that doesn't apply in this mode (Wide-face coverage in Full coverage). */
.fld.disabled { opacity: 0.45; }
.fld.disabled input { cursor: not-allowed; }
/* A disabled Undo/Redo shouldn't look clickable. */
.btn:disabled {
opacity: 0.4;
cursor: default;
box-shadow: none;
}
.btn:disabled:hover { border-color: var(--btn-line); color: var(--text); background: var(--btn); }
/* Export menu: a small panel dropped under its button, items styled as rows. */
.menu-wrap { position: relative; }
.menu {
position: absolute;
top: calc(100% + 4px);
right: 0;
z-index: 4;
display: flex;
flex-direction: column;
min-width: 100%;
padding: 4px;
border: 1px solid var(--btn-line);
border-radius: 6px;
background: var(--btn);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
}
.menu[hidden] { display: none; }
.menu button {
padding: 5px 10px;
border: 0;
border-radius: 4px;
background: none;
color: var(--text);
font: inherit;
text-align: left;
white-space: nowrap;
cursor: pointer;
}
.menu button:hover, .menu button:focus-visible { background: var(--btn-hi); color: var(--accent); outline: none; }
.menu button:disabled { opacity: 0.4; cursor: default; background: none; color: var(--text); }
/* Plugins menu (web/ui/plugins.js): the same dropped panel, wide enough for a
row per plugin with its install note; scrolls when the window is short. */
.plugins-wrap, .calibrate-wrap { position: relative; }
/* Calibrate menu (web/ui/calibrate.js) borrows this panel and its rows. */
.calibrate-intro { margin: 2px 4px 8px; color: var(--dim); font-size: 12px; line-height: 1.4; }
.plugins-panel {
width: min(380px, calc(100vw - 32px));
max-height: calc(100vh - var(--topbar-h) - 16px); /* under a bar of any height */
overflow-y: auto;
padding: 8px 10px;
gap: 2px;
font-size: 13px;
white-space: normal;
}
/* Narrow window: drop the panel from the window's right edge, not the button's, so
it can't run off the left. */
@media (max-width: 600px) {
.plugins-panel { position: fixed; top: var(--topbar-h); left: 16px; right: 16px; width: auto; min-width: 0; }
}
.plugins-computer { display: flex; align-items: center; gap: 8px; color: var(--dim); padding: 2px 0 6px; }
.plugins-computer select { flex: 1; }
.plugin { border-top: 1px solid var(--line); }
.plugin-head {
display: flex; align-items: center; gap: 8px;
padding: 7px 4px; cursor: pointer; list-style: none; border-radius: 6px;
}
.plugin-head::-webkit-details-marker { display: none; }
.plugin-head::before { /* the open/closed caret */
content: '▸'; color: var(--dim); width: 10px; flex: none;
transition: transform 0.12s;
}
.plugin[open] > .plugin-head::before { transform: rotate(90deg); }
.plugin-head:hover, .plugin-head:focus-visible { background: var(--btn-hi); outline: none; }
.plugin-name { flex: 1; }
.plugin-name b { font-weight: 600; }
.plugin-v { color: var(--dim); font-size: 12px; font-variant-numeric: tabular-nums; }
.plugin-head .btn { text-decoration: none; font-size: 12px; }
.plugin-none { color: var(--dim); font-size: 12px; }
.plugin-body { padding: 0 4px 10px 22px; }
.plugin-needs, .plugin-ver { color: var(--dim); font-size: 12px; margin-top: 2px; }
.plugin-ver { font-variant-numeric: tabular-nums; }
.plugin-how { margin-top: 4px; font-size: 12px; line-height: 1.4; }
.plugin-note { margin-top: 4px; font-size: 12px; line-height: 1.4; color: #ffb492; }
.plugins-foot { margin: 6px 0 0; padding-top: 8px; border-top: 1px solid var(--line); color: var(--dim); font-size: 12px; }
.plugins-foot a { color: var(--accent); }
.btn.primary {
border-color: var(--accent);
color: #06121f;
background: var(--accent);
font-weight: 600;
}
.btn.primary:hover { background: #6cb5ff; color: #06121f; }
.field { color: var(--dim); }
.field select,
.plugins-computer select {
margin-left: 6px;
padding: 4px 8px;
border: 1px solid var(--btn-line);
border-radius: 6px;
background: var(--btn);
color: var(--text);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45);
font: inherit;
}
.plugins-computer select { margin-left: 0; } /* its label already has an 8px gap */
.badge {
padding: 4px 10px;
border: 1px solid var(--line);
border-radius: 999px;
color: var(--dim);
font-size: 12px;
}
/* ---------- left rail ---------- */
/* One scrolling column holds the stats panel and the rotate panel, pinned from
under the top bar to the bottom of the screen. When they fit, the rotate panel
sits at the bottom (margin-top:auto) exactly as before; when the window is too
short, the rail scrolls instead of the two panels overlapping. pointer-events
are off on the rail so the empty gap between the panels still orbits the scene,
and back on for the panels themselves. */
#left-rail {
position: fixed;
top: var(--topbar-h); bottom: 14px; left: 14px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
overflow-y: auto;
overflow-x: hidden;
pointer-events: none;
scrollbar-width: thin;
}
#left-rail > * { pointer-events: auto; }
/* Pins #orient to the bottom when the rail has spare height, but shrinks to 0
under pressure so the rail scrolls cleanly instead of the panels overlapping. */
#left-rail > .rail-spacer { flex: 1 1 0; min-height: 0; pointer-events: none; }
.rail-spacer[hidden] { display: none; }
/* Right rail: same one-column-that-scrolls trick as #left-rail, for the fin
options (top, grows DOWN as draw mode adds controls) and the status/receipt
card (bottom). Independently-fixed, they used to grow into each other and off
the bottom of the screen in draw mode; sharing a rail with a flex spacer keeps
options up top, status pinned low, and scrolls the pair under a short window
instead of overlapping. pointer-events off on the rail so the gap orbits the
scene, back on for the cards. */
#right-rail {
position: fixed;
top: var(--topbar-h); bottom: 34px; right: 14px; /* bottom clears the fps readout */
z-index: 3;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 12px;
overflow-y: auto;
overflow-x: hidden;
pointer-events: none;
scrollbar-width: thin;
}
#right-rail > * { pointer-events: auto; }
#right-rail > .rail-spacer { flex: 1 1 0; min-height: 0; pointer-events: none; }
/* ---------- stats ---------- */
#stats {
/* Fixed width so a long value (a filename, the receipt's sentence) wraps
inside the panel instead of stretching the whole left column. Shrinks on
very narrow viewports. */
width: 272px;
max-width: calc(100vw - 28px);
box-sizing: border-box;
padding: 10px 14px;
border: 1px solid var(--line);
border-radius: 8px;
background: color-mix(in srgb, var(--panel) 88%, transparent);
backdrop-filter: blur(6px);
}
#stats dl {
display: grid;
grid-template-columns: auto 1fr;
gap: 3px 16px;
margin: 0;
}
#stats dt { color: var(--dim); }
#stats dd {
margin: 0;
font-variant-numeric: tabular-nums;
min-width: 0;
overflow-wrap: anywhere; /* a long filename wraps rather than overflowing */
}
#stats dd.warn { color: #ffb454; }
#stats dd.good { color: #6fcf8b; }
/* Reserve two lines for the analysis timing: it wraps to a second line once the
fin/pad time is appended, and that one-line growth used to nudge everything below
the panel down when fins were added. Always two lines high = no shift. */
#stats #s-time { display: block; min-height: 2.6em; line-height: 1.3; }
#stats hr {
border: 0;
border-top: 1px solid var(--line);
margin: 9px 0;
}
/* legend swatches, keyed to the shading colours in ui/part.js */
.sw {
display: inline-block;
width: 8px; height: 8px;
margin-right: 6px;
border-radius: 2px;
vertical-align: baseline;
}
.sw-over { background: #ff5a4d; }
.sw-small { background: #ffb454; margin: 0 3px 0 0; }
.sw-bed { background: #3f7fd0; }
.sw-fin { background: #59d98e; }
.sw-pad { background: #e8b64c; }
/* What the fins did and did not do. Wraps rather than truncating: every line
here is a limitation the user needs before they commit to a print. */
.note {
max-width: 26ch;
margin: 0;
color: var(--dim);
font-size: 11px;
line-height: 1.45;
}
.note:empty { display: none; }
.note.good { color: #6fcf8b; } /* the "you don't need me" all-clear */
/* The receipt: the "what did this get me" payoff. The exact grams are the
headline; the saving is the reason to bother. Set apart from the plain stats
so the number reads as a result, not another row. */
#receipt {
margin: 0;
padding: 7px 12px;
border: 1px solid var(--line);
border-left: 3px solid #6fcf8b;
border-radius: 6px;
background: #171a20;
display: flex;
align-items: baseline;
gap: 4px;
}
#receipt[hidden] { display: none; }
#r-grams {
font-size: 20px;
font-weight: 650;
color: #6fcf8b;
font-variant-numeric: tabular-nums;
}
#receipt .r-unit { color: var(--dim); font-size: 11px; }
/* The detail (methodology + the "20-45% less plastic" claim) lives behind this
icon's tooltip -- long and repetitive to show every time, so it's opt-in. */
#receipt .r-info {
margin-left: auto;
align-self: center;
width: 16px; height: 16px;
padding: 0;
border: 1px solid var(--line);
border-radius: 50%;
background: transparent;
color: var(--dim);
font: italic 600 11px/1 Georgia, serif;
cursor: help;
}
#receipt .r-info:hover { color: var(--text); border-color: var(--dim); }
/* The (i) on the Fins row: same opt-in-detail idea as the receipt's, for the
how-it-works / how-to-fix text that reads as a wall when it's always on. */
.note-info {
margin-left: 5px;
width: 15px; height: 15px;
padding: 0;
border: 1px solid var(--line);
border-radius: 50%;
background: transparent;
color: var(--dim);
font: italic 600 10px/1 Georgia, serif;
cursor: help;
vertical-align: middle;
}
.note-info[hidden] { display: none; }
.note-info:hover { color: var(--text); border-color: var(--dim); }
/* Fin options, anchored under the toggle rather than inside the topbar, so
turning fins on never moves another control. */
#fin-opts {
width: 264px;
max-width: calc(100vw - 28px);
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 8px;
padding: 10px 12px;
border: 1px solid var(--line);
border-radius: 8px;
background: color-mix(in srgb, var(--panel) 92%, transparent);
backdrop-filter: blur(6px);
}
#fin-opts .fld {
display: flex;
flex-direction: column;
gap: 4px;
color: var(--dim);
}
#fin-opts select,
#orient .nav-pick select,
#fin-opts input[type=number] {
padding: 4px 8px;
border: 1px solid var(--line);
border-radius: 6px;
background: var(--panel);
color: var(--text);
font: inherit;
}
#fin-opts input[type=number] { width: 72px; }
#fin-opts input[type=range] { width: 100%; margin: 0; accent-color: var(--accent); }
/* A one-control field: label left, control right, one line instead of two. */
#fin-opts .fld.row {
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 8px;
}
#fin-opts .fld.row select { min-width: 0; max-width: 130px; }
/* Bed pad's Auto option names what it built ("Auto (Sure hold)"), which the
130px cap cut off -- the one label that must be read whole. */
#fin-opts .fld.row select#bed-pad { max-width: 170px; }
/* Collapsible sections (issue #41). */
#fin-opts .sec { border-top: 1px solid var(--line); padding-top: 8px; }
#fin-opts .sec:first-child { border-top: 0; padding-top: 0; }
#fin-opts .sec > summary {
display: flex;
align-items: baseline;
gap: 8px;
cursor: pointer;
list-style: none;
user-select: none;
}
#fin-opts .sec > summary::-webkit-details-marker { display: none; }
#fin-opts .sec > summary::before {
content: "";
flex: none;
width: 0; height: 0;
align-self: center;
border-left: 4px solid var(--dim);
border-top: 4px solid transparent;
border-bottom: 4px solid transparent;
transition: transform 0.12s;
}
#fin-opts .sec[open] > summary::before { transform: rotate(90deg); }
#fin-opts .sec-title { color: var(--text); font-weight: 600; white-space: nowrap; }
#fin-opts label.sec-title { font-weight: 600; }
#fin-opts .sec-title .mut { font-weight: 400; color: var(--dim); }
#fin-opts .sec:not([open]) .sec-title .mut { display: none; } /* room for the recap */
/* The one-line recap of a section's values, only while it's collapsed. */
#fin-opts .sec-sum {
flex: 1;
min-width: 0;
text-align: right;
color: var(--dim);
font-size: 11px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#fin-opts .sec[open] > summary > .sec-sum { visibility: hidden; }
#fin-opts .sec > :not(summary) { margin-top: 8px; }
#fin-opts .sec[open] { display: flex; flex-direction: column; }
.check {
display: inline-flex;
align-items: center;
gap: 6px;
color: var(--dim);
cursor: pointer;
}
/* Draw-mode controls, revealed under the placement select when Draw is live. */
#draw-controls {
display: flex;
flex-direction: column;
gap: 8px;
padding-top: 8px;
border-top: 1px solid var(--line);
}
#draw-controls[hidden] { display: none; }
.draw-hint {
margin: 0;
max-width: 22ch;
color: var(--dim);
font-size: 11px;
line-height: 1.5;
}
.draw-hint strong { color: var(--text); }
.draw-hint kbd {
padding: 0 4px;
border: 1px solid var(--line);
border-radius: 3px;
background: var(--bg);
font: inherit;
font-size: 10px;
}
.draw-btns { display: flex; gap: 6px; }
/* No single control in the topbar may wrap its own text onto a second line (a
two-line button makes the bar jump in height as its contents change); the bar
as a whole wraps between controls on a narrow window instead. */
#topbar .btn,
#topbar .badge,
#topbar .field { white-space: nowrap; }
.field input[type=range] {
width: 92px;
margin: 0 6px;
vertical-align: middle;
accent-color: var(--accent);
}
.field output { color: var(--text); font-variant-numeric: tabular-nums; }
.custom-vol { display: inline-flex; align-items: center; gap: 4px; }
.custom-vol input {
width: 4.2em;
padding: 4px 6px;
border: 1px solid var(--btn-line);
border-radius: 6px;
background: var(--btn);
color: var(--text);
font: inherit;
font-variant-numeric: tabular-nums;
}
#orient {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
max-width: 15rem;
padding: 9px 12px;
border: 1px solid var(--line);
border-radius: 8px;
background: color-mix(in srgb, var(--panel) 88%, transparent);
backdrop-filter: blur(6px);
}
#orient .lbl { color: var(--dim); margin-right: 2px; }
#orient .readout {
flex-basis: 100%;
margin: 4px 0 0;
font-variant-numeric: tabular-nums;
}
#orient .readout #rot-delta { color: var(--accent); margin-left: 8px; }
#orient kbd {
padding: 0 4px;
border: 1px solid var(--line);
border-radius: 3px;
background: var(--bg);
font: inherit;
font-size: 11px;
}
#orient .hint {
flex-basis: 100%;
margin: 2px 0 0;
color: var(--dim);
font-size: 12px;
}
.btn.sm { padding: 3px 9px; }
#orient .suggest { flex-basis: 100%; margin-top: 6px; }
.suggest-head { display: flex; align-items: center; gap: 6px; }
#orient .suggest #suggest-note { margin-top: 4px; }
#suggest-note.good { color: #6fcf8b; } /* the "best support is no support" win */
/* Collapse/expand the results in place (keeps them, unlike a dismiss). A bare
chevron that points right when folded and rotates down when open. */
.disclosure {
flex: 0 0 auto;
width: 20px; height: 20px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
background: transparent;
color: var(--dim);
font-size: 17px;
line-height: 1;
cursor: pointer;
transition: transform .15s ease, color .15s ease;
transform: rotate(90deg); /* expanded: chevron points down */
}
.disclosure[aria-expanded="false"] { transform: rotate(0deg); } /* collapsed: points right */
.disclosure[hidden] { display: none; }
.disclosure:hover { color: var(--text); }
#suggest-body[hidden] { display: none; }
#suggest-list {
display: flex;
flex-direction: column;
gap: 5px;
margin-top: 7px;
}
/* One tight line per pose, so three suggestions cost ~90px, not ~280. */
.suggest-row {
display: flex;
align-items: baseline;
gap: 6px;
text-align: left;
padding: 5px 8px;
line-height: 1.3;
}
.suggest-row .sr-rank {
flex: 0 0 auto;
min-width: 22px;
color: var(--accent);
font-weight: 600;
font-size: 11px;
}
.suggest-row .sr-line {
flex: 1 1 auto;
min-width: 0; /* let the row wrap inside the panel instead of spilling past its border */
font-variant-numeric: tabular-nums;
font-size: 11px;
}
.suggest-row.active { border-color: var(--accent); background: var(--btn-hi); }
.suggest-row.active .sr-rank::after { content: ' ✓'; }
.suggest-row.bad .sr-rank { color: var(--dim); }
/* the "best support is no support" win: a pose that needs nothing, or that keeps
every hole/bore support-free. Celebrated, not buried as a gray "0 fins". */
.suggest-row .sr-badge {
font-size: 10px;
font-weight: 600;
letter-spacing: .02em;
color: #6fcf8b;
border: 1px solid #6fcf8b66;
border-radius: 3px;
padding: 0 4px;
margin-left: 6px;
white-space: nowrap;
}
.suggest-row.free { border-color: #6fcf8b66; }
.suggest-row.free .sr-rank { color: #6fcf8b; }
/* ---------- automatic layer view ---------- */
#orient .layerview { flex-basis: 100%; margin-top: 8px; }
.layers-toggle { color: var(--dim); font-size: 12px; }
#orient .navview { flex-basis: 100%; margin-top: 6px; }
.nav-pick { color: var(--dim); font-size: 12px; }
#orient .nav-pick select { margin-left: 6px; padding: 2px 6px; font-size: 12px; }
#orient .navview .hint { margin: 4px 0 0; }
/* ---------- strength arrow (load direction) ---------- */
#orient .loadfx .loadfx-lede { flex-basis: 100%; margin: 0 0 3px;
font-size: 13px; color: var(--text); }
#orient .loadfx .loadfx-lede strong { color: var(--accent); }
#orient .loadfx .strength-pad { flex-basis: 100%; display: grid;
grid-template-columns: repeat(2, 1fr); gap: 4px; margin: 2px 0; }
#orient .loadfx .strength-pad .btn { padding: 5px 6px; font-size: 13px; }
/* keep the lay-flat button on its own row under the rotate hint */
#orient #lay-face { flex-basis: 100%; margin-top: 2px; }
#orient .loadfx { flex-basis: 100%; margin-top: 10px; padding-top: 8px;
border-top: 1px solid var(--line); display: flex; flex-wrap: wrap; gap: 6px; }
#orient .loadfx #load-hint { flex-basis: 100%; margin: 2px 0 0; color: var(--dim); font-size: 12px; }
#orient .loadfx #load-hint strong { color: var(--text); }
.btn.sm.active { border-color: var(--accent); color: var(--accent); background: var(--btn-hi); }
.load-verdict {
flex-basis: 100%;
margin: 4px 0 0;
padding: 6px 9px;
border-left: 3px solid var(--dim);
border-radius: 3px;
background: var(--btn);
font-size: 12px;
line-height: 1.35;
}
.load-verdict.good { border-left-color: #6fcf8b; color: #cdeccf; }
.load-verdict.mixed { border-left-color: #ffb454; color: #f0dcc0; }
.load-verdict.poor { border-left-color: #ff6b6b; color: #f2cfcf; }
#orient .loadfx #load-suggest { flex-basis: 100%; margin-top: 2px; }
/* ---------- bottom-right status card ---------- */
#status {
width: 264px;
max-width: calc(100vw - 28px);
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 6px;
pointer-events: none; /* clicks pass through the empty gaps to the canvas */
text-align: left;
}
#status[hidden] { display: none; }
#status > * { pointer-events: auto; }
/* Overhang warning: the tool isn't holding these up, so it's an amber caution, not
a green all-clear. Card look sets it apart from the plainer .note lines above. */
.over-warn {
margin: 0;
padding: 6px 9px;
border-left: 3px solid #ffb454;
border-radius: 3px;
background: var(--btn);
color: #f0dcc0;
font-size: 12px;
line-height: 1.35;
}
.over-warn:empty { display: none; }
#fps {
position: fixed;
right: 14px; bottom: 12px;
color: var(--dim);
font-variant-numeric: tabular-nums;
}
/* ---------- drop target ---------- */
#drop {
position: fixed;
inset: 0;
/* centred in the space under the top bar, which grows on a narrow window */
padding: var(--topbar-h) 16px 16px;
display: grid;
place-items: center;
text-align: center;
background: color-mix(in srgb, var(--bg) 72%, transparent);
pointer-events: none;
}
#drop.hidden { display: none; }
#drop.armed { background: color-mix(in srgb, var(--accent) 12%, var(--bg) 70%); }
#drop .inner {
padding: 30px 38px;
border: 2px dashed var(--line);
border-radius: 14px;
max-width: 34rem;
text-align: left;
/* a short phone under a wrapped bar can't fit it all: the card scrolls (drops
are caught on the window, io.js, so this doesn't block them) */
max-height: 100%;
overflow-y: auto;
pointer-events: auto;
box-sizing: border-box;
}
@media (max-width: 600px) {
#drop .inner { padding: 18px 18px; }
}
#drop.armed .inner { border-color: var(--accent); }
#drop .big {
margin: 0 0 10px;
font-size: 22px;
font-weight: 650;
letter-spacing: -0.01em;
}
#drop .lede { margin: 0 0 16px; color: var(--text); }
#drop .steps {
margin: 0 0 16px;
padding-left: 1.25em;
color: var(--dim);
}
#drop .steps li { margin-bottom: 4px; }
#drop .steps strong { color: var(--text); font-weight: 600; }
#drop .small { margin: 0; color: var(--dim); font-size: 12px; }
/* ---------- Ko-fi support link ---------- */
/* Warm accent so the support link reads apart from the blue tool controls,
without shouting — the tool is the gift, this is a soft "thanks" ask. */
.btn.gh { display: inline-flex; align-items: center; text-decoration: none; }
.btn.gh svg { flex: none; }
.btn.kofi { border-color: #ff8a5b; color: #ffb492; text-decoration: none; }
.btn.kofi:hover { border-color: #ff8a5b; color: #06121f; background: #ff8a5b; }
#drop .kofi-line { margin: 14px 0 0; color: var(--dim); font-size: 12px; }
#drop .kofi-line .kofi {
pointer-events: auto; /* the #drop scrim is click-through; the link must not be */
color: #ff9e73;
text-decoration: none;
border-bottom: 1px solid color-mix(in srgb, #ff8a5b 55%, transparent);
}
#drop .kofi-line .kofi:hover { color: #ffb492; border-bottom-color: #ff8a5b; }
/* ---------- 3MF object picker ---------- */
/* A plate with several objects: choose which body to fin (or check several to
merge). Sits above everything, including the drop scrim. */
#picker {
position: fixed;
inset: 0;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
background: rgba(0, 0, 0, 0.55);
}
.picker-panel {
width: min(440px, 100%);
max-height: 80vh;
display: flex;
flex-direction: column;
padding: 18px 20px;
border: 1px solid var(--btn-line);
border-radius: 8px;
background: var(--panel);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
}
.picker-panel h2 { margin: 0 0 4px; font-size: 15px; color: var(--text); }
.picker-sub { margin: 0 0 12px; font-size: 12px; color: var(--dim); line-height: 1.4; }
#picker-list {
margin: 0 0 12px;
padding: 0;
list-style: none;
overflow-y: auto;
border: 1px solid var(--line);
border-radius: 6px;
}
#picker-list li { border-bottom: 1px solid var(--line); }
#picker-list li:last-child { border-bottom: 0; }
#picker-list label {
display: flex;
align-items: baseline;
gap: 9px;
padding: 9px 11px;
cursor: pointer;
}
#picker-list label:hover { background: var(--btn); }
#picker-list input { margin: 0; accent-color: var(--accent); align-self: center; }
#picker-list .pk-name { flex: 1; color: var(--text); font-size: 13px; word-break: break-word; }
#picker-list .pk-meta { color: var(--dim); font-size: 11px; white-space: nowrap; }
.picker-foot { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.picker-foot .note { margin: 0; font-size: 11px; color: var(--dim); }
.picker-actions { display: flex; gap: 8px; flex-shrink: 0; }