- overlays Nuxt UI (UModal/USlideover) : géométrie posée en CSS (assets/css/overlays.css) — les utilitaires tailwind attendus par @nuxt/ui ne sont pas générés (UnoCSS ne scanne pas node_modules) : drawer mobile et modales de confirmation se rendaient hors écran - a11y : title/description sur les 3 overlays (DialogTitle/Description rendus, warnings reka-ui éteints), fermeture du drawer ancrée et testée - suppression du collectif actif : bascule sur un collectif restant — jamais d'accueil orphelin (test store ajouté, 343 vitest verts) - observatoire : la route « transmis » entre dans les barres (base 100 %) - overflows mobiles à zéro : formules KaTeX (défilement), table vigueur (défilement < 768px), pill cible (retour à la ligne), inputs decider et mandat (border-box) - vérifié navigateur : drawer, modale, 14 routes sans débordement, zéro erreur console Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
525 lines
19 KiB
Vue
525 lines
19 KiB
Vue
<script setup lang="ts">
|
|
// <!-- ld-v2 --> L'établi de l'Atelier — porté du FormulaEditor v1 (sliders,
|
|
// exposants Forgeron/Comité) et du FormulaDisplay v1 (rendu KaTeX), branché sur
|
|
// l'UNIQUE moteur ~/engine (wotThreshold, smithThreshold, techcommThreshold).
|
|
// La seule maison des lettres : W, T, M, B, G, C n'apparaissent qu'ici.
|
|
import { wotThreshold, smithThreshold, techcommThreshold } from '~/engine'
|
|
import { INERTIA_LABELS, thresholdSentence } from '~/lexicon'
|
|
import type { AtelierInit } from './textsModel'
|
|
import { INERTIA_COLORS, INERTIA_ORDER, INERTIA_PARAMS, formatInt } from './textsModel'
|
|
|
|
const props = defineProps<{ initial?: AtelierInit }>()
|
|
|
|
// ── L'état de l'établi ───────────────────────────────────────
|
|
const W = ref(Math.max(1, Math.round(props.initial?.W ?? 100)))
|
|
const T = ref(Math.max(0, Math.round(props.initial?.T ?? Math.max(1, Math.round(W.value / 3)))))
|
|
const M = ref(props.initial?.M ?? 50)
|
|
const B = ref(props.initial?.B ?? 0.1)
|
|
const G = ref(props.initial?.G ?? 0.2)
|
|
const C = ref(props.initial?.C ?? 0)
|
|
const withSmith = ref(props.initial?.S !== undefined)
|
|
const S = ref(props.initial?.S ?? 0.1)
|
|
const smithW = ref(12)
|
|
const withTech = ref(false)
|
|
const techExp = ref(0.1)
|
|
const techW = ref(6)
|
|
|
|
watch(W, (w) => { if (T.value > w) T.value = w })
|
|
|
|
/** Cas Forgeron 2026 — un clic : W=7224, T=120 ⇒ 94 pour requis. */
|
|
function loadForgeron2026(): void {
|
|
W.value = 7224
|
|
T.value = 120
|
|
M.value = 50
|
|
B.value = 0.1
|
|
G.value = 0.2
|
|
C.value = 0
|
|
withSmith.value = true
|
|
S.value = 0.1
|
|
withTech.value = false
|
|
}
|
|
|
|
// ── Les seuils — source unique ~/engine ──────────────────────
|
|
const threshold = computed(() => wotThreshold(W.value, T.value, M.value, B.value, G.value, C.value))
|
|
const thresholdPct = computed(() =>
|
|
T.value > 0 ? Math.round((threshold.value / T.value) * 100) : null)
|
|
const sentence = computed(() =>
|
|
T.value > 0 ? thresholdSentence(W.value, T.value, threshold.value) : null)
|
|
const smithRequired = computed(() =>
|
|
withSmith.value ? smithThreshold(Math.max(1, smithW.value), S.value) : null)
|
|
const techRequired = computed(() =>
|
|
withTech.value ? techcommThreshold(Math.max(1, techW.value), techExp.value) : null)
|
|
|
|
// ── KaTeX (chargé globalement — repli <code> sinon) ──────────
|
|
const MAIN_TEX = 'Seuil = C + B^{W} + \\left(M + (1-M)\\cdot\\left(1 - \\left(\\tfrac{T}{W}\\right)^{G}\\right)\\right)\\cdot\\max(0,\\,T - C)'
|
|
const katexTick = ref(0)
|
|
onMounted(() => {
|
|
const w = window as unknown as { katex?: unknown }
|
|
if (w.katex) return
|
|
const timer = setInterval(() => {
|
|
if (w.katex) { katexTick.value++; clearInterval(timer) }
|
|
}, 200)
|
|
setTimeout(() => clearInterval(timer), 6000)
|
|
})
|
|
function renderTex(tex: string): string {
|
|
const w = window as unknown as {
|
|
katex?: { renderToString: (t: string, o: object) => string }
|
|
}
|
|
if (typeof window !== 'undefined' && w.katex) {
|
|
return w.katex.renderToString(tex, { throwOnError: false, displayMode: true })
|
|
}
|
|
return `<code class="lab__tex-fallback">${tex}</code>`
|
|
}
|
|
const mainFormulaHtml = computed(() => { void katexTick.value; return renderTex(MAIN_TEX) })
|
|
const smithFormulaHtml = computed(() => {
|
|
void katexTick.value
|
|
return withSmith.value ? renderTex(`Seuil_{forgerons} = \\lceil ${smithW.value}^{${S.value}} \\rceil = ${smithRequired.value}`) : null
|
|
})
|
|
const techFormulaHtml = computed(() => {
|
|
void katexTick.value
|
|
return withTech.value ? renderTex(`Seuil_{comit\\acute{e}} = \\lceil ${techW.value}^{${techExp.value}} \\rceil = ${techRequired.value}`) : null
|
|
})
|
|
|
|
// ── Table de participation : T croissant sur W donné ─────────
|
|
const PARTICIPATION_STEPS = [1, 2, 5, 10, 25, 50, 75, 100]
|
|
const participationRows = computed(() => {
|
|
const seen = new Set<number>()
|
|
return PARTICIPATION_STEPS.flatMap((pct) => {
|
|
const votes = Math.max(1, Math.round((W.value * pct) / 100))
|
|
if (seen.has(votes)) return []
|
|
seen.add(votes)
|
|
const required = wotThreshold(W.value, votes, M.value, B.value, G.value, C.value)
|
|
return [{
|
|
pct,
|
|
votes,
|
|
required,
|
|
ratio: Math.round((required / votes) * 100),
|
|
isCurrent: Math.abs(votes - T.value) === Math.min(
|
|
...PARTICIPATION_STEPS.map(p => Math.abs(Math.max(1, Math.round((W.value * p) / 100)) - T.value)),
|
|
),
|
|
}]
|
|
})
|
|
})
|
|
|
|
// ── Presets d'inertie comparés (câblage réel du domaine) ─────
|
|
const presetRows = computed(() => INERTIA_ORDER.map((preset) => {
|
|
const params = INERTIA_PARAMS[preset]
|
|
const required = T.value > 0
|
|
? wotThreshold(W.value, T.value, params.majorityPct, 0.1, params.gradientExponent, 0)
|
|
: 0
|
|
return {
|
|
preset,
|
|
label: INERTIA_LABELS[preset],
|
|
color: INERTIA_COLORS[preset],
|
|
majorityPct: params.majorityPct,
|
|
gradient: params.gradientExponent,
|
|
required,
|
|
ratio: T.value > 0 ? Math.round((required / T.value) * 100) : 0,
|
|
active: params.majorityPct === M.value && params.gradientExponent === G.value,
|
|
}
|
|
}))
|
|
|
|
/** Courbes seuil (%) / participation — portées de l'InertiaSlider v1. */
|
|
function curvePath(majorityPct: number, gradient: number): string {
|
|
const m = majorityPct / 100
|
|
const points: string[] = []
|
|
for (let i = 0; i <= 40; i++) {
|
|
const participation = i / 40
|
|
const ratio = m + (1 - m) * (1 - participation ** gradient)
|
|
points.push(`${(30 + participation * 170).toFixed(1)},${(10 + (1 - ratio) * 70).toFixed(1)}`)
|
|
}
|
|
return `M ${points.join(' L ')}`
|
|
}
|
|
const presetCurves = computed(() => presetRows.value.map(row => ({
|
|
...row,
|
|
path: curvePath(row.majorityPct, row.gradient),
|
|
})))
|
|
const currentCurve = computed(() => curvePath(M.value, G.value))
|
|
</script>
|
|
|
|
<template>
|
|
<!-- ld-v2 -->
|
|
<section class="lab">
|
|
<!-- La formule, rendue -->
|
|
<div class="lab__formula ld-card">
|
|
<div class="lab__tex" v-html="mainFormulaHtml" />
|
|
<div class="lab__legend">
|
|
<span><strong>W</strong> membres éligibles</span>
|
|
<span><strong>T</strong> votes exprimés</span>
|
|
<span><strong>M</strong> majorité cible</span>
|
|
<span><strong>B</strong> plancher dynamique</span>
|
|
<span><strong>G</strong> gradient d'inertie</span>
|
|
<span><strong>C</strong> plancher fixe</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="lab__grid">
|
|
<!-- Les réglages -->
|
|
<div class="lab__controls ld-card">
|
|
<div class="lab__scenario">
|
|
<label class="lab__field">
|
|
<span class="lab__field-label">W — membres éligibles</span>
|
|
<input v-model.number="W" type="number" min="1" max="1000000" class="lab__number">
|
|
</label>
|
|
<label class="lab__field">
|
|
<span class="lab__field-label">T — votes exprimés</span>
|
|
<input v-model.number="T" type="number" min="0" :max="W" class="lab__number">
|
|
</label>
|
|
<button type="button" class="ld-btn ld-btn--ghost lab__forgeron" @click="loadForgeron2026">
|
|
<UIcon name="i-lucide-hammer" />
|
|
Cas Forgeron 2026
|
|
</button>
|
|
</div>
|
|
|
|
<label class="lab__slider">
|
|
<span class="lab__slider-head">
|
|
<span>M — majorité cible</span><strong>{{ M }} %</strong>
|
|
</span>
|
|
<input v-model.number="M" type="range" min="0" max="100" step="1">
|
|
</label>
|
|
<label class="lab__slider">
|
|
<span class="lab__slider-head">
|
|
<span>B — plancher dynamique</span><strong>{{ B }}</strong>
|
|
</span>
|
|
<input v-model.number="B" type="range" min="0.01" max="1" step="0.01">
|
|
</label>
|
|
<label class="lab__slider">
|
|
<span class="lab__slider-head">
|
|
<span>G — gradient d'inertie</span><strong>{{ G }}</strong>
|
|
</span>
|
|
<input v-model.number="G" type="range" min="0.01" max="2" step="0.01">
|
|
</label>
|
|
<label class="lab__slider">
|
|
<span class="lab__slider-head">
|
|
<span>C — plancher fixe</span><strong>{{ C }}</strong>
|
|
</span>
|
|
<input v-model.number="C" type="range" min="0" max="100" step="1">
|
|
</label>
|
|
|
|
<div class="lab__optional">
|
|
<label class="lab__check">
|
|
<input v-model="withSmith" type="checkbox">
|
|
<span>Critère Forgeron</span>
|
|
</label>
|
|
<div v-if="withSmith" class="lab__sub">
|
|
<label class="lab__slider">
|
|
<span class="lab__slider-head"><span>S — exposant</span><strong>{{ S }}</strong></span>
|
|
<input v-model.number="S" type="range" min="0.01" max="1" step="0.01">
|
|
</label>
|
|
<label class="lab__field lab__field--inline">
|
|
<span class="lab__field-label">forgerons actifs</span>
|
|
<input v-model.number="smithW" type="number" min="1" max="10000" class="lab__number">
|
|
</label>
|
|
</div>
|
|
<label class="lab__check">
|
|
<input v-model="withTech" type="checkbox">
|
|
<span>Critère Comité technique</span>
|
|
</label>
|
|
<div v-if="withTech" class="lab__sub">
|
|
<label class="lab__slider">
|
|
<span class="lab__slider-head"><span>exposant</span><strong>{{ techExp }}</strong></span>
|
|
<input v-model.number="techExp" type="range" min="0.01" max="1" step="0.01">
|
|
</label>
|
|
<label class="lab__field lab__field--inline">
|
|
<span class="lab__field-label">membres du comité</span>
|
|
<input v-model.number="techW" type="number" min="1" max="1000" class="lab__number">
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Le résultat -->
|
|
<div class="lab__result ld-card">
|
|
<p class="lab__result-heading">Le seuil, ici et maintenant</p>
|
|
<p class="lab__result-figure">
|
|
<span class="lab__result-number">{{ formatInt(threshold) }}</span>
|
|
<span class="lab__result-unit">pour requis</span>
|
|
</p>
|
|
<p v-if="thresholdPct !== null" class="lab__result-pct">
|
|
soit {{ thresholdPct }} % des {{ formatInt(T) }} votes exprimés
|
|
</p>
|
|
<p v-if="sentence" class="lab__result-sentence">{{ sentence }}</p>
|
|
<div v-if="smithFormulaHtml" class="lab__criterion" v-html="smithFormulaHtml" />
|
|
<div v-if="techFormulaHtml" class="lab__criterion" v-html="techFormulaHtml" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Table de participation -->
|
|
<div class="lab__table-card ld-card">
|
|
<p class="lab__block-title">Le seuil descend quand la participation monte</p>
|
|
<p class="lab__block-sub">T croissant, sur une toile de {{ formatInt(W) }} membres</p>
|
|
<div class="lab__table-scroll">
|
|
<table class="lab__table">
|
|
<thead>
|
|
<tr>
|
|
<th>participation</th>
|
|
<th>T</th>
|
|
<th>seuil</th>
|
|
<th>% des exprimés</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr
|
|
v-for="row in participationRows"
|
|
:key="row.pct"
|
|
:class="{ 'lab__row--current': row.isCurrent }"
|
|
>
|
|
<td>{{ row.pct }} %</td>
|
|
<td>{{ formatInt(row.votes) }}</td>
|
|
<td class="lab__cell-strong">{{ formatInt(row.required) }}</td>
|
|
<td>{{ row.ratio }} %</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Presets d'inertie comparés -->
|
|
<div class="lab__presets ld-card">
|
|
<p class="lab__block-title">Les quatre inerties, comparées</p>
|
|
<p class="lab__block-sub">
|
|
mêmes W et T — seule l'inertie change ; ce sont les presets réels des clauses
|
|
</p>
|
|
<svg viewBox="0 0 230 100" class="lab__curves" role="img" aria-label="Courbes de seuil par inertie">
|
|
<line x1="30" y1="10" x2="30" y2="80" class="lab__axis" />
|
|
<line x1="30" y1="80" x2="200" y2="80" class="lab__axis" />
|
|
<path
|
|
v-for="curve in presetCurves"
|
|
:key="curve.preset"
|
|
:d="curve.path"
|
|
fill="none"
|
|
:style="{ stroke: curve.color }"
|
|
:stroke-width="curve.active ? 2.6 : 1.4"
|
|
:opacity="curve.active ? 1 : 0.55"
|
|
stroke-linecap="round"
|
|
/>
|
|
<path
|
|
v-if="!presetCurves.some(curve => curve.active)"
|
|
:d="currentCurve"
|
|
fill="none"
|
|
class="lab__curve-current"
|
|
stroke-width="2.2"
|
|
stroke-dasharray="5 4"
|
|
/>
|
|
<text x="14" y="14" class="lab__axis-label">100%</text>
|
|
<text x="18" y="84" class="lab__axis-label">M</text>
|
|
<text x="30" y="94" class="lab__axis-label">0%</text>
|
|
<text x="180" y="94" class="lab__axis-label">100%</text>
|
|
<text x="110" y="99" class="lab__axis-title">participation T/W</text>
|
|
<text x="4" y="52" class="lab__axis-title" transform="rotate(-90, 8, 52)">seuil</text>
|
|
</svg>
|
|
<div class="lab__preset-rows">
|
|
<button
|
|
v-for="row in presetRows"
|
|
:key="row.preset"
|
|
type="button"
|
|
class="lab__preset"
|
|
:class="{ 'lab__preset--active': row.active }"
|
|
:style="{ '--preset-color': row.color }"
|
|
@click="M = row.majorityPct; G = row.gradient; B = 0.1; C = 0"
|
|
>
|
|
<span class="lab__preset-dot" />
|
|
<span class="lab__preset-label">{{ row.label }}</span>
|
|
<span class="lab__preset-params">M{{ row.majorityPct }} · G{{ row.gradient }}</span>
|
|
<span class="lab__preset-required">{{ formatInt(row.required) }} pour ({{ row.ratio }} %)</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.lab { display: flex; flex-direction: column; gap: 1rem; }
|
|
|
|
.lab__formula { padding: clamp(0.9rem, 3vw, 1.4rem); overflow-x: auto; }
|
|
.lab__tex { text-align: center; max-width: 100%; overflow-x: auto; }
|
|
.lab__tex :deep(.lab__tex-fallback) {
|
|
font-family: monospace;
|
|
font-size: 0.8125rem;
|
|
color: var(--mood-text);
|
|
}
|
|
.lab__legend {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
justify-content: center;
|
|
gap: 0.4rem 1rem;
|
|
margin-top: 0.6rem;
|
|
font-size: 0.6875rem;
|
|
color: var(--mood-text-muted);
|
|
}
|
|
.lab__legend strong { color: var(--mood-accent); font-family: monospace; }
|
|
|
|
.lab__grid { display: grid; grid-template-columns: 1fr; gap: 1rem; }
|
|
@media (min-width: 768px) { .lab__grid { grid-template-columns: 3fr 2fr; } }
|
|
|
|
.lab__controls {
|
|
padding: clamp(0.9rem, 3vw, 1.3rem);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.9rem;
|
|
}
|
|
.lab__scenario { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: flex-end; }
|
|
.lab__field { display: flex; flex-direction: column; gap: 0.25rem; }
|
|
.lab__field--inline { flex-direction: row; align-items: center; gap: 0.5rem; }
|
|
.lab__field-label {
|
|
font-size: 0.71875rem;
|
|
font-weight: 700;
|
|
color: var(--mood-text-muted);
|
|
}
|
|
.lab__number {
|
|
width: 7.5rem;
|
|
padding: 0.4rem 0.7rem;
|
|
font-size: 0.9375rem;
|
|
font-weight: 700;
|
|
font-variant-numeric: tabular-nums;
|
|
box-shadow: inset 0 0 0 1.5px var(--mood-input-border);
|
|
}
|
|
.lab__forgeron { font-size: 0.8125rem; }
|
|
|
|
.lab__slider { display: flex; flex-direction: column; gap: 0.3rem; }
|
|
.lab__slider-head {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
font-size: 0.8125rem;
|
|
color: var(--mood-text-muted);
|
|
}
|
|
.lab__slider-head strong {
|
|
color: var(--mood-accent);
|
|
font-variant-numeric: tabular-nums;
|
|
font-family: monospace;
|
|
}
|
|
.lab__slider input[type='range'] {
|
|
width: 100%;
|
|
accent-color: var(--mood-accent);
|
|
background: transparent;
|
|
min-height: 1.4rem;
|
|
}
|
|
|
|
.lab__optional {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.6rem;
|
|
padding-top: 0.75rem;
|
|
box-shadow: 0 -1px 0 color-mix(in srgb, var(--mood-text) 8%, transparent);
|
|
}
|
|
.lab__check {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
font-size: 0.875rem;
|
|
font-weight: 700;
|
|
color: var(--mood-text);
|
|
cursor: pointer;
|
|
}
|
|
.lab__check input { accent-color: var(--mood-accent); width: 1rem; height: 1rem; }
|
|
.lab__sub {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
padding-left: 1rem;
|
|
}
|
|
|
|
.lab__result {
|
|
padding: clamp(0.9rem, 3vw, 1.3rem);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
background:
|
|
linear-gradient(150deg, var(--mood-accent-soft), transparent 60%),
|
|
var(--mood-surface);
|
|
}
|
|
.lab__result-heading {
|
|
font-size: 0.6875rem;
|
|
font-weight: 800;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.07em;
|
|
color: var(--mood-text-muted);
|
|
}
|
|
.lab__result-figure { display: flex; align-items: baseline; gap: 0.5rem; }
|
|
.lab__result-number {
|
|
font-size: clamp(2.2rem, 7vw, 3rem);
|
|
font-weight: 800;
|
|
color: var(--mood-accent);
|
|
font-variant-numeric: tabular-nums;
|
|
line-height: 1;
|
|
}
|
|
.lab__result-unit { font-size: 0.9375rem; font-weight: 700; color: var(--mood-text); }
|
|
.lab__result-pct { font-size: 0.875rem; color: var(--mood-text-muted); }
|
|
.lab__result-sentence {
|
|
font-size: 0.8125rem;
|
|
line-height: 1.55;
|
|
color: var(--mood-text);
|
|
padding: 0.6rem 0.8rem;
|
|
border-radius: var(--r-input);
|
|
background: color-mix(in srgb, var(--mood-text) 4%, transparent);
|
|
}
|
|
.lab__criterion { overflow-x: auto; font-size: 0.875rem; }
|
|
|
|
.lab__table-card, .lab__presets { padding: clamp(0.9rem, 3vw, 1.3rem); }
|
|
.lab__block-title { font-size: 0.9375rem; font-weight: 800; color: var(--mood-text); }
|
|
.lab__block-sub { font-size: 0.75rem; color: var(--mood-text-muted); margin-bottom: 0.7rem; }
|
|
.lab__table-scroll { overflow-x: auto; }
|
|
.lab__table { width: 100%; border-collapse: collapse; font-size: 0.8125rem; }
|
|
.lab__table th {
|
|
text-align: left;
|
|
font-size: 0.6875rem;
|
|
font-weight: 800;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
color: var(--mood-text-muted);
|
|
padding: 0.35rem 0.6rem;
|
|
}
|
|
.lab__table td {
|
|
padding: 0.35rem 0.6rem;
|
|
font-variant-numeric: tabular-nums;
|
|
color: var(--mood-text);
|
|
}
|
|
.lab__table tbody tr:nth-child(odd) td {
|
|
background: color-mix(in srgb, var(--mood-text) 3%, transparent);
|
|
}
|
|
.lab__row--current td { background: var(--mood-accent-soft) !important; font-weight: 700; }
|
|
.lab__cell-strong { font-weight: 800; color: var(--mood-accent); }
|
|
|
|
.lab__curves { width: 100%; max-width: 26rem; height: auto; margin: 0 auto 0.5rem; display: block; }
|
|
.lab__axis { stroke: color-mix(in srgb, var(--mood-text) 25%, transparent); stroke-width: 1; }
|
|
.lab__axis-label { font-size: 5.5px; fill: var(--mood-text-muted); font-family: monospace; }
|
|
.lab__axis-title { font-size: 5.5px; fill: var(--mood-text-muted); font-weight: 600; }
|
|
.lab__curve-current { stroke: var(--mood-text); }
|
|
|
|
.lab__preset-rows { display: flex; flex-direction: column; gap: 0.4rem; }
|
|
.lab__preset {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.6rem;
|
|
flex-wrap: wrap;
|
|
padding: 0.55rem 0.8rem;
|
|
border-radius: var(--r-input);
|
|
background: color-mix(in srgb, var(--preset-color) 6%, transparent);
|
|
cursor: pointer;
|
|
text-align: left;
|
|
transition: transform 0.1s ease, box-shadow 0.1s ease;
|
|
}
|
|
.lab__preset:hover { transform: translateY(-1px); box-shadow: 0 3px 10px var(--mood-shadow); }
|
|
.lab__preset--active { box-shadow: 0 0 0 2px var(--preset-color); }
|
|
.lab__preset-dot {
|
|
width: 9px;
|
|
height: 9px;
|
|
border-radius: 50%;
|
|
background: var(--preset-color);
|
|
flex-shrink: 0;
|
|
}
|
|
.lab__preset-label { font-size: 0.8125rem; font-weight: 800; color: var(--mood-text); }
|
|
.lab__preset-params {
|
|
font-size: 0.6875rem;
|
|
font-family: monospace;
|
|
color: var(--mood-text-muted);
|
|
}
|
|
.lab__preset-required {
|
|
margin-left: auto;
|
|
font-size: 0.8125rem;
|
|
font-weight: 700;
|
|
font-variant-numeric: tabular-nums;
|
|
color: var(--preset-color);
|
|
}
|
|
</style>
|