tweakit 0.5.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/AGENTS.md +32 -30
  2. package/README.md +55 -29
  3. package/THIRD-PARTY-NOTICES.md +0 -1
  4. package/dist/tweaks/auto.js +2 -0
  5. package/dist/tweaks/auto.js.map +6 -0
  6. package/dist/tweaks/bezier-S2BOXQBM.js +2 -0
  7. package/dist/tweaks/bezier-S2BOXQBM.js.map +6 -0
  8. package/dist/tweaks/chunk-3MZAAMC2.js +2 -0
  9. package/dist/tweaks/chunk-3MZAAMC2.js.map +6 -0
  10. package/dist/tweaks/chunk-ETB35KFV.js +2 -0
  11. package/dist/tweaks/chunk-ETB35KFV.js.map +6 -0
  12. package/dist/tweaks/chunk-YB5GKWZI.js +2 -0
  13. package/dist/tweaks/chunk-YB5GKWZI.js.map +6 -0
  14. package/dist/tweaks/clip-A3VI53BK.js +2 -0
  15. package/dist/tweaks/clip-A3VI53BK.js.map +6 -0
  16. package/dist/tweaks/color-UGYMAF3W.js +2 -0
  17. package/dist/tweaks/color-UGYMAF3W.js.map +6 -0
  18. package/dist/tweaks/core.js +2 -1
  19. package/dist/tweaks/core.js.map +6 -0
  20. package/dist/tweaks/fold-GY47HIQA.js +2 -0
  21. package/dist/tweaks/fold-GY47HIQA.js.map +6 -0
  22. package/dist/tweaks/gradient-BTX25WSH.js +2 -0
  23. package/dist/tweaks/gradient-BTX25WSH.js.map +6 -0
  24. package/dist/tweaks/heavy-NPFIGHRM.js +2 -0
  25. package/dist/tweaks/heavy-NPFIGHRM.js.map +6 -0
  26. package/dist/tweaks/image-HTQQ4WX6.js +4 -0
  27. package/dist/tweaks/image-HTQQ4WX6.js.map +6 -0
  28. package/dist/tweaks/image-store-QD5SMZXG.js +2 -0
  29. package/dist/tweaks/image-store-QD5SMZXG.js.map +6 -0
  30. package/dist/tweaks/interval-BHPI4LRN.js +2 -0
  31. package/dist/tweaks/interval-BHPI4LRN.js.map +6 -0
  32. package/dist/tweaks/markup-QM53TQJZ.js +2 -0
  33. package/dist/tweaks/markup-QM53TQJZ.js.map +6 -0
  34. package/dist/tweaks/monitor-722WIYGW.js +3 -0
  35. package/dist/tweaks/monitor-722WIYGW.js.map +6 -0
  36. package/dist/tweaks/motion-HMCZK6HJ.js +2 -0
  37. package/dist/tweaks/motion-HMCZK6HJ.js.map +6 -0
  38. package/dist/tweaks/plot-D3NQ6XNN.js +3 -0
  39. package/dist/tweaks/plot-D3NQ6XNN.js.map +6 -0
  40. package/dist/tweaks/point-XDXKMDXH.js +2 -0
  41. package/dist/tweaks/point-XDXKMDXH.js.map +6 -0
  42. package/dist/tweaks/presets-EUWNSCUW.js +2 -0
  43. package/dist/tweaks/presets-EUWNSCUW.js.map +6 -0
  44. package/dist/tweaks/shadow-AM7VSRNS.js +2 -0
  45. package/dist/tweaks/shadow-AM7VSRNS.js.map +6 -0
  46. package/dist/tweaks/shadow-root-RYUV3AGS.js +3 -0
  47. package/dist/tweaks/shadow-root-RYUV3AGS.js.map +6 -0
  48. package/dist/tweaks/sides-JWIURDA4.js +2 -0
  49. package/dist/tweaks/sides-JWIURDA4.js.map +6 -0
  50. package/dist/tweaks/spring-BF5GBTPM.js +2 -0
  51. package/dist/tweaks/spring-BF5GBTPM.js.map +6 -0
  52. package/dist/tweaks/tabs-PRVUUZXZ.js +2 -0
  53. package/dist/tweaks/tabs-PRVUUZXZ.js.map +6 -0
  54. package/dist/tweaks.css +1 -1
  55. package/dist/tweaks.js +7 -3
  56. package/dist/tweaks.js.map +6 -0
  57. package/dist/types/tweaks/easing.d.ts +8 -11
  58. package/dist/types/tweaks/enhance.d.ts +3 -2
  59. package/dist/types/tweaks/panel.d.ts +6 -4
  60. package/dist/types/tweaks/schema.d.ts +4 -8
  61. package/dist/types/tweaks/standalone.d.ts +3 -3
  62. package/dist/types/tweaks/types.d.ts +499 -116
  63. package/package.json +30 -5
  64. package/src/tweaks/auto.ts +16 -0
  65. package/src/tweaks/clip.ts +156 -0
  66. package/src/tweaks/controls/basic.ts +727 -0
  67. package/src/tweaks/controls/bezier.ts +204 -0
  68. package/src/tweaks/controls/color.ts +861 -0
  69. package/src/tweaks/controls/gradient.ts +419 -0
  70. package/src/tweaks/controls/image.ts +555 -0
  71. package/src/tweaks/controls/interval.ts +189 -0
  72. package/src/tweaks/controls/monitor.ts +211 -0
  73. package/src/tweaks/controls/motion.ts +316 -0
  74. package/src/tweaks/controls/plot.ts +440 -0
  75. package/src/tweaks/controls/point.ts +208 -0
  76. package/src/tweaks/controls/shadow.ts +156 -0
  77. package/src/tweaks/controls/sides.ts +196 -0
  78. package/src/tweaks/controls/spring.ts +308 -0
  79. package/src/tweaks/controls/tabs.ts +68 -0
  80. package/src/tweaks/core.ts +9 -0
  81. package/src/tweaks/easing.ts +118 -0
  82. package/src/tweaks/enhance.ts +35 -0
  83. package/src/tweaks/feedback.ts +218 -0
  84. package/src/tweaks/fold.ts +33 -0
  85. package/src/tweaks/heavy.ts +626 -0
  86. package/src/tweaks/icons.ts +26 -0
  87. package/src/tweaks/image-store.ts +189 -0
  88. package/src/tweaks/lazy.ts +63 -0
  89. package/src/tweaks/markup.ts +317 -0
  90. package/src/tweaks/panel.ts +1693 -0
  91. package/src/tweaks/presets.ts +85 -0
  92. package/src/tweaks/schema.ts +351 -0
  93. package/src/tweaks/shadow-root.ts +178 -0
  94. package/src/tweaks/shared.ts +1137 -0
  95. package/src/tweaks/single.ts +22 -0
  96. package/src/tweaks/standalone.ts +179 -0
  97. package/src/tweaks/types.ts +1027 -0
  98. package/src/tweaks/undo.ts +104 -0
  99. package/src/tweaks.css +965 -0
  100. package/src/wide-gamut.ts +658 -0
  101. package/dist/tweaks/bezier-2RABC6LD.js +0 -1
  102. package/dist/tweaks/chunk-4ATI34PB.js +0 -1
  103. package/dist/tweaks/chunk-7OSCMZ2J.js +0 -1
  104. package/dist/tweaks/chunk-W3JNIJ2F.js +0 -1
  105. package/dist/tweaks/clip-ECGCECEC.js +0 -1
  106. package/dist/tweaks/colour-2RE3FQ5E.js +0 -1
  107. package/dist/tweaks/fold-UGN7TXGX.js +0 -1
  108. package/dist/tweaks/gradient-DB6XTDHO.js +0 -1
  109. package/dist/tweaks/heavy-PKOMHB3C.js +0 -1
  110. package/dist/tweaks/image-7MCLBOZ3.js +0 -1
  111. package/dist/tweaks/interval-L2E4L3AZ.js +0 -1
  112. package/dist/tweaks/markup-ZJJ6CCWM.js +0 -1
  113. package/dist/tweaks/monitor-IEK742PG.js +0 -2
  114. package/dist/tweaks/motion-N5UNVR55.js +0 -1
  115. package/dist/tweaks/plot-VVTOY3RA.js +0 -2
  116. package/dist/tweaks/point-YMMHSLO7.js +0 -1
  117. package/dist/tweaks/shadow-IKYOV3VN.js +0 -1
  118. package/dist/tweaks/sides-FH227Z54.js +0 -1
  119. package/dist/tweaks/spring-3PKAFIH7.js +0 -1
  120. package/dist/tweaks/tabs-BKK5NIIY.js +0 -1
package/src/tweaks.css ADDED
@@ -0,0 +1,965 @@
1
+ /* Tweaks — a dependency-free real-time parameter panel. One stylesheet, namespaced
2
+ * under `twk-` / `--twk-`, themed entirely through CSS custom properties. */
3
+ /* Design tokens, for the panel and for every portaled node (all carry .twk-portal). Each
4
+ * color is one light-dark(light, dark) pair, resolved by the color-scheme of the element that
5
+ * paints it (see "Scheme resolution" below), so a token keeps one name and one meaning in both
6
+ * schemes and a theme override wins in both. A shadow panel (opts.shadow) and its portals carry .twk-in-shadow and skip
7
+ * this rule: there the build copies it onto the kit's own :host, where the page's rules on that host override it. Keep
8
+ * the selector as written; build.mjs finds the rule by it. */
9
+ :where(.twk-panel, .twk-portal):not(.twk-in-shadow) {
10
+ /* Surfaces — faint films over the base. Light: items a step brighter than the off-white
11
+ * base; hover/active darken for contrast. */
12
+ --twk-surface: light-dark(rgba(255, 255, 255, 0.82), rgba(255, 255, 255, 0.04));
13
+ --twk-surface-hover: light-dark(rgba(0, 0, 0, 0.04), rgba(255, 255, 255, 0.075));
14
+ --twk-surface-active: light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.13));
15
+ /* Text — secondary/tertiary style 10–12px text, so they sit at WCAG 4.5:1 on the base. */
16
+ --twk-text-root: light-dark(#1a1a1a, #fafafa);
17
+ --twk-text-section: light-dark(rgba(0, 0, 0, 0.62), rgba(255, 255, 255, 0.64));
18
+ --twk-text-label: light-dark(rgba(0, 0, 0, 0.58), rgba(255, 255, 255, 0.64));
19
+ --twk-text-focus: light-dark(#000000, #ffffff);
20
+ --twk-text-primary: light-dark(rgba(0, 0, 0, 0.88), rgba(255, 255, 255, 0.92));
21
+ --twk-text-secondary: light-dark(rgba(0, 0, 0, 0.58), rgba(255, 255, 255, 0.56));
22
+ --twk-text-tertiary: light-dark(rgba(0, 0, 0, 0.55), rgba(255, 255, 255, 0.5)); /* light .54 sat at 4.48:1 on the base */
23
+ /* Text drawn on the accent; resolveTheme overrides it to black/white by the accent's
24
+ * luminance when a theme sets `accent`. */
25
+ --twk-on-accent: var(--twk-text-primary);
26
+ /* Borders — faint; the panel leans on its shadow for definition. */
27
+ --twk-border: light-dark(rgba(0, 0, 0, 0.045), rgba(255, 255, 255, 0.045));
28
+ --twk-border-hover: light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.11));
29
+ /* Text-selection wash, not the browser's blue. */
30
+ --twk-selection: light-dark(rgba(0, 0, 0, 0.14), rgba(255, 255, 255, 0.22));
31
+ /* Base — the panel background and recessed insets; opaque, a step brighter than a dark page. */
32
+ --twk-base: light-dark(#f3f3f3, #242424);
33
+ --twk-checker: repeating-conic-gradient(#6b6b6b 0% 25%, #9a9a9a 0% 50%) 0 0 / 8px 8px; /* the alpha checker under swatches and strips */
34
+ --twk-dropdown-bg: light-dark(#f6f6f6, #292929);
35
+ --twk-radius: 8px;
36
+ --twk-row-height: 32px;
37
+ --twk-width: 256px; /* the panel's width (theme `width`) */
38
+ /* A shadow isn't a color, so light-dark() goes on each layer's color. */
39
+ --twk-shadow-dropdown: 0 12px 32px -8px light-dark(rgba(0, 0, 0, 0.14), rgba(0, 0, 0, 0.6));
40
+ /* Elevation — on dark a 1px rim-light, since a black shadow alone can't separate the
41
+ * panel; on light a dark hairline edge, as a bright rim reads oddly there. -lifted deepens
42
+ * it while floating or dragged. The two schemes' drop layers differ in geometry, not just
43
+ * color, so each scheme's pair is listed and goes transparent in the other: a transparent
44
+ * layer paints nothing, and both lists keep one length, so the lift still interpolates. */
45
+ --twk-shadow-panel: 0 0 0 1px light-dark(var(--twk-border), rgba(255, 255, 255, 0.06)),
46
+ 0 8px 28px -8px light-dark(transparent, rgba(0, 0, 0, 0.5)), 0 18px 48px -16px light-dark(transparent, rgba(0, 0, 0, 0.38)),
47
+ 0 2px 8px -2px light-dark(rgba(0, 0, 0, 0.08), transparent), 0 18px 44px -12px light-dark(rgba(0, 0, 0, 0.16), transparent);
48
+ --twk-shadow-panel-lifted: 0 0 0 1px light-dark(var(--twk-border-hover), rgba(255, 255, 255, 0.08)),
49
+ 0 12px 32px -8px light-dark(transparent, rgba(0, 0, 0, 0.55)), 0 28px 64px -16px light-dark(transparent, rgba(0, 0, 0, 0.45)),
50
+ 0 6px 16px -3px light-dark(rgba(0, 0, 0, 0.12), transparent), 0 30px 70px -16px light-dark(rgba(0, 0, 0, 0.2), transparent);
51
+ /* Success (the copy check) and danger (the plot's invalid cue). */
52
+ --twk-success: light-dark(#15803d, #86efac);
53
+ --twk-danger: light-dark(#c23232, #ff5f5f);
54
+ /* Type — the system UI font, unbound from host tokens; mono is declared so the fontMono
55
+ * theme lever has a token to land on. */
56
+ --twk-font-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
57
+ --twk-font-mono: ui-monospace, "SF Mono", Menlo, monospace;
58
+ /* Motion — one shared easing vocabulary: --out decelerates, --spring settles, --pop
59
+ * overshoots on release, --inout is symmetric for longer mechanical moves. */
60
+ --twk-ease-out: cubic-bezier(0.25, 1, 0.5, 1);
61
+ --twk-ease-spring: cubic-bezier(0.22, 1, 0.36, 1);
62
+ --twk-ease-pop: cubic-bezier(0.34, 1.56, 0.64, 1);
63
+ --twk-ease-inout: cubic-bezier(0.4, 0, 0.2, 1);
64
+ /* --fold is a spring (visualDuration 0.35s, bounce 0.1) sampled into linear(); it starts
65
+ * from rest, so folder children dissolve in step with the height. Pairs with 0.35s. */
66
+ --twk-ease-fold: linear(0, 0.077, 0.237, 0.411, 0.570, 0.699, 0.797, 0.869, 0.918, 0.952, 0.973, 0.986, 0.994, 0.998, 1);
67
+ /* Press: a dip in three tiers by size, so every control's edge moves about 1.5–2px. */
68
+ --twk-press-icon: 0.88; --twk-press-chip: 0.95; --twk-press-row: 0.985;
69
+ }
70
+ /* A shadow panel's first styles land at once (shadow-root.ts): held still meanwhile, nothing animates from the unstyled
71
+ * layout. */
72
+ :host([data-twk-settling]) *, :host([data-twk-settling]) *::before, :host([data-twk-settling]) *::after { transition: none !important; }
73
+ .twk-panel, .twk-portal {
74
+ font-family: var(--twk-font-sans);
75
+ /* Buttons use `font: inherit`, so a host's global line-height would ride into every
76
+ * control; pin it. */
77
+ line-height: normal;
78
+ -webkit-font-smoothing: antialiased;
79
+ -moz-osx-font-smoothing: grayscale;
80
+ -webkit-tap-highlight-color: transparent;
81
+ /* Picks every light-dark() token, and the UA internals (scrollbars, carets) with them; set
82
+ * here, so the host's own color-scheme never reaches the panel. */
83
+ color-scheme: light dark;
84
+ box-sizing: border-box;
85
+ }
86
+ .twk-panel {
87
+ width: var(--twk-width);
88
+ background: var(--twk-base);
89
+ box-shadow: var(--twk-shadow-panel);
90
+ /* Concentric with the controls: control radius + padding, 8 + 10 = 18. */
91
+ border-radius: calc(var(--twk-radius) + 10px);
92
+ /* No backdrop-filter: it would make the panel the containing block for the
93
+ * popovers' position:fixed, detaching them from their triggers. */
94
+ padding: 10px;
95
+ }
96
+ .twk-panel *,
97
+ .twk-panel *::before,
98
+ .twk-panel *::after { box-sizing: border-box; }
99
+
100
+ :is(.twk-panel, .twk-portal) ::selection { background: var(--twk-selection); color: var(--twk-text-focus); }
101
+
102
+ /* Floating mode. The z ladder (panel 99990 < popovers 99999 < tip 100000 < grab
103
+ * guide 100001) sits above the 100–9999 band host sticky headers occupy. The placement
104
+ * (panel.ts apply() writes the --twk-float-* insets and height cap), z-index and shadow sit at
105
+ * plain .twk-panel specificity, so a host class loaded after this sheet overrides any of them. */
106
+ .twk-panel[data-mode="floating"] { position: fixed; }
107
+ .twk-panel:where([data-mode="floating"]) { left: var(--twk-float-x); top: var(--twk-float-y); max-height: var(--twk-float-max-h); z-index: 99990; box-shadow: var(--twk-shadow-panel-lifted); }
108
+ .twk-panel:where(.is-raised) { z-index: 99991; } /* the floating panel touched last (panel.ts raise()) */
109
+ /* The lift into the floating shadow eases on a draggable panel only: a pinned one starts floated, with nothing to ease,
110
+ * and an inline one keeps its shadow still. */
111
+ .twk-panel[data-draggable="true"][data-mode="floating"] { transition: box-shadow 0.25s var(--twk-ease-out); }
112
+ /* Registered as a <length>, so a host's --twk-float-gap in rem, vh or calc() computes to px for panel.ts to read; the
113
+ * negative initial value reads as unset. */
114
+ @property --twk-float-gap { syntax: "<length>"; inherits: true; initial-value: -1px; }
115
+ /* The footprint a lifted panel leaves in its old slot, so the host layout holds still. */
116
+ .twk-lift-slot { display: block; flex: none; pointer-events: none; }
117
+ /* The header is the drag handle. The negative margin (canceled by an equal padding-top)
118
+ * stretches its hit area up through the panel's top padding to the edge. Draggable panels
119
+ * only: a pinned floating one keeps a plain cursor, and a swipe on its header scrolls it. */
120
+ .twk-panel[data-draggable="true"] > .twk-header,
121
+ .twk-panel[data-draggable="true"] > .twk-header .twk-header-toggle { cursor: grab; }
122
+ .twk-panel[data-draggable="true"] > .twk-header { touch-action: none; margin-top: -10px; padding-top: 10px; }
123
+ /* A drop near an edge eases the rest of the way to the margin (panel.ts endDrag()). */
124
+ .twk-panel.is-snapping { transition: left 0.32s var(--twk-ease-spring), top 0.32s var(--twk-ease-spring), right 0.32s var(--twk-ease-spring), bottom 0.32s var(--twk-ease-spring); }
125
+ .twk-panel.is-dragging { -webkit-user-select: none; user-select: none; box-shadow: var(--twk-shadow-panel-lifted); z-index: 99991; }
126
+ /* A floating panel scrolls, header and all, inside the height cap its position sets (--twk-float-max-h). The scrollbar
127
+ * is hidden: wheel, trackpad, touch and keyboard focus still scroll it. A slider covers its row and takes every touch;
128
+ * here a vertical swipe pans, a sideways one scrubs. */
129
+ .twk-panel[data-mode="floating"] { overflow-y: auto; overscroll-behavior: contain; scrollbar-width: none; }
130
+ .twk-panel[data-mode="floating"]::-webkit-scrollbar { display: none; } /* Safari before scrollbar-width */
131
+ .twk-panel[data-mode="floating"] .twk-slider { touch-action: pan-y; }
132
+ .twk-panel.is-dragging > .twk-header,
133
+ .twk-panel.is-dragging > .twk-header .twk-header-toggle { cursor: grabbing; }
134
+
135
+ /* Every :hover rule sits inside `@media (hover: hover) and (pointer: fine)`: a touch
136
+ * screen keeps :hover on after a tap, so an unguarded hover style sticks. */
137
+ /* A pane drags by its edge (popover() in shared.ts); its parts keep their own cursors. */
138
+ :is(.twk-color-pop, .twk-presets-menu) { cursor: grab; }
139
+ :is(.twk-color-pop, .twk-presets-menu) > * { cursor: auto; }
140
+ :is(.twk-color-pop, .twk-presets-menu).is-moving, :is(.twk-color-pop, .twk-presets-menu).is-moving * { cursor: grabbing; }
141
+
142
+ /* ── Header (root folder title) ── */
143
+ .twk-header {
144
+ display: flex;
145
+ align-items: center;
146
+ justify-content: space-between;
147
+ gap: 8px;
148
+ padding-bottom: 9px;
149
+ margin-bottom: 8px;
150
+ border-bottom: 1px solid var(--twk-border); /* a surface film would read white on a light panel */
151
+ }
152
+ .twk-title {
153
+ font-size: 14px;
154
+ font-weight: 500;
155
+ line-height: 17px; /* not normal: a toolbar-less header is 37px whatever the font (44 with the toolbar's 24px buttons) */
156
+ color: var(--twk-text-root);
157
+ min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
158
+ }
159
+ .twk-header-toggle {
160
+ display: flex; align-items: center; gap: 6px; flex: 1; min-width: 0;
161
+ background: none; border: none; padding: 0; margin: 0; cursor: pointer; color: inherit; font: inherit; text-align: left;
162
+ }
163
+ .twk-chevron { width: 15px; height: 15px; flex-shrink: 0; color: var(--twk-text-section); transition: transform 0.2s var(--twk-ease-out), color 0.15s; }
164
+ .twk-panel.is-collapsed .twk-chevron { transform: rotate(-90deg); }
165
+ .twk-toolbar { display: flex; align-items: center; gap: 4px; }
166
+ /* Hint marker. pointer-events:auto overrides the slider label's pointer-events:none. */
167
+ .twk-hint { display: inline-flex; align-items: center; justify-content: center; width: 14px; height: 14px; margin-left: 4px; padding: 0; vertical-align: -2px; flex: none; background: none; border: none; color: var(--twk-text-tertiary); cursor: help; pointer-events: auto; transition: color 0.15s, transform 0.18s var(--twk-ease-spring); }
168
+ .twk-hint:focus-visible { color: var(--twk-text-secondary); transform: scale(1.2); }
169
+ @media (hover: hover) and (pointer: fine) {
170
+ .twk-hint:hover { color: var(--twk-text-secondary); transform: scale(1.2); }
171
+ }
172
+ .twk-hint:focus-visible { outline: 2px solid var(--twk-accent, var(--twk-text-focus)); outline-offset: 1px; border-radius: 999px; }
173
+ .twk-hint svg { width: 13px; height: 13px; display: block; }
174
+ .twk-tip { position: fixed; z-index: 100000; max-width: 220px; padding: 8px 12px; font-size: 12px; line-height: 1.4; color: var(--twk-text-primary); background: var(--twk-dropdown-bg); border: 1px solid var(--twk-border); border-radius: 8px; box-shadow: var(--twk-shadow-dropdown); pointer-events: none; opacity: 0; transform: translateY(2px); transition: opacity 0.12s, transform 0.12s; }
175
+ .twk-tip.is-open { opacity: 1; transform: none; }
176
+ /* The screen-reader status region (announce() in shared.ts): in the page, drawn as nothing. */
177
+ .twk-sr, .twk-image-live { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; border: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
178
+
179
+ /* Collapse: grid-rows 1fr→0fr animates the body height. foldBody (shared.ts) animates only the visible part; this is the fallback. */
180
+ .twk-body { display: grid; grid-template-rows: 1fr; transition: grid-template-rows 0.35s var(--twk-ease-fold); }
181
+ /* Clip y only, so a slider rubber-banding past its end still shows. Must be `clip`,
182
+ * not `hidden`: `hidden` on one axis forces the other to compute to `auto`. */
183
+ /* The 3px padding, offset by an equal negative margin so nothing moves, sets the clip edges out far enough not to cut a
184
+ * first or last row's pick band (3px past the row). A collapsed panel drops the pair, or its 0fr row would show 3px of
185
+ * the first row. */
186
+ .twk-body > .twk-controls { overflow-x: visible; overflow-y: clip; min-height: 0; min-width: 0; padding-block: 3px; margin-block: -3px; }
187
+ .twk-panel.is-collapsed .twk-body > .twk-controls { padding-block: 0; margin-block: 0; }
188
+ .twk-panel.is-collapsed .twk-body { grid-template-rows: 0fr; }
189
+ .twk-header { transition: margin-bottom 0.35s var(--twk-ease-fold), padding-bottom 0.35s var(--twk-ease-fold), border-color 0.35s var(--twk-ease-fold); }
190
+ /* Collapsed, the header is the whole panel; drop its separator so the padding is even. */
191
+ .twk-panel.is-collapsed .twk-header { margin-bottom: 0; padding-bottom: 0; border-color: transparent; }
192
+ /* A first folder brings its own spacing; the header's 8px gap would stack on it. */
193
+ .twk-header:has(+ .twk-body > .twk-controls > .twk-folder:first-child) { margin-bottom: 0; }
194
+ .twk-toolbar-btn {
195
+ position: relative; display: flex; align-items: center; justify-content: center;
196
+ width: 24px; height: 24px; padding: 0; flex-shrink: 0;
197
+ background: var(--twk-surface); border: none; border-radius: calc(var(--twk-radius) - 2px);
198
+ color: var(--twk-text-label); cursor: pointer;
199
+ will-change: transform; /* keep the layer standing (Safari pixel-snap) */
200
+ transition: background 0.15s, color 0.15s, transform 0.24s var(--twk-ease-pop);
201
+ }
202
+ @media (hover: hover) and (pointer: fine) {
203
+ .twk-toolbar-btn:hover { background: var(--twk-surface-hover); color: var(--twk-text-primary); }
204
+ }
205
+ .twk-toolbar-btn:disabled { cursor: default; } /* the lazy window is "not yet", not "never" */
206
+ /* The press dip shrinks the box, so a cursor near the edge would lose :hover mid-press;
207
+ * ::after keeps the resting footprint hot ((24+4)·0.88 ≈ 24.6 ≥ 24). */
208
+ .twk-toolbar-btn::after { content: ""; position: absolute; inset: -2px; }
209
+ .twk-toolbar-btn:active { transform: scale(var(--twk-press-icon)); transition: transform 0.08s ease-out; }
210
+ .twk-toolbar-btn svg { width: 14px; height: 14px; display: block; }
211
+ /* Copy ⇄ check cross-fade. Safari: will-change and a resting blur(0), never none —
212
+ * WebKit demotes the layer when a transform/filter animation ends and re-snaps the
213
+ * raster to device pixels, which read as a ~1px icon shift. */
214
+ .twk-toolbar-btn__icons { position: relative; display: block; width: 14px; height: 14px; }
215
+ .twk-toolbar-btn__icons svg { position: absolute; inset: 0; will-change: transform, filter; filter: blur(0); transition: opacity 0.25s var(--twk-ease-inout), transform 0.25s var(--twk-ease-inout), filter 0.25s var(--twk-ease-inout); }
216
+ .twk-toolbar-btn__check { opacity: 0; transform: scale(0.25); filter: blur(2px); color: var(--twk-success); }
217
+ .twk-toolbar-btn--swap.is-copied .twk-toolbar-btn__copy { opacity: 0; transform: scale(0.25); filter: blur(2px); }
218
+ .twk-toolbar-btn--swap.is-copied .twk-toolbar-btn__check { opacity: 1; transform: none; filter: blur(0); }
219
+ /* Reset: hover sets --twk-wind, each click adds −360deg to --twk-spin (JS spinReset). A
220
+ * transition, not keyframes, so a second click mid-spin retargets from the current angle. */
221
+ .twk-toolbar-btn--reset svg { --twk-spin: 0deg; --twk-wind: 0deg; will-change: transform; transform: rotate(calc(var(--twk-spin) + var(--twk-wind))); transition: transform 0.34s cubic-bezier(0.34, 1.55, 0.5, 1); }
222
+ @media (hover: hover) and (pointer: fine) {
223
+ .twk-toolbar-btn--reset:not(:disabled):hover svg { --twk-wind: 22deg; }
224
+ }
225
+ .twk-toolbar-btn--reset.is-spinning svg { transition-duration: 0.5s; transition-timing-function: var(--twk-ease-inout); }
226
+ /* Icon parts move on hover (icons.ts): the button sets the distances, the parts read them. */
227
+ .twk-copy-back { transform: translate(calc(-1 * var(--twk-fan, 0px)), calc(-1 * var(--twk-fan, 0px))); }
228
+ .twk-copy-front { transform: translate(var(--twk-fan, 0px), var(--twk-fan, 0px)); }
229
+ .twk-layer-top { transform: translateY(calc(-1 * var(--twk-lift, 0px))); }
230
+ .twk-copy-back, .twk-copy-front, .twk-layer-top { transition: transform 0.3s var(--twk-ease-pop); }
231
+ @media (hover: hover) and (pointer: fine) {
232
+ .twk-toolbar-btn:not(:disabled):hover { --twk-fan: 1px; --twk-lift: 1.75px; } /* a disabled button (the lazy window) holds still */
233
+ }
234
+ .twk-toolbar-btn[aria-expanded="true"] { --twk-lift: 1.75px; }
235
+
236
+ /* Focus rings. Inset inside the controls stack, which clips for the collapse; outset
237
+ * on the chips and handles. :focus-visible matches a clicked text field too, so
238
+ * quietFocus (shared.ts) marks pointer-origin focus with .twk-focus-quiet. */
239
+ .twk-trigger:focus-visible, .twk-select-trigger:focus-visible, .twk-button:focus-visible,
240
+ .twk-seg-btn:focus-visible, .twk-seg[role="switch"]:focus-visible, .twk-header-toggle:focus-visible, .twk-folder-header:focus-visible,
241
+ .twk-slider:focus-visible, .twk-pad:focus-visible, .twk-motion-stage:focus-visible, .twk-sides-link:focus-visible, .twk-radiogrid-btn:focus-visible, .twk-buttongroup-btn:focus-visible, .twk-tabs-tab:focus-visible,
242
+ .twk-image-clear:focus-visible,
243
+ .twk-text:focus-visible:not(.twk-focus-quiet), .twk-num:focus-visible:not(.twk-focus-quiet),
244
+ .twk-color-chan-input:focus-visible:not(.twk-focus-quiet), .twk-color-mode:focus-visible,
245
+ .twk-presets-load:focus-visible, .twk-presets-savebtn:focus-visible, .twk-presets-del:focus-visible, .twk-presets-input:focus-visible:not(.twk-focus-quiet) {
246
+ outline: 2px solid var(--twk-accent, var(--twk-text-focus)); outline-offset: -2px;
247
+ }
248
+ .twk-toolbar-btn:focus-visible { outline: 2px solid var(--twk-accent, var(--twk-text-focus)); outline-offset: 1px; }
249
+ .twk-gradient-stop:focus-visible, .twk-gradient-add:focus-visible, .twk-bezier-handle:focus-visible { outline: 2px solid var(--twk-accent, var(--twk-text-focus)); outline-offset: 2px; }
250
+
251
+ /* ── Controls stack ── */
252
+ .twk-controls { display: flex; flex-direction: column; gap: 6px; }
253
+ /* A flex item's floor is its min-content width; one nowrap child would widen every row. */
254
+ .twk-controls > * { min-width: 0; }
255
+ .twk-cond-hidden { display: none !important; }
256
+ .is-disabled { opacity: 0.4; cursor: not-allowed; }
257
+ .is-disabled * { pointer-events: none; } /* the row stays a target for the not-allowed cursor */
258
+ /* Reset target (double-click). touch-action: manipulation drops double-tap-to-zoom. */
259
+ .twk-resettable { touch-action: manipulation; -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }
260
+ /* Hold-to-reset fill. A transition, so an early release drains it. Doubled class: targets
261
+ * like .twk-slider-value declare their own `transition: color`, which would win on order. */
262
+ .twk-resettable.twk-resettable { transition: color 0.15s, background-color 0.3s ease-out; }
263
+ .twk-resettable.twk-reset-holding { border-radius: calc(var(--twk-radius) - 4px); background-color: var(--twk-surface-active); transition: color 0.15s, background-color 0.5s ease-out; }
264
+
265
+ /* ── Slider ── */
266
+ .twk-slider-wrap { position: relative; height: var(--twk-row-height); }
267
+ .twk-slider {
268
+ position: absolute; inset: 0;
269
+ cursor: pointer; user-select: none; -webkit-user-select: none; overflow: hidden;
270
+ background: var(--twk-surface); border-radius: var(--twk-radius);
271
+ touch-action: none;
272
+ }
273
+ .twk-slider-hashmarks { position: absolute; inset: 0; pointer-events: none; }
274
+ .twk-slider-hashmark {
275
+ position: absolute; top: 50%; width: 1px; height: 8px; border-radius: 999px;
276
+ transform: translateX(-50%) translateY(-50%);
277
+ background: rgba(255, 255, 255, 0); transition: background 200ms;
278
+ }
279
+ .twk-slider.is-active .twk-slider-hashmark { background: var(--twk-border-hover); }
280
+ .twk-slider.is-active .twk-slider-value { color: var(--twk-text-focus); }
281
+ .twk-slider-fill {
282
+ position: absolute; top: 0; bottom: 0; left: 0; width: 0;
283
+ background: var(--twk-surface-active); transition: background 0.15s; pointer-events: none;
284
+ }
285
+ .twk-slider.is-active .twk-slider-fill { background: var(--twk-accent, var(--twk-border-hover)); }
286
+ .twk-slider-handle {
287
+ position: absolute; top: 50%; left: 0; width: 3px; height: 20px;
288
+ border-radius: 999px; background: var(--twk-text-primary);
289
+ transform: translateY(-50%) scaleX(0.25); opacity: 0; pointer-events: none;
290
+ transition: opacity 0.15s, transform 0.2s var(--twk-ease-spring);
291
+ }
292
+ /* .is-hover is set in JS (pointerenter/leave) as a companion to :hover. */
293
+ .twk-slider.is-hover .twk-slider-handle,
294
+ .twk-slider:focus-within .twk-slider-handle { opacity: 0.5; transform: translateY(-50%) scaleX(1); }
295
+ @media (hover: hover) and (pointer: fine) {
296
+ .twk-slider:hover .twk-slider-handle { opacity: 0.5; transform: translateY(-50%) scaleX(1); }
297
+ }
298
+ .twk-slider.is-active .twk-slider-handle { opacity: 0.9; transform: translateY(-50%) scaleX(1); }
299
+ /* Interval handles are focusable; the single slider's are decorative. */
300
+ .twk-slider-handle:focus-visible { opacity: 0.9; transform: translateY(-50%) scaleX(1); outline: 2px solid var(--twk-accent, var(--twk-text-focus)); outline-offset: 2px; }
301
+ /* The dodge: the handle yields under the label or value text. */
302
+ .twk-slider.is-active.is-dodge .twk-slider-handle,
303
+ .twk-slider.is-hover.is-dodge .twk-slider-handle { opacity: 0.1; transform: translateY(-50%) scaleX(1) scaleY(0.75); }
304
+ @media (hover: hover) and (pointer: fine) {
305
+ .twk-slider:hover.is-dodge .twk-slider-handle { opacity: 0.1; transform: translateY(-50%) scaleX(1) scaleY(0.75); }
306
+ }
307
+ /* Interval has two handles, so the dodge is per-handle (the one under the text). */
308
+ .twk-interval.is-active .twk-slider-handle.is-dodge { opacity: 0.1; transform: translateY(-50%) scaleX(1) scaleY(0.75); }
309
+ .twk-slider-label {
310
+ position: absolute; left: 12px; top: 50%; transform: translateY(-50%); /* 12px = the row's left padding */
311
+ max-width: calc(100% - 64px);
312
+ font-size: 12px; font-weight: 500; color: var(--twk-text-label); transition: color 0.15s;
313
+ pointer-events: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
314
+ }
315
+ .twk-interval .twk-slider-label { max-width: calc(100% - 112px); } /* the control tightens this inline on render */
316
+ .twk-slider-value {
317
+ position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
318
+ font-size: 12px; font-weight: 500;
319
+ color: var(--twk-text-label); pointer-events: auto;
320
+ transition: color 0.15s;
321
+ }
322
+ /* The 800ms hover arms editing; the underline makes click-to-type discoverable. */
323
+ .twk-slider-value.is-editable { cursor: text; color: var(--twk-text-focus); text-decoration: underline; text-decoration-color: var(--twk-text-tertiary); text-underline-offset: 3px; }
324
+ .twk-slider-input {
325
+ position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
326
+ min-width: 4ch; max-width: 60%; /* the editor sizes its width to the text it opens on */
327
+ font-size: 12px; font-weight: 500; font-family: var(--twk-font-sans);
328
+ color: var(--twk-text-label); background: transparent; border: none;
329
+ padding: 0; outline: none; text-align: right;
330
+ }
331
+ .twk-slider-input:focus { color: var(--twk-text-focus); }
332
+
333
+ /* ── Labeled row (label + control side by side) ── */
334
+ .twk-row {
335
+ display: flex; align-items: center; justify-content: space-between; gap: 12px;
336
+ height: var(--twk-row-height); padding: 2px 10px 2px 12px;
337
+ background: var(--twk-surface); border-radius: var(--twk-radius);
338
+ }
339
+ .twk-row-label {
340
+ font-size: 12px; font-weight: 500; line-height: 17px; color: var(--twk-text-label); transition: color 0.15s;
341
+ min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
342
+ }
343
+ /* Every row lights its label on hover, never its value. */
344
+ @media (hover: hover) and (pointer: fine) {
345
+ .twk-slider-wrap:not(.is-disabled):hover .twk-slider-label,
346
+ :is(.twk-row, .twk-sides, .twk-radiogrid):not(.is-disabled):hover :is(.twk-row-label, .twk-radiogrid-label),
347
+ :is(.twk-spring, .twk-bezier, .twk-plot):not(.is-disabled):hover :is(.twk-spring-label, .twk-bezier-label, .twk-plot-label) { color: var(--twk-text-primary); }
348
+ }
349
+ /* An explicit `label: ""` hides the element so it takes no gap. */
350
+ .twk-slider-label:empty, .twk-row-label:empty, .twk-select-label:empty, .twk-radiogrid-label:empty, .twk-trigger-label:empty, .twk-fps-label:empty { display: none; }
351
+ .twk-row-label:empty + *, .twk-select-label:empty + *, .twk-trigger-label:empty + * { margin-left: auto; }
352
+
353
+ /* ── Segmented control (also the Off/On toggle) ── */
354
+ .twk-seg { position: relative; display: flex; gap: 2px; padding: 2px; border-radius: var(--twk-radius); }
355
+ /* The pill sits 4px off the top and bottom, so -8px (the row's 10px right padding, less
356
+ * the 4px gap, less the seg's 2px pad) puts it 4px off the right too. */
357
+ .twk-row .twk-seg { flex-shrink: 0; margin-right: -8px; }
358
+ .twk-seg-pill {
359
+ position: absolute; top: 2px; bottom: 2px; left: 0; width: 0;
360
+ background: var(--twk-accent, var(--twk-surface-active)); border-radius: calc(var(--twk-radius) - 2px); z-index: 0; pointer-events: none;
361
+ }
362
+ .twk-seg.is-ready .twk-seg-pill { transition: left 0.24s var(--twk-ease-out), width 0.24s var(--twk-ease-out); }
363
+ .twk-seg-btn {
364
+ position: relative; z-index: 1; flex: 0 0 auto; min-width: 40px; padding: 6px 8px; text-align: center;
365
+ font: inherit; font-size: 12px; line-height: 14px; font-weight: 500; /* 14px, not normal: the same height whatever the font */
366
+ background: transparent; border: none; cursor: pointer;
367
+ color: var(--twk-text-label); transition: color 0.15s, transform 0.24s var(--twk-ease-pop);
368
+ }
369
+ .twk-seg-btn[data-active="true"] { color: var(--twk-on-accent); }
370
+ @media (hover: hover) and (pointer: fine) {
371
+ .twk-seg-btn:not([data-active="true"]):hover, .twk-tabs-tab:not([data-active="true"]):hover { color: var(--twk-text-primary); } /* a tab's label lights like a segment's */
372
+ }
373
+ /* A press squashes the label; the pill itself does the liquid stretch (JS, WAAPI). */
374
+ .twk-seg-btn:active { transform: scale(var(--twk-press-chip)); transition: transform 0.08s ease-out; }
375
+ /* In a row the buttons hug their text; 5px padding matches the tabs' pill height. */
376
+ .twk-row .twk-seg .twk-seg-btn { min-width: 0; padding: 5px; }
377
+
378
+ /* ── Radio grid — a segmented control wrapped into a 2- or 3-column grid ── */
379
+ /* A 4px frame round the cells: one row is 24px cell + 8px frame = the 32px row height. */
380
+ .twk-radiogrid {
381
+ display: flex; align-items: flex-start; gap: 10px; /* a two-line grid keeps its label on the first line, as every tall row does */
382
+ min-height: var(--twk-row-height); padding: 4px 4px 4px 12px;
383
+ background: var(--twk-surface); border-radius: var(--twk-radius);
384
+ }
385
+ .twk-radiogrid-label {
386
+ flex: 1 1 5ch; min-width: 5ch; line-height: 24px; /* a cell's height: its text lands 9px down */
387
+ font-size: 12px; font-weight: 500; color: var(--twk-text-label); transition: color 0.15s;
388
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
389
+ }
390
+ /* One row of choices: each cell hugs its word like the Off/On toggle's segments, and a tight row
391
+ * shortens the label to 5ch before it cuts a cell. */
392
+ .twk-radiogrid-grid {
393
+ flex: 0 1 auto; min-width: 0; margin-left: auto;
394
+ display: grid; grid-template-columns: repeat(var(--twk-rg-cols, 3), minmax(0, auto)); gap: 4px;
395
+ }
396
+ /* Several rows: equal columns beside a label that keeps its width; a grid too wide for the row
397
+ * shrinks its cells, since a choice that long belongs in a list. */
398
+ .twk-radiogrid.is-grid .twk-radiogrid-label { flex: 0 1 auto; min-width: 0; }
399
+ .twk-radiogrid.is-grid .twk-radiogrid-grid { flex: 1 1 0; max-width: max-content; grid-template-columns: repeat(var(--twk-rg-cols, 3), minmax(0, 1fr)); }
400
+ .twk-radiogrid-btn {
401
+ font: inherit; font-size: 12px; line-height: 14px; font-weight: 500; font-variant-numeric: tabular-nums; /* 14px, not normal: the pill is 24px whatever the font */
402
+ min-width: 24px; padding: 5px; text-align: center; /* 5px, the 24px pill height shared with the toggle; at least as wide, so a glyph cell is square */
403
+ color: var(--twk-text-label); background: transparent;
404
+ border: none; border-radius: calc(var(--twk-radius) - 2px);
405
+ cursor: pointer; transition: background 0.15s, color 0.15s, transform 0.24s var(--twk-ease-pop);
406
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
407
+ }
408
+ @media (hover: hover) and (pointer: fine) {
409
+ .twk-radiogrid-btn:hover { background: var(--twk-surface-hover); color: var(--twk-text-primary); }
410
+ }
411
+ .twk-radiogrid-btn:active { transform: scale(var(--twk-press-chip)); transition: background 0.15s, color 0.15s, transform 0.08s ease-out; }
412
+ .twk-radiogrid-btn[data-active="true"] { background: var(--twk-accent, var(--twk-surface-active)); color: var(--twk-on-accent); }
413
+
414
+ /* ── Button (action) ── */
415
+ .twk-button {
416
+ display: flex; align-items: center; justify-content: center;
417
+ width: 100%; height: var(--twk-row-height); padding: 0 16px; font: inherit; font-size: 12px; font-weight: 500;
418
+ color: var(--twk-text-secondary); background: var(--twk-surface);
419
+ border: none; border-radius: var(--twk-radius); cursor: pointer;
420
+ transition: background 0.15s, color 0.15s, transform 0.24s var(--twk-ease-pop);
421
+ }
422
+ @media (hover: hover) and (pointer: fine) {
423
+ .twk-button:hover { background: var(--twk-surface-hover); color: var(--twk-text-primary); } /* a whole-row button also lights its fill */
424
+ }
425
+ .twk-button:active { background: var(--twk-surface-active); transform: scale(var(--twk-press-row)); transition: background 0.15s, color 0.15s, transform 0.08s ease-out; }
426
+
427
+ /* ── Button group — compact actions under one label ── */
428
+ /* -6px (the row's 10px right padding, less the 4px gap) sits the buttons 4px off the right
429
+ * like the top and bottom; a button is never narrower than its label, so many buttons wrap
430
+ * rather than cut their words. */
431
+ .twk-buttongroup { height: auto; min-height: var(--twk-row-height); padding-top: 4px; padding-bottom: 4px; } /* 4px above and below a wrapped group too, not only a one-line one */
432
+ .twk-buttongroup-btns { display: flex; flex-wrap: wrap; gap: 4px; flex: 1; min-width: 0; margin-right: -6px; }
433
+ .twk-buttongroup:not(:has(> .twk-row-label)) { padding-left: 4px; } /* with no label, the left matches the other three sides */
434
+ .twk-buttongroup-btn { flex: 1 1 0; min-width: min-content; padding: 5px 8px; font: inherit; font-size: 12px; line-height: 14px; font-weight: 500; color: var(--twk-text-label); background: var(--twk-surface-active); border: none; border-radius: calc(var(--twk-radius) - 2px); cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: background 0.15s, color 0.15s, transform 0.24s var(--twk-ease-pop); }
435
+ @media (hover: hover) and (pointer: fine) {
436
+ .twk-buttongroup-btn:hover { background: var(--twk-surface-hover); color: var(--twk-text-primary); }
437
+ }
438
+ .twk-buttongroup-btn:active { transform: scale(var(--twk-press-chip)); transition: background 0.15s, color 0.15s, transform 0.08s ease-out; }
439
+
440
+ /* ── Separator — a thin divider between sections ── */
441
+ .twk-separator { height: 1px; margin: 3px 2px; background: var(--twk-border); }
442
+
443
+ /* ── Tabs — paged controls with a sliding pill ── */
444
+ .twk-tabs { display: flex; flex-direction: column; gap: 8px; }
445
+ .twk-tabs-bar { position: relative; display: flex; gap: 4px; padding: 4px; min-width: 0; /* the rows' 4px frame round 24px pills: a 32px row */ background: var(--twk-surface); border-radius: var(--twk-radius); }
446
+ .twk-tabs-pill { position: absolute; top: 4px; bottom: 4px; left: 0; width: 0; background: var(--twk-surface-active); border-radius: calc(var(--twk-radius) - 2px); z-index: 0; transition: left 0.24s var(--twk-ease-out), width 0.24s var(--twk-ease-out); }
447
+ .twk-tabs-tab { position: relative; z-index: 1; flex: 1; min-width: 0; padding: 5px 8px; font: inherit; font-size: 12px; line-height: 14px; font-weight: 500; color: var(--twk-text-label); background: transparent; border: none; border-radius: calc(var(--twk-radius) - 2px); cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: color 0.15s, transform 0.24s var(--twk-ease-pop); }
448
+ .twk-tabs-tab[data-active="true"] { color: var(--twk-text-primary); }
449
+ .twk-tabs-tab:active { transform: scale(var(--twk-press-chip)); transition: color 0.15s, transform 0.08s ease-out; }
450
+ .twk-tabs-page { display: none; flex-direction: column; gap: 6px; }
451
+ /* The page's entrance replays on every activation (display flips). */
452
+ .twk-tabs-page[data-active="true"] { display: flex; animation: twk-page-in 0.3s var(--twk-ease-out); }
453
+ @keyframes twk-page-in { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: none; } }
454
+
455
+ /* ── Select ── */
456
+ .twk-select { position: relative; }
457
+ .twk-select-trigger {
458
+ display: flex; align-items: center; justify-content: space-between; gap: 10px; width: 100%; /* the gap holds once the value can shrink to the label */
459
+ height: var(--twk-row-height); padding: 0 10px 0 12px; /* the row idiom: 12 left, 10 right */
460
+ font: inherit; font-size: 12px; font-weight: 500; color: var(--twk-text-label);
461
+ background: var(--twk-surface); border: none; border-radius: var(--twk-radius);
462
+ cursor: pointer; transition: background 0.15s, transform 0.24s var(--twk-ease-pop);
463
+ }
464
+ @media (hover: hover) and (pointer: fine) {
465
+ .twk-select-trigger:hover { background: var(--twk-surface-hover); } /* the whole row is the button */
466
+ .twk-select-trigger:hover .twk-select-label, .twk-select-trigger:hover .twk-select-chevron { color: var(--twk-text-primary); } /* the chevron lights like a folder's */
467
+ }
468
+ .twk-select-trigger:active { transform: scale(var(--twk-press-row)); transition: background 0.15s, transform 0.08s ease-out; }
469
+ .twk-select.is-open .twk-select-trigger { background: var(--twk-surface-active); }
470
+ .twk-select-label { transition: color 0.15s; font-weight: 500; flex: 1 1 0; min-width: 5ch; text-align: left; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } /* a rigid label took the chevron out of the panel */
471
+ .twk-select-right { display: flex; align-items: center; gap: 8px; min-width: 0; flex: 0 1 auto; max-width: calc(100% - 5ch - 10px); }
472
+ .twk-select-value { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
473
+ /* The folder's 15px chevron; the glyph sits 3.75px inside its box, so pull it out by that side-bearing. */
474
+ .twk-select-chevron { width: 15px; height: 15px; flex-shrink: 0; color: var(--twk-text-section); opacity: 0.6; margin-right: -3.75px; transition: transform 0.2s var(--twk-ease-out), color 0.15s; }
475
+ .twk-select.is-open .twk-select-chevron { transform: rotate(180deg); }
476
+ /* The three portaled panes' shell (list dropdown, presets menu, color pop): fixed on <body> and placed by JS, the dropdown
477
+ * surface, and the entrance. Each one's own rule below sets its size, radius and origin. */
478
+ :is(.twk-select-dropdown, .twk-presets-menu, .twk-color-pop) {
479
+ position: fixed; z-index: 99999;
480
+ background: var(--twk-dropdown-bg); border: 1px solid var(--twk-border); box-shadow: var(--twk-shadow-dropdown);
481
+ opacity: 0; transform: translateY(-6px) scale(var(--twk-pop-s)); pointer-events: none;
482
+ transition: opacity 0.16s var(--twk-ease-out), transform 0.24s var(--twk-ease-spring);
483
+ }
484
+ .twk-select-dropdown {
485
+ padding: 4px; border-radius: calc(var(--twk-radius) + 2px); /* concentric: option (radius − 2) + padding 4 */
486
+ max-height: 60vh; overflow-y: auto;
487
+ transform-origin: top; transition-duration: 0.16s, 0.22s;
488
+ }
489
+ /* A pane matches its row, but never runs past the window's 8px margins. */
490
+ :is(.twk-select-dropdown, .twk-color-pop) { max-width: calc(100vw - 16px); }
491
+ /* Opened above the trigger: grow up from it. */
492
+ :is(.twk-select-dropdown, .twk-color-pop)[data-side="above"] { transform-origin: bottom; transform: translateY(6px) scale(var(--twk-pop-s)); }
493
+ .twk-select-option {
494
+ display: flex; align-items: center; gap: 8px; width: 100%; padding: 8px 10px; font: inherit; font-size: 12px; font-weight: 500;
495
+ line-height: 14px; /* not normal: a 30px option whatever the font, the chips' 14px */
496
+ color: var(--twk-text-label); background: transparent; border: none; border-radius: calc(var(--twk-radius) - 2px);
497
+ cursor: pointer; text-align: left; transition: background 0.15s, color 0.15s;
498
+ }
499
+ @media (hover: hover) and (pointer: fine) {
500
+ .twk-select-option:hover { background: var(--twk-surface-hover); }
501
+ }
502
+ .twk-select-option:focus-visible { background: var(--twk-surface-hover); color: var(--twk-text-primary); outline: none; } /* roving focus reads as the hover highlight */
503
+ .twk-select-option[data-selected="true"] { color: var(--twk-on-accent); background: var(--twk-accent, var(--twk-surface-active)); }
504
+ .twk-option-swatch { flex-shrink: 0; width: 12px; height: 12px; border-radius: 4px; box-shadow: inset 0 0 0 1px var(--twk-border-hover); }
505
+ .twk-option-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
506
+
507
+ /* A row or folder picked for ⌘C / ⌘V (a press beside it): a filled band behind it, edge to edge across the panel at any
508
+ * depth (the panel clips it to its rounded edge while something is picked). A row's band reaches 3px past it, halfway
509
+ * across the 6px gap to its neighbors, the same on both sides wherever it sits; nothing moves. A folder's band runs
510
+ * between its own rules. */
511
+ .twk-panel:not([data-mode="floating"]):has(.is-picked) { overflow: clip; } /* both axes, so it follows the rounded corners; a floating panel already clips, scrolling */
512
+ .twk-panel .is-picked { position: relative; isolation: isolate; }
513
+ .twk-panel .is-picked::before { content: ""; position: absolute; inset: -3px -100vmax; z-index: -1; pointer-events: none; background: color-mix(in srgb, var(--twk-accent, var(--twk-text-focus)) 5%, transparent); }
514
+ .twk-panel .twk-folder.is-picked::before { inset-block: 0; }
515
+ /* A paste onto the pick answers on the band: a brief brightening when it lands, a brief danger tint when nothing fits. */
516
+ .twk-panel .is-picked::before { transition: background 0.35s var(--twk-ease-out); }
517
+ .twk-panel .is-picked.is-pasted::before { background: color-mix(in srgb, var(--twk-accent, var(--twk-text-focus)) 20%, transparent); transition-duration: 0.08s; }
518
+ .twk-panel .is-picked.is-refused::before { background: color-mix(in srgb, var(--twk-danger) 18%, transparent); transition-duration: 0.08s; }
519
+
520
+ /* ── Presets menu — portaled; save / load / delete named snapshots ── */
521
+ .twk-presets-menu {
522
+ width: 208px;
523
+ display: flex; flex-direction: column; gap: 6px; padding: 8px;
524
+ border-radius: calc(var(--twk-radius) + 4px); /* concentric: inner picker surface (radius) + padding 4 */
525
+ transform-origin: top right;
526
+ }
527
+ .twk-presets-menu[data-side="above"] { transform-origin: bottom right; transform: translateY(6px) scale(var(--twk-pop-s)); }
528
+ /* Open: after every pane's [data-side] origin, which it must beat at the same specificity. */
529
+ :is(.twk-select-dropdown, .twk-presets-menu, .twk-color-pop).is-open { opacity: 1; transform: none; pointer-events: auto; }
530
+ .twk-presets-save { display: flex; gap: 6px; }
531
+ .twk-presets-input { flex: 1; min-width: 0; font: inherit; font-size: 12px; font-weight: 500; color: var(--twk-text-primary); background: var(--twk-surface); border: 1px solid var(--twk-border); border-radius: calc(var(--twk-radius) - 2px); padding: 6px 8px; outline: none; }
532
+ .twk-presets-input:focus { border-color: var(--twk-border-hover); }
533
+ .twk-presets-input::placeholder { color: var(--twk-text-tertiary); }
534
+ .twk-presets-savebtn { flex-shrink: 0; font: inherit; font-size: 12px; font-weight: 500; color: var(--twk-text-primary); background: var(--twk-surface); border: none; border-radius: calc(var(--twk-radius) - 2px); padding: 6px 10px; cursor: pointer; transition: background 0.15s, transform 0.24s var(--twk-ease-pop); }
535
+ @media (hover: hover) and (pointer: fine) {
536
+ .twk-presets-savebtn:hover { background: var(--twk-surface-hover); }
537
+ }
538
+ .twk-presets-list { display: flex; flex-direction: column; gap: 2px; max-height: 200px; overflow-y: auto; }
539
+ .twk-presets-empty { padding: 8px; font-size: 12px; font-weight: 500; color: var(--twk-text-tertiary); text-align: center; }
540
+ .twk-presets-row { display: flex; align-items: center; gap: 2px; }
541
+ .twk-presets-load { flex: 1; min-width: 0; text-align: left; font: inherit; font-size: 12px; font-weight: 500; color: var(--twk-text-label); background: transparent; border: none; border-radius: calc(var(--twk-radius) - 2px); padding: 6px 8px; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: background 0.15s, color 0.15s; }
542
+ @media (hover: hover) and (pointer: fine) {
543
+ .twk-presets-load:hover { background: var(--twk-surface-hover); color: var(--twk-text-primary); }
544
+ }
545
+ .twk-presets-del { flex-shrink: 0; display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; color: var(--twk-text-tertiary); background: transparent; border: none; border-radius: calc(var(--twk-radius) - 2px); cursor: pointer; transition: background 0.15s, color 0.15s, transform 0.24s var(--twk-ease-pop); }
546
+ @media (hover: hover) and (pointer: fine) {
547
+ .twk-presets-del:hover { background: var(--twk-surface-hover); color: var(--twk-text-primary); }
548
+ }
549
+ .twk-presets-del:active { transform: scale(var(--twk-press-icon)); transition: background 0.15s, color 0.15s, transform 0.08s ease-out; }
550
+ .twk-presets-savebtn:active { transform: scale(var(--twk-press-chip)); transition: background 0.15s, transform 0.08s ease-out; }
551
+ .twk-presets-del svg { width: 12px; height: 12px; display: block; }
552
+ /* Clear, in the gallery's head: a small text button the label's size, quiet until hovered; its text ends 10px in, as a row's value. */
553
+ .twk-image-clear { flex-shrink: 0; height: 24px; padding: 0 6px; font: inherit; font-size: 12px; font-weight: 500; color: var(--twk-text-tertiary); background: transparent; border: none; border-radius: calc(var(--twk-radius) - 2px); cursor: pointer; transition: background 0.15s, color 0.15s, transform 0.24s var(--twk-ease-pop); }
554
+ @media (hover: hover) and (pointer: fine) {
555
+ .twk-image-clear:hover { background: var(--twk-surface-hover); color: var(--twk-text-primary); }
556
+ }
557
+ .twk-image-clear:active { transform: scale(var(--twk-press-chip)); transition: background 0.15s, color 0.15s, transform 0.08s ease-out; }
558
+
559
+ /* ── Trigger rows — the row button behind the color, gradient and point controls ── */
560
+ .twk-trigger {
561
+ display: flex; align-items: center; justify-content: space-between; gap: 10px; width: 100%;
562
+ height: var(--twk-row-height); padding: 0 10px 0 12px; /* the row idiom: 12 left, 10 right */
563
+ font: inherit; font-size: 12px; font-weight: 500; color: var(--twk-text-label); cursor: pointer;
564
+ background: var(--twk-surface); border: none; border-radius: var(--twk-radius); transition: background 0.15s, transform 0.24s var(--twk-ease-pop);
565
+ }
566
+ @media (hover: hover) and (pointer: fine) {
567
+ .twk-trigger:hover { background: var(--twk-surface-hover); } /* the whole row is the button */
568
+ .twk-trigger:hover .twk-trigger-label { color: var(--twk-text-primary); }
569
+ }
570
+ .twk-trigger:active { transform: scale(var(--twk-press-row)); transition: background 0.15s, transform 0.08s ease-out; }
571
+ .is-open > .twk-trigger { background: var(--twk-surface-active); }
572
+ /* The label takes what the right cluster leaves, down to 5ch; shrink weights instead
573
+ * shaved a fraction of a pixel off the readout and drew an ellipsis over a value that fit. */
574
+ .twk-trigger-label { transition: color 0.15s; flex: 1 1 0; min-width: 5ch; text-align: left; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } /* a <button> centers its text by default */
575
+ .twk-trigger-right { display: flex; align-items: center; gap: 8px; min-width: 0; flex: 0 1 auto; max-width: calc(100% - 5ch - 10px); }
576
+ /* word-spacing separates a multi-part readout ("60% 0.23 286") into columns; visual only. */
577
+ .twk-trigger-value { word-spacing: 0.1em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; max-width: 20ch; }
578
+ /* The 18px preview chip — swatch, gradient strip or mini pad. */
579
+ .twk-trigger-chip { height: 18px; flex-shrink: 0; border-radius: calc(var(--twk-radius) - 4px); box-shadow: inset 0 0 0 1px var(--twk-border-hover); transition: transform 0.2s var(--twk-ease-spring); }
580
+ .is-open > .twk-trigger .twk-trigger-chip { transform: scale(1.07); } /* 1.14 read as a jump on the wider chips */
581
+
582
+ /* ── Color — a trigger row that opens the picker ── */
583
+ .twk-color { position: relative; }
584
+ .twk-color-swatch { width: 18px; }
585
+
586
+ /* Portaled surfaces open on <body>, escaping any ancestor that becomes a containing block
587
+ * for position:fixed (a transform or filter) and the panel's overflow. Each carries .twk-portal,
588
+ * which hands it the tokens, font and line-height above. */
589
+ .twk-portal *, .twk-portal *::before, .twk-portal *::after { box-sizing: border-box; }
590
+ /* Entrance scale from the height placeBelow() records: the far edge moves ~5px at any height, so a tall pop doesn't zoom. */
591
+ .twk-portal { --twk-pop-s: max(0.96, calc(1 - 5 / var(--twk-pop-h, 125))); }
592
+
593
+ /* the color dropdown */
594
+ .twk-color-pop {
595
+ min-width: 236px; /* the default panel's row width */
596
+ display: flex; flex-direction: column; gap: 8px; padding: 10px;
597
+ border-radius: calc(var(--twk-radius) + 10px); /* concentric: inner radius + padding 10 */
598
+ max-height: calc(100dvh - 16px); overflow-y: auto; overflow-x: clip; /* a short viewport scrolls the picker */
599
+ transform-origin: top;
600
+ }
601
+ /* The picker body as one column, so the gradient pop can mount it under the stop bar. */
602
+ .twk-color-body { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
603
+ .twk-color-mode-row { display: flex; align-items: center; gap: 8px; }
604
+ .twk-color-mode { appearance: none; font: inherit; font-size: 12px; font-weight: 500; color: var(--twk-text-primary); background-color: var(--twk-surface); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='15' height='15' viewBox='0 0 24 24' fill='none' stroke='%23888888' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 6.25px center; border: 1px solid var(--twk-border); border-radius: calc(var(--twk-radius) - 2px); padding: 4px 26px 4px 10px; line-height: 14px; cursor: pointer; outline: none; } /* 14px, not normal: 24px tall whatever the font, the chips' 14px. The folder's 15px chevron: right 6.25px + its 3.75px side-bearing = 10 */
605
+ @media (hover: hover) and (pointer: fine) {
606
+ .twk-color-mode:hover { background-color: var(--twk-surface-hover); }
607
+ }
608
+ .twk-color-mode:focus-visible { border-color: var(--twk-border-hover); } /* match the channel inputs' focus cue */
609
+ .twk-color-gamut { margin-left: auto; font-size: 12px; font-weight: 500; color: var(--twk-text-tertiary); }
610
+ .twk-color-channels { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
611
+ .twk-color-channels--hex { grid-template-columns: 1fr; }
612
+ .twk-color-chan { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
613
+ .twk-color-chan-label { font-size: 10px; font-weight: 600; letter-spacing: 0.03em; color: var(--twk-text-tertiary); text-transform: uppercase; }
614
+ .twk-color-chan-input { width: 100%; min-width: 0; font: inherit; font-size: 12px; font-weight: 500; color: var(--twk-text-label); background: var(--twk-surface); border: 1px solid var(--twk-border); border-radius: calc(var(--twk-radius) - 2px); padding: 5px 6px; outline: none; text-transform: uppercase; }
615
+ .twk-color-chan-input:focus { color: var(--twk-text-focus); border-color: var(--twk-border-hover); }
616
+
617
+ /* The plane and strips: no overflow:hidden, since the thumbs ride the edges. */
618
+ .twk-wg-area { position: relative; width: 100%; height: 140px; border-radius: var(--twk-radius); cursor: crosshair; touch-action: none; box-shadow: inset 0 0 0 1px var(--twk-border); }
619
+ .twk-wg-canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; border-radius: inherit; }
620
+ /* Thumbs are positioned by left/top, not transform, so the grab scale doesn't lag the drag. */
621
+ .twk-wg-thumb { position: absolute; width: 14px; height: 14px; border-radius: 999px; border: 2px solid #fff; box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.45); transform: translate(-50%, -50%); pointer-events: none; transition: transform 0.18s var(--twk-ease-spring), box-shadow 0.18s; }
622
+ .twk-wg-hue, .twk-wg-alpha { position: relative; width: 100%; height: 14px; border-radius: 999px; cursor: ew-resize; touch-action: none; box-shadow: inset 0 0 0 1px var(--twk-border); }
623
+ .twk-wg-alpha { background: var(--twk-checker); }
624
+ .twk-wg-hue-canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; border-radius: inherit; }
625
+ .twk-wg-hue-thumb { position: absolute; top: 50%; width: 16px; height: 16px; border-radius: 50%; border: 2px solid #fff; box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.45), 0 1px 3px rgba(0, 0, 0, 0.4); transform: translate(-50%, -50%); pointer-events: none; transition: transform 0.18s var(--twk-ease-spring), box-shadow 0.18s; }
626
+ .twk-wg-area.is-grabbing .twk-wg-thumb, .twk-wg-hue.is-grabbing .twk-wg-hue-thumb, .twk-wg-alpha.is-grabbing .twk-wg-hue-thumb { transform: translate(-50%, -50%) scale(1.3); box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.45), 0 2px 8px rgba(0, 0, 0, 0.45); }
627
+ .twk-wg-alpha-grad { position: absolute; inset: 0; border-radius: inherit; }
628
+
629
+ /* ── Gradient — a trigger row that opens a stop bar + picker body ── */
630
+ .twk-gradient { position: relative; }
631
+ .twk-gradient-preview { width: 72px; background: var(--twk-checker); }
632
+
633
+ /* Native selects size to their widest option, so two in one row need tight padding to fit the 236px floor. */
634
+ .twk-gradient-pop .twk-color-mode-row { gap: 4px; }
635
+ .twk-gradient-pop .twk-color-mode { padding-left: 6px; padding-right: 20px; }
636
+ .twk-gradient-bar-row { display: flex; align-items: stretch; gap: 6px; }
637
+ .twk-gradient-bar-row > .twk-gradient-bar { flex: 1; min-width: 0; }
638
+ .twk-gradient-bar { position: relative; width: 100%; height: 28px; border-radius: var(--twk-radius); box-shadow: inset 0 0 0 1px var(--twk-border); background: var(--twk-checker); }
639
+ .twk-gradient-grad { position: absolute; inset: 0; border-radius: inherit; pointer-events: none; }
640
+ .twk-gradient-rail { position: absolute; left: 0; right: 0; bottom: -5px; height: 14px; pointer-events: none; }
641
+ .twk-gradient-stop { position: absolute; top: 0; width: 13px; height: 13px; padding: 0; transform: translateX(-50%); border-radius: calc(var(--twk-radius) - 4px); background: var(--stop); border: 2px solid var(--twk-dropdown-bg); box-shadow: 0 0 0 1px var(--twk-border-hover), 0 1px 3px rgba(0, 0, 0, 0.4); cursor: ew-resize; touch-action: none; pointer-events: auto; transition: opacity 0.12s ease, transform 0.16s var(--twk-ease-spring), box-shadow 0.16s; }
642
+ .twk-gradient-stop[data-sel="true"] { box-shadow: 0 0 0 2px var(--twk-accent, var(--twk-text-focus)), 0 1px 3px rgba(0, 0, 0, 0.4); z-index: 1; }
643
+ /* Hit targets: an invisible padded pseudo-element takes the press from ~21px around. */
644
+ .twk-gradient-stop::before, .twk-bezier-handle::before { content: ""; position: absolute; inset: -4px; border-radius: inherit; }
645
+ @media (hover: hover) and (pointer: fine) {
646
+ .twk-gradient-stop:hover { transform: translateX(-50%) scale(1.2); }
647
+ }
648
+ /* A stop dragged clear of the bar: release to remove. */
649
+ .twk-gradient-stop[data-removing="true"] { opacity: 0.3; transform: translateX(-50%) scale(0.7); }
650
+ .twk-gradient-add { display: flex; align-items: center; justify-content: center; flex: none; width: 28px; padding: 0; border: none; border-radius: var(--twk-radius); background: var(--twk-surface); color: var(--twk-text-primary); cursor: pointer; will-change: transform; transition: background 0.15s, transform 0.24s var(--twk-ease-pop); } /* will-change: the Safari pixel-snap fix */
651
+ .twk-gradient-add:active { transform: scale(var(--twk-press-icon)); transition: background 0.15s, transform 0.08s ease-out; }
652
+ .twk-gradient-add svg { width: 16px; height: 16px; display: block; will-change: transform; transition: transform 0.2s var(--twk-ease-pop); }
653
+ @media (hover: hover) and (pointer: fine) {
654
+ .twk-gradient-add:hover { background: var(--twk-surface-hover); }
655
+ .twk-gradient-add:hover svg { transform: scale(1.2); }
656
+ }
657
+
658
+ /* ── Text + number rows (string / number blades) ── */
659
+ .twk-text, .twk-num {
660
+ flex: 1; min-width: 0; font: inherit; font-size: 12px; font-weight: 500;
661
+ color: var(--twk-text-label); background: transparent; border: none; outline: none;
662
+ text-align: right; cursor: text; padding: 0;
663
+ border-radius: calc(var(--twk-radius) - 4px); /* rounds the focus ring (outline follows radius) */
664
+ }
665
+ .twk-text:focus, .twk-num:focus { color: var(--twk-text-focus); }
666
+ .twk-text::placeholder { color: var(--twk-text-tertiary); }
667
+ .twk-row-multiline { position: relative; flex-direction: column; align-items: stretch; height: auto; gap: 4px; padding: 7.5px 10px 10px 12px; } /* top: the label's text 9px down, as a 32px row's; bottom 10 > the 8px radius keeps the grip clear */
668
+ .twk-textarea { flex: none; width: 100%; resize: vertical; min-height: 3em; padding: 0; line-height: 1.45; text-align: left; }
669
+ /* The native resizer grip is invisible on mobile Safari, so draw one; a mask lets it
670
+ * take the token color in both schemes. */
671
+ .twk-textarea::-webkit-resizer { background: transparent; }
672
+ /* Text past the field's edges fades rather than cuts (basic.ts clip()). */
673
+ .twk-textarea:is(.is-clip-top, .is-clip-bottom) { --twk-clip-top: 0px; --twk-clip-bottom: 0px; -webkit-mask-image: linear-gradient(to bottom, transparent, #000 var(--twk-clip-top), #000 calc(100% - var(--twk-clip-bottom)), transparent); mask-image: linear-gradient(to bottom, transparent, #000 var(--twk-clip-top), #000 calc(100% - var(--twk-clip-bottom)), transparent); }
674
+ .twk-textarea.is-clip-top { --twk-clip-top: 14px; }
675
+ .twk-textarea.is-clip-bottom { --twk-clip-bottom: 14px; }
676
+ .twk-row-multiline::after {
677
+ content: ""; position: absolute; right: 8px; bottom: 8px; width: 11px; height: 11px;
678
+ pointer-events: none; background-color: var(--twk-text-tertiary);
679
+ --twk-grip: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M11 3 3 11M11 7 7 11' stroke='%23000' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E");
680
+ -webkit-mask: var(--twk-grip) right bottom / contain no-repeat; mask: var(--twk-grip) right bottom / contain no-repeat;
681
+ }
682
+
683
+ /* ── Image — a trigger row that is also the drop zone; with options, a gallery: the plot's stack of a flush label, the tiles and the kit's button ── */
684
+ .twk-image { position: relative; }
685
+ .twk-image-input { display: none; }
686
+ .twk-image-text, .twk-image-note { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
687
+ .twk-image-note:empty { display: none; }
688
+ .twk-image-note:not(:empty) + :is(.twk-image-text, .twk-trigger-value) { display: none; } /* a refused file's reason takes the text's place for a moment */
689
+ .twk-image-thumb { width: 24px; background: var(--twk-base) center / cover no-repeat; } /* 24 × 18: the tiles' 4:3 */
690
+ .twk-image-thumb[data-set="false"] { background: var(--twk-checker); }
691
+ /* JS toggles [data-over] on the trigger, or on the gallery root, while a file is dragged over it. */
692
+ .twk-image .twk-trigger[data-over="true"], .twk-image-gallery[data-over="true"] .twk-image-upload { background: var(--twk-surface-hover); outline: 1.5px dashed var(--twk-border-hover); outline-offset: -2px; }
693
+ .twk-image .twk-trigger[data-over="true"] .twk-image-thumb { transform: scale(1.14); }
694
+ .twk-image-gallery { display: flex; flex-direction: column; gap: 6px; }
695
+ /* Under the tiles: the upload button, then Clear, as tall as the button. They fold the way a folder does, on its curve:
696
+ * Clear folds sideways when there's nothing to clear and the upload button eases into its room; a foot with nothing in
697
+ * it folds up, and the pane eases shorter. The foot's own space is part of the pane's drag edge; its buttons still take
698
+ * their own presses. The focus rings are inset, so the clipping leaves them whole. */
699
+ .twk-image-foot { display: grid; grid-template-rows: 1fr; pointer-events: none; transition: grid-template-rows 0.35s var(--twk-ease-fold), margin-top 0.35s var(--twk-ease-fold); }
700
+ .twk-image-foot[data-open="false"] { grid-template-rows: 0fr; margin-top: -6px; } /* -6px takes back the pane's gap above it */
701
+ .twk-image-foot-row { display: flex; align-items: stretch; justify-content: flex-end; min-height: 0; overflow: hidden; }
702
+ .twk-image-foot-row > * { pointer-events: auto; }
703
+ .twk-image-foot .twk-image-upload { flex: 1; min-width: 0; }
704
+ .twk-image-clear-slot { display: grid; grid-template-columns: 1fr; margin-left: 6px; overflow: hidden; transition: grid-template-columns 0.35s var(--twk-ease-fold), margin-left 0.35s var(--twk-ease-fold), opacity 0.35s var(--twk-ease-fold); } /* the fade too, as a folder's content */
705
+ .twk-image-clear-slot[data-open="false"] { grid-template-columns: 0fr; margin-left: 0; opacity: 0; }
706
+ .twk-image-clear-slot .twk-image-clear { height: auto; min-height: 24px; min-width: 0; padding: 0 10px; white-space: nowrap; transition: padding 0.35s var(--twk-ease-fold), background 0.15s, color 0.15s, transform 0.24s var(--twk-ease-pop); }
707
+ .twk-image-clear-slot[data-open="false"] .twk-image-clear { padding: 0; } /* its padding folds too, or 20px of it would stay */
708
+ .twk-image-grid { display: grid; grid-template-columns: repeat(var(--twk-img-cols, 2), minmax(0, 1fr)); gap: 6px; }
709
+ /* A tile is its image; a hairline ring set off by a gap marks the chosen one, so the picture stays readable.
710
+ * The gap is outline-offset, which shows the panel behind it. */
711
+ .twk-image-tile {
712
+ aspect-ratio: 4 / 3; padding: 0; background: var(--twk-surface) center / cover no-repeat;
713
+ border: none; border-radius: calc(var(--twk-radius) - 2px); box-shadow: inset 0 0 0 1px var(--twk-border-hover);
714
+ cursor: pointer; transition: transform 0.24s var(--twk-ease-pop), box-shadow 0.15s;
715
+ }
716
+ .twk-image-tile:active { transform: scale(var(--twk-press-chip)); transition: transform 0.08s ease-out; }
717
+ .twk-image-tile[data-active="true"] { outline: 1.5px solid var(--twk-accent, var(--twk-text-focus)); outline-offset: 1px; }
718
+ /* The kit's inner focus ring, after the chosen tile's rule so it wins there too. An outline, not a shadow: the hover
719
+ * hairline is a shadow and would replace it, a host's own button:focus-visible outline would add a second ring, and
720
+ * forced colors drop shadows. */
721
+ .twk-image-tile:focus-visible { outline: 2px solid var(--twk-accent, var(--twk-text-focus)); outline-offset: -2px; }
722
+ .twk-image-upload { gap: 6px; } /* a .twk-button, plus the icon's gap */
723
+ .twk-image-upload svg { width: 14px; height: 14px; flex: none; }
724
+ @media (hover: hover) and (pointer: fine) {
725
+ .twk-image-tile:hover { box-shadow: inset 0 0 0 1px var(--twk-text-tertiary); } /* the hairline lights, nothing moves; the chosen tile too: on, and about to be pressed, are two states */
726
+ }
727
+
728
+ /* ── FPS graph (monitor blade) ── */
729
+ .twk-fps {
730
+ display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 0 8px;
731
+ background: var(--twk-surface); border-radius: var(--twk-radius); padding: 9px 10px 10px 12px; /* the row idiom: 12 left / 10 right; the label's text 9px down */
732
+ line-height: 14px; /* not normal: a graphless monitor is 33px whatever the font, the chips' 14px */
733
+ }
734
+ .twk-fps-label { font-size: 12px; font-weight: 500; color: var(--twk-text-label); }
735
+ .twk-fps-val { font-size: 12px; color: var(--twk-text-primary); text-align: right; }
736
+ .twk-fps-canvas { grid-column: 1 / -1; width: 100%; height: 40px; display: block; margin-top: 6px; }
737
+ /* Monitor string buffer — the last N values, newest at the bottom. */
738
+ .twk-monitor-buffer { grid-column: 1 / -1; margin: 6px 0 0; height: calc(var(--twk-monitor-rows, 4) * 1.45em); overflow: hidden; font-family: var(--twk-font-sans); font-variant-numeric: tabular-nums; font-size: 11px; line-height: 1.45; color: var(--twk-text-secondary); white-space: pre; word-break: break-all; }
739
+
740
+ /* ── Number grab handle (drag to scrub) ── */
741
+ .twk-num-wrap { display: flex; align-items: center; gap: 4px; }
742
+ .twk-slider-unit { color: var(--twk-text-tertiary); } /* the slider readout's unit ("24 px") */
743
+ /* The unit shares the input's size and line box, so centering lands both on one baseline. */
744
+ .twk-num-unit { flex-shrink: 0; font-size: 12px; line-height: normal; color: var(--twk-text-tertiary); padding-right: 2px; user-select: none; -webkit-user-select: none; }
745
+ .twk-num-wrap .twk-num { flex: 0 0 auto; width: 6ch; text-overflow: ellipsis; } /* a wide value shows it is cut */
746
+ .twk-row > .twk-num-wrap .twk-num { width: 9ch; } /* the Number row has room a field grid lacks */
747
+ .twk-num-grab { display: flex; align-items: center; justify-content: center; width: 12px; height: 18px; flex-shrink: 0; color: var(--twk-text-tertiary); cursor: ew-resize; touch-action: none; transition: color 0.15s, transform 0.18s var(--twk-ease-spring); }
748
+ .twk-num-grab svg { width: 12px; height: 12px; display: block; }
749
+ .twk-num-grab.is-dragging { color: var(--twk-text-primary); transform: scale(1.2); }
750
+ @media (hover: hover) and (pointer: fine) {
751
+ .twk-num-grab:hover { color: var(--twk-text-primary); transform: scale(1.2); }
752
+ }
753
+ /* Grab guide — portaled to <body> while scrubbing a number field. */
754
+ .twk-grab-guide { position: fixed; inset: 0; z-index: 100001; pointer-events: none; } /* tops the kit's z ladder */
755
+ /* round dots with a controllable gap (`dotted` packs them too tightly) */
756
+ .twk-grab-line { position: fixed; height: 2px; border: 0; transform: translateY(-50%); background: radial-gradient(circle closest-side, var(--twk-text-label) 99%, transparent) 0 50% / var(--twk-grab-gap, 7px) 2px repeat-x; }
757
+ .twk-grab-dot { position: fixed; width: 5px; height: 5px; border-radius: 50%; background: var(--twk-text-label); transform: translate(-50%, -50%); }
758
+ .twk-grab-bubble { position: fixed; transform: translate(-50%, -100%); padding: 2px 7px; border-radius: calc(var(--twk-radius) - 2px); background: var(--twk-dropdown-bg); border: 1px solid var(--twk-border); box-shadow: var(--twk-shadow-dropdown); color: var(--twk-text-primary); font-size: 11px; font-weight: 500; white-space: nowrap; transform-origin: bottom center; animation: twk-bubble-in 0.18s var(--twk-ease-pop); }
759
+ @keyframes twk-bubble-in { from { opacity: 0; transform: translate(-50%, -100%) scale(0.8); } to { opacity: 1; transform: translate(-50%, -100%) scale(1); } }
760
+ .twk-grab-arrow { position: fixed; width: 0; height: 0; border-top: 4px solid transparent; border-bottom: 4px solid transparent; border-left: 7px solid var(--twk-text-label); }
761
+
762
+ /* ── Folder — collapsible titled group ── */
763
+ /* Consecutive folders share one rule; the first and last drop their outer rule. */
764
+ .twk-folder { display: flex; flex-direction: column; border-top: 1px solid var(--twk-border); border-bottom: 1px solid var(--twk-border); }
765
+ .twk-folder + .twk-folder { border-top: none; margin-top: -6px; } /* cancel the 6px stack gap so the rules merge */
766
+ .twk-folder .twk-folder { margin-left: 10px; }
767
+ .twk-controls > .twk-folder:first-child { border-top: none; }
768
+ .twk-folder:last-child { border-bottom: none; }
769
+ .twk-folder-header { display: flex; align-items: center; justify-content: space-between; gap: 6px; width: 100%; height: var(--twk-row-height); padding: 0; background: none; border: none; cursor: pointer; color: inherit; font: inherit; text-align: left; }
770
+ .twk-folder-title { font-size: 12px; font-weight: 600; color: var(--twk-text-section); letter-spacing: -0.01em; transition: color 0.15s; }
771
+ @media (hover: hover) and (pointer: fine) {
772
+ .twk-folder-header:hover .twk-folder-title { color: var(--twk-text-primary); }
773
+ .twk-folder-header:hover .twk-chevron { color: var(--twk-text-primary); }
774
+ }
775
+ /* The 15px box carries 3.75px of side-bearing; pull it out so the glyph lands 2px in. The
776
+ * caret rides the body's 0.35s --fold clock so it turns in step with the fade. */
777
+ .twk-folder-header .twk-chevron { color: var(--twk-text-section); opacity: 0.6; margin-right: -1.75px; transition: transform 0.35s var(--twk-ease-fold), color 0.15s; }
778
+ .twk-folder.is-collapsed .twk-folder-header .twk-chevron { transform: rotate(180deg); }
779
+ /* contain: layout isolates the fold's per-frame re-layout from the rest of the panel. */
780
+ .twk-folder-body { display: grid; grid-template-rows: 1fr; transition: grid-template-rows 0.35s var(--twk-ease-fold); contain: layout; }
781
+ .twk-folder.is-collapsed .twk-folder-body { grid-template-rows: 0fr; }
782
+ /* Height and opacity share the --fold clock; y-clip only, as on .twk-body > .twk-controls. */
783
+ .twk-folder-body > .twk-controls { overflow-x: visible; overflow-y: clip; min-height: 0; min-width: 0; padding: 3px 0 8px; margin-top: -3px; opacity: 1; /* top: the pick band's clip edge, as on .twk-body */ transition: opacity 0.35s var(--twk-ease-fold), padding-bottom 0.35s var(--twk-ease-fold), margin-bottom 0.35s var(--twk-ease-fold); }
784
+ /* Collapse the padding too, so a collapsed group is exactly its header. */
785
+ .twk-folder.is-collapsed .twk-folder-body > .twk-controls { opacity: 0; padding-bottom: 0; }
786
+ /* The 8px only clears the bottom hairline, which the last folder drops. */
787
+ .twk-folder:last-child .twk-folder-body > .twk-controls { padding-bottom: 0; }
788
+ .twk-folder:last-child:not(.is-collapsed) .twk-folder-body > .twk-controls { padding-bottom: 3px; margin-bottom: -3px; } /* the pick band's clip edge; both fold away together */
789
+ /* In a flex column the rows would shrink to fit the folding track; keep their height. */
790
+ .twk-body > .twk-controls > *, .twk-folder-body > .twk-controls > * { flex-shrink: 0; }
791
+
792
+ /* ── Shadow, sides, motion and spring ── */
793
+ /* The shadow control (shadow.ts). */
794
+ .twk-shadow-chip, .twk-motion-chip { width: 26px; background: var(--twk-base); overflow: hidden; } /* the two drawn chips: a tile with the shadow, a curve */
795
+ .twk-shadow-chip { display: flex; align-items: center; justify-content: center; }
796
+ .twk-shadow-chip-tile { width: 12px; height: 8px; border-radius: 2px; background: var(--twk-dropdown-bg); }
797
+ .twk-shadow-stage { display: flex; align-items: center; justify-content: center; height: 84px; border-radius: var(--twk-radius); background: var(--twk-base); overflow: hidden; }
798
+ .twk-shadow-card { width: 64px; height: 40px; border-radius: 9px; background: var(--twk-dropdown-bg); }
799
+ .twk-shadow-color { width: 100%; } /* the color row in the pop discloses the picker body */
800
+ .twk-shadow-color .twk-trigger-right, .twk-shadow-color .twk-trigger-value { max-width: none; } /* the row has the pop's full width */
801
+ .twk-shadow-pop .twk-color-body[hidden] { display: none; } /* display: flex would beat the hidden attribute */
802
+ .twk-shadow-chevron { flex-shrink: 0; width: 12px; height: 12px; color: var(--twk-text-tertiary); transition: transform 0.2s var(--twk-ease-pop); }
803
+ .twk-shadow-color[aria-expanded="true"] .twk-shadow-chevron { transform: rotate(180deg); }
804
+ .twk-shadow-pop .twk-color-body { margin-top: 2px; }
805
+ /* The sides control (sides.ts). */
806
+ .twk-sides-link { flex-shrink: 0; display: flex; align-items: center; justify-content: center; width: 22px; height: 22px; padding: 0; color: var(--twk-text-tertiary); background: transparent; border: none; border-radius: calc(var(--twk-radius) - 3px); cursor: pointer; transition: color 0.15s, background 0.15s, transform 0.24s var(--twk-ease-pop); }
807
+ .twk-sides-link svg { width: 14px; height: 14px; }
808
+ .twk-sides-link[aria-pressed="true"] { color: var(--twk-accent, var(--twk-text-primary)); }
809
+ @media (hover: hover) and (pointer: fine) {
810
+ .twk-sides-link:hover { background: var(--twk-surface-hover); color: var(--twk-text-primary); }
811
+ }
812
+ .twk-sides-link:active { transform: scale(var(--twk-press-icon)); transition: color 0.15s, background 0.15s, transform 0.08s ease-out; }
813
+ .twk-sides > .twk-row .twk-num-wrap { margin-left: auto; } /* the link is last in the row too, at the pixels the head's link takes */
814
+ .twk-sides.is-linked .twk-sides-multi, .twk-sides:not(.is-linked) > .twk-row { display: none; }
815
+ /* The open block: a head with the row's exact geometry (so label and link stay put on toggle), then four fields in a
816
+ * line. Four across leaves ~38px inside a field — room for the grab and three digits, not the unit, which the head
817
+ * shows once after the readout, ending 14px from the link like the row's "px" does. */
818
+ .twk-sides-multi { display: flex; flex-direction: column; gap: 6px; padding: 0 10px 10px 12px; background: var(--twk-surface); border-radius: var(--twk-radius); }
819
+ .twk-sides-head { display: flex; align-items: center; gap: 12px; height: var(--twk-row-height); }
820
+ .twk-fields.twk-shadow-fields { grid-auto-flow: row; grid-template-columns: 1fr 1fr; } /* 2×2: four across left one digit showing */
821
+ .twk-sides-multi .twk-fields { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 4px; direction: ltr; } /* the places are physical; an rtl host must not mirror them */
822
+ .twk-sides-multi .twk-field .twk-num-wrap { padding: 3px 5px; }
823
+ .twk-sides-multi .twk-field .twk-num-unit { display: none; }
824
+ .twk-sides-glyph { display: flex; align-items: center; height: 12px; } /* the glyph sits in a text label's line */
825
+ .twk-sides-glyph svg { width: 12px; height: 12px; }
826
+ .twk-sides-glyph[data-at="top-right"] svg, .twk-sides-glyph[data-at="right"] svg { transform: rotate(90deg); }
827
+ .twk-sides-glyph[data-at="bottom-right"] svg, .twk-sides-glyph[data-at="bottom"] svg { transform: rotate(180deg); }
828
+ .twk-sides-glyph[data-at="bottom-left"] svg, .twk-sides-glyph[data-at="left"] svg { transform: rotate(270deg); }
829
+ /* The readout is set like the row's field — the digits' tone and weight, the unit's 4px gap, weight and inset — so a
830
+ * toggle swaps one for the other without a glyph moving or changing tone. */
831
+ .twk-sides-sum { margin-left: auto; font-size: 12px; font-weight: 500; color: var(--twk-text-label); word-spacing: 0.1em; }
832
+ .twk-sides-unit { margin-left: 4px; padding-right: 2px; font-weight: 400; color: var(--twk-text-tertiary); }
833
+ /* The motion control (motion.ts). */
834
+ .twk-motion-chip { position: relative; }
835
+ .twk-motion-chip svg { display: block; width: 100%; height: 100%; }
836
+ .twk-motion-chip path { fill: none; stroke: var(--twk-accent, var(--twk-text-primary)); stroke-width: 1.25; stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke; } /* non-scaling: 1.25 CSS px whatever the viewBox */
837
+ .twk-motion-body, .twk-motion-pane { display: flex; flex-direction: column; gap: 8px; }
838
+ .twk-motion-pop .twk-bezier-graph, .twk-motion-pop .twk-spring-viz { height: 120px; } /* the popover has the room the inline editors don't */
839
+ .twk-motion-pane .twk-spring > .twk-row { display: none; } /* duration + bounce only (motion.ts springTime) */
840
+ .twk-motion-stage { display: flex; align-items: center; height: 56px; padding: 0 16px; border-radius: var(--twk-radius); background: var(--twk-surface); overflow: hidden; cursor: pointer; } /* padding: STAGE_PAD in motion.ts, room for overshoot */
841
+ .twk-motion-box { width: 24px; height: 24px; border-radius: 7px; background: var(--twk-accent, var(--twk-text-primary)); will-change: transform; }
842
+ .twk-spring { display: flex; flex-direction: column; gap: 6px; }
843
+ .twk-spring-viz { width: 100%; height: 96px; border-radius: var(--twk-radius); background: var(--twk-surface); overflow: hidden; cursor: crosshair; touch-action: none; transition: background 0.15s; }
844
+ @media (hover: hover) and (pointer: fine) {
845
+ .twk-spring-viz:hover { background: var(--twk-surface-hover); }
846
+ }
847
+ /* No resting border, so the grab cue is an inset ring. */
848
+ .twk-spring-viz.is-dragging { background: var(--twk-surface-hover); box-shadow: inset 0 0 0 1px var(--twk-border-hover); }
849
+ .twk-spring-canvas { width: 100%; height: 100%; display: block; }
850
+ /* Labeled numeric fields — shared by Spring, Point and Bezier */
851
+ .twk-fields { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); gap: 6px; }
852
+ /* ── Point — a trigger row that opens the 2D pad ── */
853
+ .twk-point { position: relative; }
854
+ /* The mini pad's radius equals the dot's (2.5px), so a cornered dot sits concentric. */
855
+ .twk-point-preview { position: relative; width: 18px; border-radius: 2.5px; background: var(--twk-base); overflow: hidden; }
856
+ .twk-point-preview-dot { position: absolute; width: 5px; height: 5px; border-radius: 999px; background: var(--twk-accent, var(--twk-text-primary)); transform: translate(-50%, -50%); }
857
+ .twk-point-body { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
858
+ .twk-point-pop .twk-pad { max-width: 240px; margin: 0 auto; } /* fill the pane, square, up to 240px */
859
+ /* ── 2D point pad — overflow:visible so the thumb may overhang at the extremes ── */
860
+ .twk-pad { position: relative; width: 100%; max-width: 176px; aspect-ratio: 1; margin: 0 auto 8px; border-radius: var(--twk-radius); background: var(--twk-base); box-shadow: inset 0 0 0 1px var(--twk-border); cursor: crosshair; touch-action: none; overflow: visible; }
861
+ .twk-pad-line { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
862
+ .twk-pad-line line { stroke: var(--twk-accent, var(--twk-text-tertiary)); stroke-width: 1.5; vector-effect: non-scaling-stroke; stroke-linecap: round; }
863
+ .twk-pad-axis { position: absolute; background: var(--twk-border); pointer-events: none; }
864
+ .twk-pad-axis-x { left: 0; right: 0; top: 50%; height: 1px; transform: translateY(-50%); }
865
+ .twk-pad-axis-y { top: 0; bottom: 0; left: 50%; width: 1px; transform: translateX(-50%); }
866
+ .twk-pad-thumb { position: absolute; width: 14px; height: 14px; border-radius: 999px; background: var(--twk-accent, var(--twk-text-primary)); border: 2px solid var(--twk-base); box-shadow: 0 0 0 1px var(--twk-border-hover), 0 1px 3px rgba(0, 0, 0, 0.3); transform: translate(-50%, -50%); pointer-events: none; transition: transform 0.18s var(--twk-ease-spring), box-shadow 0.18s; }
867
+ .twk-pad.is-grabbing .twk-pad-thumb { transform: translate(-50%, -50%) scale(1.3); box-shadow: 0 0 0 1px var(--twk-border-hover), 0 3px 9px rgba(0, 0, 0, 0.4); }
868
+ .twk-field { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
869
+ .twk-field-label { font-size: 10px; line-height: 12px; font-weight: 600; letter-spacing: 0.02em; color: var(--twk-text-tertiary); text-transform: uppercase; }
870
+ .twk-field .twk-num-wrap { background: var(--twk-base); border: 1px solid var(--twk-border); border-radius: calc(var(--twk-radius) - 2px); padding: 3px 4px; gap: 2px; line-height: 15px; transition: border-color 0.15s; }
871
+ .twk-field .twk-num-unit { line-height: inherit; } /* the input's 15px line box; with the label's 12px, a field is 42px whatever the font */
872
+ .twk-field .twk-num-wrap:focus-within { border-color: var(--twk-border-hover); }
873
+ /* Keyboard focus rings the whole boxed field, not the input alone inside it. */
874
+ .twk-field .twk-num-wrap:has(> .twk-num:focus-visible:not(.twk-focus-quiet)), .twk-field .twk-num-wrap:has(> .twk-color-chan-input:focus-visible:not(.twk-focus-quiet)) { outline: 2px solid var(--twk-accent, var(--twk-text-focus)); outline-offset: -1px; }
875
+ .twk-field .twk-num-wrap > .twk-num:focus-visible, .twk-field .twk-num-wrap > .twk-color-chan-input:focus-visible { outline: none; }
876
+ /* The same for the plot's expression field and a slider's inline number field. */
877
+ .twk-plot-field:has(> .twk-plot-input:focus-visible:not(.twk-focus-quiet)) { outline: 2px solid var(--twk-accent, var(--twk-text-focus)); outline-offset: -2px; }
878
+ .twk-slider:has(.twk-slider-input:focus-visible:not(.twk-focus-quiet)) { outline: 2px solid var(--twk-accent, var(--twk-text-focus)); outline-offset: -2px; }
879
+ .twk-wg-area:focus-visible, .twk-wg-hue:focus-visible, .twk-wg-alpha:focus-visible { outline: 2px solid var(--twk-accent, var(--twk-text-focus)); outline-offset: 2px; }
880
+ .twk-field .twk-num { flex: 1; width: auto; min-width: 0; }
881
+ .twk-field .twk-num-grab { width: 10px; }
882
+
883
+ /* ── Cubic bezier — interactive curve editor ── */
884
+ .twk-bezier { display: flex; flex-direction: column; gap: 6px; }
885
+ .twk-bezier-graph { position: relative; width: 100%; height: 96px; background: var(--twk-surface); border-radius: var(--twk-radius); overflow: hidden; touch-action: none; }
886
+ .twk-bezier-svg { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
887
+ .twk-bezier-guide { stroke: var(--twk-border); stroke-width: 1; }
888
+ .twk-bezier-diag { stroke: var(--twk-surface-active); stroke-width: 1; stroke-dasharray: 2 3; }
889
+ .twk-bezier-ctl { stroke: var(--twk-text-tertiary); stroke-width: 1.5; }
890
+ .twk-bezier-curve { fill: none; stroke: var(--twk-accent, var(--twk-text-primary)); stroke-width: 2; stroke-linecap: round; }
891
+ .twk-bezier-handle {
892
+ position: absolute; width: 14px; height: 14px; margin: 0; padding: 0; /* 14px, as the color-area and point-pad thumbs */
893
+ transform: translate(-50%, -50%); border-radius: 999px;
894
+ background: var(--twk-text-primary); border: 2px solid var(--twk-base);
895
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4); cursor: grab; touch-action: none;
896
+ transition: transform 0.18s var(--twk-ease-spring), box-shadow 0.18s;
897
+ }
898
+ @media (hover: hover) and (pointer: fine) {
899
+ .twk-bezier-handle:hover { transform: translate(-50%, -50%) scale(1.2); }
900
+ }
901
+ .twk-bezier-handle.is-dragging { cursor: grabbing; transform: translate(-50%, -50%) scale(1.3); box-shadow: 0 3px 9px rgba(0, 0, 0, 0.45); }
902
+ .twk-bezier-fields { gap: 4px; }
903
+
904
+ /* Plot — a function graph with an editable, safely-parsed expression. */
905
+ .twk-plot { display: flex; flex-direction: column; gap: 6px; }
906
+ .twk-plot-label, .twk-spring-label, .twk-bezier-label { font-size: 12px; font-weight: 500; color: var(--twk-text-label); padding: 0; transition: color 0.15s; } /* flush with the full-width graph below */
907
+ /* As a panel row, a graph sits in a row's frame, as a radio grid does: 4px round the graph, the label
908
+ * 12px in like every row's. (The motion pane's editors aren't rows, and stay flush.) */
909
+ :is(.twk-controls, .twk-controls > [data-twk]) > :is(.twk-spring, .twk-bezier, .twk-plot) { padding: 4px; background: var(--twk-surface); border-radius: var(--twk-radius); } /* a markup panel's control sits in its host */
910
+ /* A 24px line for the label: in the 4px frame its text lands 9px down, where a
911
+ * 32px row's centered label does. */
912
+ :is(.twk-controls, .twk-controls > [data-twk]) > :is(.twk-spring, .twk-bezier, .twk-plot) > :is(.twk-spring-label, .twk-bezier-label, .twk-plot-label) { padding-left: 8px; line-height: 24px; }
913
+ .twk-plot-graph { position: relative; width: 100%; height: 140px; background: var(--twk-surface); border-radius: var(--twk-radius); overflow: hidden; box-shadow: inset 0 0 0 1px var(--twk-border); }
914
+ .twk-plot-svg { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
915
+ .twk-plot-axis { stroke: var(--twk-border); stroke-width: 1; }
916
+ .twk-plot-curve { fill: none; stroke: var(--twk-accent, var(--twk-text-primary)); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
917
+ .twk-plot-field { display: flex; align-items: center; gap: 7px; height: var(--twk-row-height); padding: 0 10px 0 12px; background: var(--twk-surface); border-radius: var(--twk-radius); box-shadow: inset 0 0 0 1px transparent; transition: box-shadow 0.15s; } /* the row idiom: 12 left, 10 right */
918
+ .twk-plot-field:focus-within { box-shadow: inset 0 0 0 1px var(--twk-border-hover); }
919
+ .twk-plot-fx { flex: none; font-size: 12px; color: var(--twk-text-tertiary); font-variant-numeric: tabular-nums; -webkit-user-select: none; user-select: none; }
920
+ .twk-plot-input { flex: 1; min-width: 0; padding: 0; background: none; border: none; outline: none; color: var(--twk-text-primary); font-family: var(--twk-font-mono); font-size: 12px; }
921
+ .twk-plot-input::placeholder { color: var(--twk-text-tertiary); }
922
+ .twk-plot.is-invalid .twk-plot-curve { opacity: 0.25; }
923
+ .twk-plot.is-invalid .twk-plot-field { box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--twk-danger) 60%, transparent); }
924
+ .twk-plot.is-invalid .twk-plot-fx { color: color-mix(in srgb, var(--twk-danger) 80%, transparent); }
925
+
926
+ /* ── Scheme resolution ──────────────────────────────────────────────────────────
927
+ * Panels and portals follow the OS (color-scheme: light dark, above), dark when it asks for
928
+ * dark. A [data-twk-scheme="dark"] or "light" pin on the node or any ancestor sets the scheme
929
+ * instead, and a light pin outranks a dark one wherever they nest: it comes later at the same
930
+ * specificity, so a dark pin inside a light scope cannot re-darken its subtree. The pins set
931
+ * color-scheme on the kit's own nodes only, never on the host element that carries the
932
+ * attribute. The :where() keeps both rules at base class specificity. */
933
+ .twk-panel:where([data-twk-scheme="dark"], [data-twk-scheme="dark"] *), .twk-portal:where([data-twk-scheme="dark"], [data-twk-scheme="dark"] *) { color-scheme: dark; }
934
+ .twk-panel:where([data-twk-scheme="light"], [data-twk-scheme="light"] *), .twk-portal:where([data-twk-scheme="light"], [data-twk-scheme="light"] *) { color-scheme: light; }
935
+ /* The one rule light-dark() can't carry: on light the slider handle takes an edge for definition, and on dark it has no
936
+ * shadow at all (a transparent one still moves the antialiasing of its rounded ends). So it picks the scheme the way the
937
+ * pins above do: light under the OS unless a dark pin covers it, or under a light pin anywhere. */
938
+ @media (prefers-color-scheme: light) { .twk-slider-handle:where(:not([data-twk-scheme="dark"] *)) { box-shadow: 0 0 0 0.5px rgba(0, 0, 0, 0.08); } }
939
+ .twk-slider-handle:where([data-twk-scheme="light"] *) { box-shadow: 0 0 0 0.5px rgba(0, 0, 0, 0.08); }
940
+ /* Tabular figures on the readouts. After the control rules: `font: inherit` also clears
941
+ * font-variant-numeric, so this has to win on source order. */
942
+ .twk-slider-value, .twk-slider-input, .twk-fps-val, .twk-num, .twk-grab-bubble,
943
+ .twk-trigger-value, .twk-color-gamut, .twk-color-chan-input, .twk-sides-sum {
944
+ font-variant-numeric: tabular-nums;
945
+ }
946
+
947
+ @media (prefers-reduced-motion: reduce) {
948
+ .twk-panel, .twk-panel *, .twk-portal, .twk-portal * { transition: none !important; }
949
+ /* the transition kill doesn't touch animations */
950
+ .twk-tabs-page[data-active="true"], .twk-grab-bubble { animation: none; }
951
+ }
952
+
953
+ /* Forced colors flatten every background, so state shown only by background needs an
954
+ * outline or border in the system Highlight, which survives. */
955
+ @media (forced-colors: active) {
956
+ .twk-seg-pill, .twk-tabs-pill, .twk-slider-fill { outline: 2px solid Highlight; outline-offset: -2px; }
957
+ .twk-radiogrid-btn[data-active="true"] { outline: 2px solid Highlight; outline-offset: -2px; }
958
+ .twk-image-tile[data-active="true"] { outline-color: Highlight; }
959
+ .twk-gradient-stop[data-sel="true"] { border-color: Highlight; }
960
+ .twk-select-option:focus-visible { outline: 2px solid Highlight; outline-offset: -2px; } /* the roving focus is a background, flattened here */
961
+ /* Where the color is the content, keep it: a swatch, strip, bar or thumb flattened to Canvas shows nothing. */
962
+ .twk-color-swatch, .twk-option-swatch, .twk-image-thumb, .twk-gradient-preview, .twk-gradient-bar, .twk-gradient-stop, .twk-wg-area, .twk-wg-hue, .twk-wg-alpha, .twk-pad-thumb { forced-color-adjust: none; }
963
+ /* …but their focus ring is still the system's: opting out would leave it in the accent, which may match the flattened background. */
964
+ .twk-gradient-stop:focus-visible, .twk-wg-area:focus-visible, .twk-wg-hue:focus-visible, .twk-wg-alpha:focus-visible { outline-color: Highlight; }
965
+ }