| 1 |
:root { |
| 2 |
color-scheme: light dark; |
| 3 |
--bg: #f2f2f6; --card: #ffffff; --text: #1c1c1e; --muted: #6e6e73; |
| 4 |
--accent: #0a7cff; --border: #d5d5da; --danger: #d0342c; |
| 5 |
--swatch: 66px; /* full-size spool swatch; small spools indent to match it */ |
| 6 |
--mini: 32px; /* grid swatch; the grid cell keeps this size whatever fills it */ |
| 7 |
} |
| 8 |
@media (prefers-color-scheme: dark) { |
| 9 |
:root { |
| 10 |
--bg: #101014; --card: #1c1c21; --text: #f2f2f7; --muted: #98989e; |
| 11 |
--accent: #3395ff; --border: #3a3a40; |
| 12 |
} |
| 13 |
} |
| 14 |
* { box-sizing: border-box; } |
| 15 |
/* Keep the hidden attribute working on elements whose author display rule |
| 16 |
(e.g. .row's flex) would otherwise beat the UA's [hidden] style. */ |
| 17 |
[hidden] { display: none !important; } |
| 18 |
body { margin: 0; background: var(--bg); color: var(--text); font: 16px/1.45 system-ui, sans-serif; } |
| 19 |
main { max-width: 640px; margin: 0 auto; padding: 8px 12px 56px; } |
| 20 |
/* The nav is a single row until it cannot be: six items stop fitting around |
| 21 |
360px, so the gap tightens and wrapping catches whatever still does not fit |
| 22 |
rather than pushing the whole page into a sideways scroll. */ |
| 23 |
nav { display: flex; flex-wrap: wrap; gap: 16px; align-items: center; max-width: 640px; margin: 0 auto; padding: 12px; } |
| 24 |
@media (max-width: 400px) { nav { gap: 12px; } } |
| 25 |
nav a { color: var(--accent); text-decoration: none; font-weight: 600; } |
| 26 |
nav .brand { margin-right: auto; color: var(--text); font-size: 18px; } |
| 27 |
h1 { font-size: 22px; margin: 14px 0 10px; } |
| 28 |
a { color: var(--accent); } |
| 29 |
.card { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 12px; margin: 0 0 10px; } |
| 30 |
.row { display: flex; gap: 10px; align-items: center; } |
| 31 |
.grow { flex: 1; min-width: 0; } |
| 32 |
/* Spool swatch: filament-colored ring, black hub hole, and a conic-gradient |
| 33 |
pie in the hub (12 o'clock, clockwise) showing --pct of the spool left. */ |
| 34 |
.swatch { display: block; position: relative; width: var(--swatch); height: var(--swatch); border-radius: 50%; border: 1px solid var(--border); flex-shrink: 0; background: var(--fc, #888888); } |
| 35 |
/* 250 g / 100 g spools (and any spool marked "small" on the edit form) stay at |
| 36 |
the pre-existing 44px at every width; full-size spools are 1.5x that, growing |
| 37 |
to 2x on phones. The width a small spool leaves over becomes a left margin, |
| 38 |
so its swatch ends where a full-size one does and every card's text starts on |
| 39 |
the same vertical line. */ |
| 40 |
.swatch.small { width: 44px; height: 44px; margin-left: calc(var(--swatch) - 44px); } |
| 41 |
@media (max-width: 480px) { :root { --swatch: 88px; } } |
| 42 |
.swatch::before { content: ''; position: absolute; inset: 22%; border-radius: 50%; background: #000; box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.15); z-index: 1; } |
| 43 |
.swatch::after { content: ''; position: absolute; inset: 29%; border-radius: 50%; background: conic-gradient(var(--pc, #fff) var(--pct, 0%), transparent 0); z-index: 2; } |
| 44 |
/* Color-shifting / two-tone filament: a repeating two-color gradient drifting |
| 45 |
along its own (horizontal) axis. Each 44px background tile holds exactly one |
| 46 |
color period, so tiles join seamlessly on any swatch size and moving the |
| 47 |
position by one tile per cycle loops invisibly. Defined before .rainbow so a |
| 48 |
(nonsensical) rainbow+two-tone combination keeps the rainbow ring. */ |
| 49 |
.swatch.twotone { background-image: linear-gradient(90deg, var(--fc, #888888) 0 32%, var(--fc2, var(--fc, #888888)) 50% 82%, var(--fc, #888888) 100%); background-size: 44px 100%; animation: ttslide 4s linear infinite; } |
| 50 |
@keyframes ttslide { to { background-position: 44px 0; } } |
| 51 |
/* Rainbow filament: full-hue ring, slowly cycled with a hue-rotate animation |
| 52 |
(black hub and white pie are unaffected by hue rotation, so they stay put). */ |
| 53 |
.swatch.rainbow { background: conic-gradient(#f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00); animation: huespin 6s linear infinite; } |
| 54 |
@keyframes huespin { to { filter: hue-rotate(360deg); } } |
| 55 |
/* Silk filament: a diagonal highlight sweeps across the ring once every 2 s. |
| 56 |
The sheen is a wide (250%) background layer whose position is animated; |
| 57 |
at rest both end positions leave only its transparent parts visible. */ |
| 58 |
.swatch.silk { background-image: linear-gradient(105deg, transparent 40%, rgba(255, 255, 255, 0.8) 50%, transparent 60%); background-color: var(--fc, #888888); background-size: 250% 100%; background-repeat: no-repeat; background-position: 100% 0; animation: shine 2s ease-in-out infinite; } |
| 59 |
/* The extra position values only apply to swatches with that many background |
| 60 |
layers (silk.transparent has three); browsers ignore the surplus. Keeping |
| 61 |
the non-sheen layers pinned at 0 0 stops the checkerboard from sliding. */ |
| 62 |
@keyframes shine { 0% { background-position: 100% 0, 0 0, 0 0; } 45%, 100% { background-position: 0% 0, 0 0, 0 0; } } |
| 63 |
/* Dual-color silks are common: sheen (layer 1) and drifting gradient (layer 2) |
| 64 |
share one keyframe track — two animations can't both own background-position. |
| 65 |
The gradient covers one 44px period per cycle at a constant rate, so its |
| 66 |
loop stays seamless; the sheen sweeps once during the first 45%. Placed |
| 67 |
before .rainbow.silk so a rainbow spool keeps its ring here too. */ |
| 68 |
.swatch.silk.twotone { background-image: linear-gradient(105deg, transparent 40%, rgba(255, 255, 255, 0.8) 50%, transparent 60%), linear-gradient(90deg, var(--fc, #888888) 0 32%, var(--fc2, var(--fc, #888888)) 50% 82%, var(--fc, #888888) 100%); background-size: 250% 100%, 44px 100%; background-repeat: no-repeat, repeat; animation: ttshine 4s linear infinite; } |
| 69 |
@keyframes ttshine { 0% { background-position: 100% 0, 0 0; } 45% { background-position: 0% 0, 19.8px 0; } 100% { background-position: 0% 0, 44px 0; } } |
| 70 |
.swatch.rainbow.silk { background-image: linear-gradient(105deg, transparent 40%, rgba(255, 255, 255, 0.8) 50%, transparent 60%), conic-gradient(#f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00); background-size: 250% 100%, 100% 100%; background-repeat: no-repeat; background-position: 100% 0; animation: shine 2s ease-in-out infinite, huespin 6s linear infinite; } |
| 71 |
/* Transparent / clear filament: the color is laid at 55% opacity over an |
| 72 |
image-editor-style checkerboard, which shows through the ring. On browsers |
| 73 |
without color-mix() the rule is dropped and the swatch stays solid. */ |
| 74 |
.swatch.transparent { background-image: linear-gradient(color-mix(in srgb, var(--fc, #888888) 55%, transparent), color-mix(in srgb, var(--fc, #888888) 55%, transparent)), repeating-conic-gradient(#a9a9b0 0% 25%, #f2f2f5 0% 50%); background-size: 100% 100%, 10px 10px; } |
| 75 |
.swatch.rainbow.transparent { background-image: conic-gradient(rgba(255, 0, 0, 0.55), rgba(255, 255, 0, 0.55), rgba(0, 255, 0, 0.55), rgba(0, 255, 255, 0.55), rgba(0, 0, 255, 0.55), rgba(255, 0, 255, 0.55), rgba(255, 0, 0, 0.55)), repeating-conic-gradient(#a9a9b0 0% 25%, #f2f2f5 0% 50%); background-size: 100% 100%, 10px 10px; } |
| 76 |
.swatch.silk.transparent { background-image: linear-gradient(105deg, transparent 40%, rgba(255, 255, 255, 0.8) 50%, transparent 60%), linear-gradient(color-mix(in srgb, var(--fc, #888888) 55%, transparent), color-mix(in srgb, var(--fc, #888888) 55%, transparent)), repeating-conic-gradient(#a9a9b0 0% 25%, #f2f2f5 0% 50%); background-size: 250% 100%, 100% 100%, 10px 10px; background-repeat: no-repeat, no-repeat, repeat; background-position: 100% 0, 0 0, 0 0; } |
| 77 |
/* Transparent + two-tone: the drifting gradient is laid at 55% opacity over |
| 78 |
the checkerboard; the keyframes pin the checker layer at 0 0 so only the |
| 79 |
color layer moves. */ |
| 80 |
.swatch.transparent.twotone { background-image: linear-gradient(90deg, color-mix(in srgb, var(--fc, #888888) 55%, transparent) 0 32%, color-mix(in srgb, var(--fc2, var(--fc, #888888)) 55%, transparent) 50% 82%, color-mix(in srgb, var(--fc, #888888) 55%, transparent) 100%), repeating-conic-gradient(#a9a9b0 0% 25%, #f2f2f5 0% 50%); background-size: 44px 100%, 10px 10px; animation: ttclear 4s linear infinite; } |
| 81 |
@keyframes ttclear { to { background-position: 44px 0, 0 0; } } |
| 82 |
/* Sparkle / glitter (galaxy-style filament): the swatch's <i class="spark"> |
| 83 |
child holds tiny white flecks on three differently sized tiles drifting in |
| 84 |
different directions (each shift a whole number of tiles, so the loop is |
| 85 |
seamless) while the layer's opacity twinkles. Being a child element it |
| 86 |
stacks with any ring style for free; the hub (::before, z-index 1) and pie |
| 87 |
(::after, z-index 2) stay on top, masking the flecks to the ring. */ |
| 88 |
.swatch .spark { display: none; } |
| 89 |
.swatch.sparkle .spark { display: block; position: absolute; inset: 0; border-radius: 50%; background-image: radial-gradient(circle at 3px 4px, rgba(255, 255, 255, 0.95) 0 0.8px, transparent 1.6px), radial-gradient(circle at 9px 12px, rgba(255, 255, 255, 0.75) 0 0.7px, transparent 1.4px), radial-gradient(circle at 14px 6px, rgba(255, 255, 255, 0.9) 0 0.6px, transparent 1.3px); background-size: 13px 13px, 17px 17px, 19px 19px; animation: sparkdrift 7s linear infinite, twinkle 2.1s ease-in-out infinite; } |
| 90 |
@keyframes sparkdrift { to { background-position: 13px 26px, -17px 17px, 19px -19px; } } |
| 91 |
@keyframes twinkle { 0%, 100% { opacity: 1; } 50% { opacity: 0.45; } } |
| 92 |
@media (prefers-reduced-motion: reduce) { .swatch, .swatch .spark { animation: none !important; } } |
| 93 |
.muted { color: var(--muted); font-size: 14px; overflow-wrap: anywhere; } |
| 94 |
.error { color: var(--danger); font-weight: 600; } |
| 95 |
.flash { background: var(--accent); color: #fff; border-radius: 10px; padding: 10px 12px; margin: 10px 0; } |
| 96 |
.badge { font-size: 12px; border: 1px solid var(--border); border-radius: 6px; padding: 1px 6px; color: var(--muted); white-space: nowrap; } |
| 97 |
.bar { height: 6px; background: var(--border); border-radius: 3px; overflow: hidden; margin-top: 8px; } |
| 98 |
.bar i { display: block; height: 100%; background: var(--accent); } |
| 99 |
.rowlinks { display: flex; flex-direction: column; gap: 10px; text-align: right; } |
| 100 |
.rowlinks a { text-decoration: none; font-weight: 600; } |
| 101 |
label { display: block; margin: 10px 0 3px; font-weight: 600; font-size: 14px; } |
| 102 |
form p { margin: 6px 0; } |
| 103 |
input, select, textarea { width: 100%; min-height: 44px; padding: 8px 10px; font: inherit; color: inherit; background: var(--card); border: 1px solid var(--border); border-radius: 10px; } |
| 104 |
input:focus, select:focus, textarea:focus { outline: 2px solid var(--accent); border-color: transparent; } |
| 105 |
input[type=checkbox] { width: 24px; height: 24px; min-height: 0; accent-color: var(--accent); margin: 0; } |
| 106 |
input[type=color] { width: 64px; padding: 4px; flex-shrink: 0; } |
| 107 |
.check { display: flex; align-items: center; gap: 10px; font-weight: 400; font-size: 16px; min-height: 44px; margin: 4px 0; } |
| 108 |
.check-row { display: flex; flex-wrap: wrap; gap: 2px 24px; margin: 6px 0; } |
| 109 |
.check-row .check { margin: 0; } |
| 110 |
button, .btn { display: inline-block; min-height: 44px; padding: 10px 18px; font: inherit; font-weight: 600; color: #fff; background: var(--accent); border: 0; border-radius: 10px; cursor: pointer; text-decoration: none; text-align: center; } |
| 111 |
button.secondary, .btn.secondary { background: transparent; color: var(--accent); border: 1px solid var(--border); } |
| 112 |
button.danger { background: var(--danger); } |
| 113 |
.actions { display: flex; gap: 10px; margin: 16px 0; flex-wrap: wrap; } |
| 114 |
.bulkgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 0 14px; } |
| 115 |
.bulkpick { display: flex; align-items: center; margin: 0; flex-shrink: 0; } |
| 116 |
.bulkpick input { width: 26px; height: 26px; } |
| 117 |
/* Quick-pick pills for the common spool amounts (1 kg / 500 g / 250 g / 100 g). */ |
| 118 |
.chips { display: flex; gap: 8px; flex-wrap: wrap; margin: 8px 0 4px; } |
| 119 |
.chip { min-height: 36px; padding: 4px 14px; font-size: 15px; background: transparent; color: var(--accent); border: 1px solid var(--border); border-radius: 999px; } |
| 120 |
.chip.active { background: var(--accent); color: #fff; border-color: var(--accent); } |
| 121 |
/* Wraps for the same reason the nav does: the sort select plus the view and |
| 122 |
filter buttons overrun a 320px screen otherwise. */ |
| 123 |
.sortbar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 0 0 10px; } |
| 124 |
.sortbar label { margin: 0; color: var(--muted); } |
| 125 |
.sortbar select { width: auto; min-height: 36px; padding: 6px 10px; } |
| 126 |
.btn-sm { min-height: 36px; padding: 6px 14px; font-size: 15px; } |
| 127 |
/* Color grid view: per-type sections of mini swatches; tapping one reveals |
| 128 |
that spool's full card below the row it was tapped from (app.js moves the |
| 129 |
card into the grid; without JS it stays after the section). */ |
| 130 |
.typehead { font-size: 13px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); margin: 18px 0 8px; } |
| 131 |
.colorgrid { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 10px; } |
| 132 |
/* The button, not the swatch, is the grid cell: it holds one uniform size so |
| 133 |
rows and columns still line up when a small spool draws a smaller circle. */ |
| 134 |
.gswatch { display: flex; align-items: center; justify-content: center; min-height: 0; width: calc(var(--mini) + 4px); height: calc(var(--mini) + 4px); padding: 0; background: none; border: 0; border-radius: 50%; } |
| 135 |
.gswatch.open .swatch { outline: 2px solid var(--accent); outline-offset: 1px; } |
| 136 |
.swatch.mini { width: var(--mini); height: var(--mini); } |
| 137 |
/* Small spools shrink to the same 2:3 the full-size cards use, so the grid |
| 138 |
shows at a glance which spools are 250 g or less. The cell — and the tap |
| 139 |
target — stays put; only the circle inside it gets smaller. */ |
| 140 |
.swatch.mini.small { width: calc(var(--mini) * 2 / 3); height: calc(var(--mini) * 2 / 3); margin-left: 0; } |
| 141 |
/* Minis are the grid's tap targets, so at 32px the cell is a 36px target — |
| 142 |
under the 44px everything else here keeps. 48px gives a 52px target and |
| 143 |
still fits six per row on a 390px screen. */ |
| 144 |
@media (max-width: 480px) { :root { --mini: 48px; } } |
| 145 |
/* An opened card is moved into the grid, where it takes a row of its own. */ |
| 146 |
.colorgrid > .card { flex: 0 0 100%; margin: 0; } |
| 147 |
/* Filter panel on the inventory list. */ |
| 148 |
.filtersec { margin: 0 0 8px; } |
| 149 |
.filtersec > strong { display: block; font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); margin-bottom: 2px; } |
| 150 |
.filterpanel .check { min-height: 34px; margin: 0; } |
| 151 |
/* Stats page: KPI tiles and the per-material breakdown. */ |
| 152 |
.statgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px; margin: 0 0 10px; } |
| 153 |
.stat { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 12px 14px; } |
| 154 |
.stat b { display: block; font-size: 24px; line-height: 1.15; margin-bottom: 4px; } |
| 155 |
.stat span { color: var(--muted); font-size: 13px; } |
| 156 |
.matrow { margin: 12px 0; } |
| 157 |
.matrow .row { justify-content: space-between; } |
| 158 |
.matrow .bar { margin-top: 6px; } |
| 159 |
.statline { text-align: center; color: var(--muted); font-size: 14px; margin: 18px 0 0; } |
| 160 |
.drop { display: block; border: 2px dashed var(--border); border-radius: 12px; padding: 26px 14px; text-align: center; color: var(--muted); font-weight: 400; cursor: pointer; } |
| 161 |
.drop.hover { border-color: var(--accent); color: var(--accent); } |
| 162 |
video { width: 100%; border-radius: 12px; margin-top: 10px; } |
| 163 |
#camPanel .actions { margin: 8px 0; } |
| 164 |
#camVideo { max-height: 42vh; object-fit: cover; margin-top: 0; } |
| 165 |
|