v2 : les 14 écrans — le tour complet de la démocratie d'exercice

- Aujourd'hui (Fil 13 sections + capture sticky) + Le chemin (tunnel 2 gestes,
  Q0 inline, 3 chips, dérogation asymétrique, alternatives réglage/consignation)
- Registre + fiche décision (timeline, périmètre premier/second lieu auditable,
  affluence non-ignorable, S'instruire condensé, éléments + cartographie de
  clôture, épreuve du réel, Remettre en question, PV A4, gravure)
- Salle de vote 5 modalités (consentement, nuancé+histogramme, binaire hérité
  avec jauge inertielle, Réglage collectif complet — faisceau, médiane basse,
  Pour moi, Explorer, cristallisation-geste —, élection à départage humain)
- Textes (Pacte en clair, document vivant, diff, vue projetée, Atelier des
  formules porté du v1 sur le moteur unique) + Mandats (faits comptés, feux
  de la rampe, wizard 3 étapes) + Observatoire (consigner→observer→protocoliser)
- Onboarding 7 gabarits + Données locales (export/import, attributs, atelier)
- voting→framing gardé (Reformuler d'un réglage figé non cristallisé)
- Pages v1 supprimées (login, documents, mandates, protocols, sanctuary,
  tools, decisions/new) — 342 tests verts, build zéro erreur

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-08-11 13:47:20 +02:00
co-authored by Claude Fable 5
parent d886302b59
commit e164b5f6c1
96 changed files with 14097 additions and 11777 deletions
+255
View File
@@ -0,0 +1,255 @@
<script setup lang="ts">
// <!-- ld-v2 --> /mandats/[id] — le pouvoir confié, visible et reprenable.
// Origine cliquable (le mandat EST né d'une décision), faits comptés (Δ7),
// feux de la rampe, révocation en un geste au protocole d'origine.
import { useCollectiveStore } from '~/stores/collective'
import { MANDATE_REVOKE, NOMINATION_LABELS } from '~/lexicon'
import {
MANDATE_STATUS_LABELS,
MANDATE_STATUS_PILL,
} from '~/components/mandates/mandateUi'
const route = useRoute()
const col = useCollectiveStore()
const mandate = computed(() =>
col.mandates.find(m => m.id === String(route.params.id)),
)
const holder = computed(() =>
col.people.find(p => p.id === mandate.value?.holderId),
)
const originDecision = computed(() =>
col.decisions.find(d => d.id === mandate.value?.originDecisionId),
)
const domainCircles = computed(() =>
(mandate.value?.domain.circleIds ?? [])
.map(id => col.circles.find(c => c.id === id))
.filter(c => c !== undefined),
)
const electorCircle = computed(() =>
col.circles.find(c => c.id === mandate.value?.electorCircleId),
)
const revokeTo = computed(() =>
mandate.value ? `/decider?mandat=${mandate.value.id}&chain=revocation` : '/decider',
)
</script>
<template>
<!-- ld-v2 -->
<div v-if="mandate" class="mfiche">
<NuxtLink to="/mandats" class="mfiche__back">
<UIcon name="i-lucide-arrow-left" />
Mandats
</NuxtLink>
<!-- En-tête : le pouvoir, explicite -->
<header class="mfiche__head ld-card">
<div class="mfiche__title-row">
<h1 class="mfiche__title">{{ mandate.title }}</h1>
<span class="status-pill" :class="MANDATE_STATUS_PILL[mandate.status]">
{{ MANDATE_STATUS_LABELS[mandate.status] }}
</span>
</div>
<div v-if="holder" class="mfiche__holder">
<LdAvatarStack :people="[{ person: holder }]" :size="38" />
<div>
<p class="mfiche__holder-name">{{ holder.displayName }}</p>
<p class="mfiche__holder-role">titulaire</p>
</div>
</div>
<dl class="mfiche__meta">
<div class="mfiche__meta-row">
<dt>Origine</dt>
<dd>
<NuxtLink
v-if="originDecision"
:to="`/decisions/${originDecision.id}`"
class="mfiche__origin"
>
<UIcon name="i-lucide-scale" />
{{ originDecision.title }}
</NuxtLink>
<span v-else class="mfiche__muted">décision fondatrice hors de ce fichier</span>
</dd>
</div>
<div class="mfiche__meta-row">
<dt>Domaine</dt>
<dd class="mfiche__chips">
<span v-for="c in domainCircles" :key="c.id" class="mfiche__chip">
<UIcon name="i-lucide-circle-dashed" />
{{ c.name }}
</span>
<span v-for="tag in mandate.domain.tags" :key="tag" class="mfiche__tag">
#{{ tag }}
</span>
<span v-if="!domainCircles.length && !mandate.domain.tags.length" class="mfiche__muted">
domaine non précisé
</span>
</dd>
</div>
<div class="mfiche__meta-row">
<dt>Cercle électeur</dt>
<dd>
<span class="mfiche__chip">
<UIcon name="i-lucide-users-round" />
{{ electorCircle?.name ?? '—' }}
</span>
</dd>
</div>
<div class="mfiche__meta-row">
<dt>Nomination</dt>
<dd>{{ NOMINATION_LABELS[mandate.nominationMethod] }}</dd>
</div>
<div class="mfiche__meta-row mfiche__meta-row--period">
<dt>Période</dt>
<dd>
<MandatePeriodBar :starts-at="mandate.startsAt" :ends-at="mandate.endsAt" />
</dd>
</div>
</dl>
</header>
<!-- Exercice du mandat faits comptés -->
<MandateExercise :mandate="mandate" />
<!-- Feux de la rampe -->
<MandateSpotlights :mandate="mandate" />
<!-- Révocation en un geste jamais improvisée -->
<section v-if="mandate.status === 'active'" class="mfiche__revoke ld-card">
<NuxtLink :to="revokeTo" class="ld-btn ld-btn--ghost mfiche__revoke-btn">
<UIcon name="i-lucide-key-square" />
{{ MANDATE_REVOKE }}
</NuxtLink>
<p class="mfiche__revoke-note">
La révocation suit le protocole de nomination d'origine
({{ NOMINATION_LABELS[mandate.nominationMethod] }}), avec le cercle électeur
« {{ electorCircle?.name ?? '' }} » comme périmètre jamais plus coûteuse
que la nomination.
</p>
</section>
</div>
<div v-else class="mfiche mfiche--missing">
<p class="ld-card mfiche__missing-card">
Ce mandat est introuvable dans ce collectif.
<NuxtLink to="/mandats" class="mfiche__origin">Revenir aux mandats</NuxtLink>
</p>
</div>
</template>
<style scoped>
.mfiche {
max-width: 46rem;
margin: 0 auto;
width: 100%;
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.mfiche__back {
display: inline-flex;
align-items: center;
gap: 0.375rem;
align-self: flex-start;
font-size: 0.875rem;
font-weight: 600;
color: var(--mood-text-muted);
text-decoration: none;
}
.mfiche__back:hover { color: var(--mood-text); }
.mfiche__head {
padding: 1.375rem 1.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.mfiche__title-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 0.75rem;
flex-wrap: wrap;
}
.mfiche__title {
margin: 0;
font-size: clamp(1.25rem, 3.5vw, 1.625rem);
font-weight: 800;
line-height: 1.25;
letter-spacing: -0.01em;
}
.mfiche__holder { display: flex; align-items: center; gap: 0.75rem; }
.mfiche__holder-name { margin: 0; font-size: 1rem; font-weight: 700; }
.mfiche__holder-role { margin: 0; font-size: 0.8125rem; color: var(--mood-text-muted); }
.mfiche__meta { margin: 0; display: flex; flex-direction: column; gap: 0.625rem; }
.mfiche__meta-row {
display: grid;
grid-template-columns: 7.5rem 1fr;
gap: 0.75rem;
align-items: baseline;
}
.mfiche__meta-row dt {
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--mood-text-muted);
}
.mfiche__meta-row dd { margin: 0; font-size: 0.9375rem; min-width: 0; }
.mfiche__meta-row--period dd { padding-top: 0.25rem; }
@media (max-width: 479px) {
.mfiche__meta-row { grid-template-columns: 1fr; gap: 0.125rem; }
}
.mfiche__origin {
display: inline-flex;
align-items: center;
gap: 0.375rem;
font-weight: 600;
color: var(--mood-accent);
text-decoration: none;
}
.mfiche__origin:hover { text-decoration: underline; }
.mfiche__chips { display: flex; flex-wrap: wrap; gap: 0.375rem; align-items: center; }
.mfiche__chip {
display: inline-flex;
align-items: center;
gap: 0.25rem;
font-size: 0.8125rem;
font-weight: 600;
color: var(--mood-accent);
background: var(--mood-accent-soft);
padding: 2px 10px;
border-radius: var(--r-pill);
}
.mfiche__tag { font-size: 0.8125rem; color: var(--mood-text-muted); }
.mfiche__muted { color: var(--mood-text-muted); font-style: italic; font-size: 0.875rem; }
.mfiche__revoke {
padding: 1.125rem 1.375rem;
display: flex;
flex-direction: column;
gap: 0.625rem;
}
.mfiche__revoke-btn { align-self: flex-start; color: var(--mood-status-revoque); }
.mfiche__revoke-note {
margin: 0;
font-size: 0.8125rem;
line-height: 1.55;
color: var(--mood-text-muted);
}
.mfiche--missing { padding-top: 2rem; }
.mfiche__missing-card {
padding: 2rem;
text-align: center;
display: flex;
flex-direction: column;
gap: 0.75rem;
align-items: center;
}
</style>
+145
View File
@@ -0,0 +1,145 @@
<script setup lang="ts">
// <!-- ld-v2 --> /mandats — la carte des pouvoirs confiés : qui peut décider
// quoi, jusqu'à quand. Cartes par statut, compteurs bruts (Δ7 : aucune jauge).
import type { Mandate } from '~/types/domain'
import { useCollectiveStore } from '~/stores/collective'
import { MANDATES_TITLE, MANDATE_CLAIM } from '~/lexicon'
import { MANDATE_STATUS_LABELS } from '~/components/mandates/mandateUi'
const col = useCollectiveStore()
const ORDER: Mandate['status'][] = ['active', 'proposed', 'expired', 'revoked']
const groups = computed(() =>
ORDER
.map(status => ({
status,
label: MANDATE_STATUS_LABELS[status],
items: col.mandates
.filter(m => m.status === status)
.sort((a, b) => (a.endsAt < b.endsAt ? -1 : 1)),
}))
.filter(g => g.items.length > 0),
)
</script>
<template>
<!-- ld-v2 -->
<div class="mpage">
<header class="mpage__header">
<div>
<h1 class="mpage__title">{{ MANDATES_TITLE }}</h1>
<p class="mpage__sub">qui peut décider quoi, jusqu'à quand</p>
</div>
<NuxtLink to="/mandats/nouveau" class="ld-btn">
<UIcon name="i-lucide-key-round" />
{{ MANDATE_CLAIM }}
</NuxtLink>
</header>
<template v-if="groups.length">
<section v-for="group in groups" :key="group.status" class="mpage__group">
<h2 class="mpage__group-title">
{{ group.label }}
<span class="mpage__count">{{ group.items.length }}</span>
</h2>
<div class="mpage__grid">
<MandateCard v-for="m in group.items" :key="m.id" :mandate="m" />
</div>
</section>
</template>
<div v-else class="mpage__empty ld-card">
<UIcon name="i-lucide-key-round" class="mpage__empty-icon" />
<p class="mpage__empty-title">Aucun pouvoir confié pour l'instant.</p>
<p class="mpage__empty-text">
Un mandat naît d'une décision : un rôle, un domaine, une durée bornée
et des comptes à rendre.
</p>
<NuxtLink to="/mandats/nouveau" class="ld-btn">
<UIcon name="i-lucide-key-round" />
{{ MANDATE_CLAIM }}
</NuxtLink>
</div>
</div>
</template>
<style scoped>
.mpage {
max-width: 64rem;
margin: 0 auto;
width: 100%;
display: flex;
flex-direction: column;
gap: 1.75rem;
}
.mpage__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
}
.mpage__title {
margin: 0;
font-size: clamp(1.375rem, 3.5vw, 1.75rem);
font-weight: 800;
letter-spacing: -0.01em;
}
.mpage__sub {
margin: 0.25rem 0 0;
font-size: 0.9375rem;
font-style: italic;
color: var(--mood-text-muted);
}
.mpage__group { display: flex; flex-direction: column; gap: 0.875rem; }
.mpage__group-title {
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0;
font-size: 0.8125rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--mood-text-muted);
}
.mpage__count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.375rem;
height: 1.375rem;
padding: 0 0.375rem;
border-radius: var(--r-pill);
background: var(--mood-accent-soft);
color: var(--mood-accent);
font-size: 0.75rem;
font-variant-numeric: tabular-nums;
}
.mpage__grid {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
@media (min-width: 768px) {
.mpage__grid { grid-template-columns: 1fr 1fr; }
}
.mpage__empty {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.625rem;
padding: 3rem 1.5rem;
text-align: center;
}
.mpage__empty-icon { font-size: 2rem; color: var(--mood-accent); }
.mpage__empty-title { margin: 0; font-size: 1.0625rem; font-weight: 700; }
.mpage__empty-text {
margin: 0 0 0.625rem;
max-width: 26rem;
font-size: 0.9375rem;
color: var(--mood-text-muted);
line-height: 1.55;
}
</style>
+389
View File
@@ -0,0 +1,389 @@
<script setup lang="ts">
// <!-- ld-v2 --> /mandats/nouveau — wizard 3 étapes porté du v1 (la structure
// à pépite : pills de progression, cartes cliquables, transitions), branché sur
// le pivot : la nomination EST une décision (createsMandate, route collective).
// Pré-remplissage par query ?titre=&tags= (suggestion R6 du chemin).
import type { Id, MandateDraft, NominationMethod, TriageInput } from '~/types/domain'
import type { ApplyPathEdits } from '~/stores/decisions'
import { useCollectiveStore } from '~/stores/collective'
import { useDecisionsStore } from '~/stores/decisions'
import { MANDATE_CLAIM, NOMINATION_LABELS } from '~/lexicon'
const col = useCollectiveStore()
const decisions = useDecisionsStore()
const route = useRoute()
const STEPS = ['Rôle et domaine', 'Nomination', 'Durée et comptes'] as const
const step = ref(0)
// ── Étape 1 : rôle et domaine ──
const title = ref(typeof route.query.titre === 'string' ? route.query.titre : '')
const mission = ref('')
const circleIds = ref<Id[]>([])
const tags = ref<string[]>(
typeof route.query.tags === 'string'
? route.query.tags.split(',').map(t => t.trim()).filter(t => t.length > 0)
: [],
)
const tagInput = ref('')
function toggleCircle(id: Id): void {
circleIds.value = circleIds.value.includes(id)
? circleIds.value.filter(c => c !== id)
: [...circleIds.value, id]
}
function addTag(): void {
const tag = tagInput.value.trim().replace(/^#/, '').toLowerCase()
if (tag && !tags.value.includes(tag)) tags.value.push(tag)
tagInput.value = ''
}
function removeTag(tag: string): void {
tags.value = tags.value.filter(t => t !== tag)
}
// ── Étape 2 : modalité de nomination (6) ──
const method = ref<NominationMethod | null>(null)
// ── Étape 3 : durée bornée, cadence, cercle électeur ──
const durationDays = ref(180)
const withReports = ref(true)
const reportEveryDays = ref(90)
const electorCircleId = ref<Id>(col.current?.collective.rootCircleId ?? '')
const canNext = computed(() => {
if (step.value === 0) return title.value.trim().length > 0
if (step.value === 1) return method.value !== null
return durationDays.value >= 1 && electorCircleId.value !== ''
})
const electorCircle = computed(() => col.circles.find(c => c.id === electorCircleId.value))
const domainNames = computed(() =>
circleIds.value
.map(id => col.circles.find(c => c.id === id)?.name)
.filter(n => n !== undefined),
)
/** La modalité choisie résout le protocole via le Pacte (repli : consentement). */
function protocolFor(m: NominationMethod): Id | undefined {
const byRange = col.settings?.protocolByRange
if (!byRange) return undefined
if (m === 'election-no-candidate') return byRange.election ?? byRange.consent
if (m === 'nuanced-vote') return byRange.nuanced ?? byRange.consent
return byRange.consent // ratification par consentement (auto-désignation, tirage, rotation)
}
// ── Sortie : une Decision createsMandate, route collective ──
const submitting = ref(false)
const error = ref<string | null>(null)
function submit(): void {
if (!method.value || submitting.value) return
submitting.value = true
error.value = null
const captured = decisions.capture(`Confier le mandat « ${title.value.trim()} »`)
if ('ok' in captured) { error.value = captured.reason; submitting.value = false; return }
const scope = {
selfOnly: false,
// Le cercle électeur EN PREMIER : il devient le périmètre d'élection et de révocation.
circleIds: [electorCircleId.value, ...circleIds.value.filter(id => id !== electorCircleId.value)],
personIds: [] as Id[],
}
const input: TriageInput = {
title: captured.title,
tags: tags.value,
scope,
reversibility: 'costly',
weight: 'binding',
urgent: false,
}
const path = decisions.runTriage(input)
if ('ok' in path) { error.value = path.reason; submitting.value = false; return }
const draft: MandateDraft = {
title: title.value.trim(),
domainCircleIds: [...circleIds.value],
domainTags: [...tags.value],
durationDays: durationDays.value,
...(withReports.value ? { reportEveryDays: reportEveryDays.value } : {}),
}
const protocolId = protocolFor(method.value)
const edits: ApplyPathEdits = {
route: 'collective',
scope,
reversibility: 'costly',
weight: 'binding',
tags: [...tags.value],
visibility: 'collective',
createsMandate: draft,
...(mission.value.trim() ? { body: mission.value.trim() } : {}),
...(protocolId !== undefined ? { protocolId } : {}),
}
const applied = decisions.applyPath(captured, path, edits)
if ('ok' in applied) { error.value = applied.reason; submitting.value = false; return }
navigateTo(`/decisions/${applied.id}`)
}
</script>
<template>
<!-- ld-v2 -->
<div class="mwiz">
<nav class="mwiz__nav">
<button v-if="step > 0" type="button" class="mwiz__back" @click="step--">
<UIcon name="i-lucide-arrow-left" />
Retour
</button>
<NuxtLink v-else to="/mandats" class="mwiz__back">
<UIcon name="i-lucide-arrow-left" />
Mandats
</NuxtLink>
<div class="mwiz__progress">
<template v-for="(label, i) in STEPS" :key="label">
<span v-if="i > 0" class="mwiz__sep"></span>
<span
class="mwiz__pill"
:class="{ 'mwiz__pill--active': step === i, 'mwiz__pill--done': step > i }"
>
{{ i + 1 }} · {{ label }}
</span>
</template>
</div>
</nav>
<Transition name="slide-fade" mode="out-in">
<!-- ÉTAPE 1 : rôle et domaine -->
<div v-if="step === 0" key="role" class="mwiz__step">
<header class="mwiz__header">
<h1 class="mwiz__title">{{ MANDATE_CLAIM }}</h1>
<p class="mwiz__sub">Un rôle nommé, un domaine explicite le pouvoir se voit.</p>
</header>
<label class="mwiz__label" for="mwiz-title">Titre du rôle</label>
<input
id="mwiz-title"
v-model="title"
type="text"
class="mwiz__input"
lang="fr"
spellcheck="true"
placeholder="ex : Trésorerie, Animation du jardin…"
>
<label class="mwiz__label" for="mwiz-mission">La mission, en une phrase</label>
<input
id="mwiz-mission"
v-model="mission"
type="text"
class="mwiz__input"
lang="fr"
spellcheck="true"
placeholder="ex : Tenir les comptes et rendre la caisse lisible par tous."
>
<p class="mwiz__label">Cercles du domaine</p>
<div class="mwiz__chips">
<button
v-for="c in col.circles"
:key="c.id"
type="button"
class="mwiz__chip"
:class="{ 'mwiz__chip--on': circleIds.includes(c.id) }"
@click="toggleCircle(c.id)"
>
{{ c.name }}
</button>
</div>
<label class="mwiz__label" for="mwiz-tag">Tags du domaine</label>
<div class="mwiz__chips">
<span v-for="tag in tags" :key="tag" class="mwiz__chip mwiz__chip--on">
#{{ tag }}
<button type="button" class="mwiz__chip-x" :aria-label="`Retirer ${tag}`" @click="removeTag(tag)">
<UIcon name="i-lucide-x" />
</button>
</span>
<input
id="mwiz-tag"
v-model="tagInput"
type="text"
class="mwiz__input mwiz__input--tag"
placeholder="ajouter un tag ⏎"
@keydown.enter.prevent="addTag"
@blur="addTag"
>
</div>
</div>
<!-- ÉTAPE 2 : modalité de nomination -->
<div v-else-if="step === 1" key="nomination" class="mwiz__step">
<header class="mwiz__header">
<h1 class="mwiz__title">Comment nommer ?</h1>
<p class="mwiz__sub">Six modalités, chacune avec sa pédagogie le cercle électeur tranchera.</p>
</header>
<MandateNominationPicker v-model="method" />
</div>
<!-- ÉTAPE 3 : durée bornée, cadence, cercle électeur -->
<div v-else key="terms" class="mwiz__step">
<header class="mwiz__header">
<h1 class="mwiz__title">Borné, redevable</h1>
<p class="mwiz__sub">Un mandat a toujours une fin et des comptes à rendre avant.</p>
</header>
<label class="mwiz__label" for="mwiz-duration">Durée du mandat (jours)</label>
<div class="mwiz__row">
<input
id="mwiz-duration"
v-model.number="durationDays"
type="number"
class="mwiz__input mwiz__input--num"
min="7"
max="730"
>
<input
v-model.number="durationDays"
type="range"
class="mwiz__slider"
min="7"
max="730"
step="1"
aria-label="Durée du mandat en jours"
>
</div>
<label class="mwiz__check">
<input v-model="withReports" type="checkbox" class="mwiz__checkbox">
<span>Avec rapports réguliers</span>
</label>
<div v-if="withReports" class="mwiz__row">
<label class="mwiz__label mwiz__label--inline" for="mwiz-cadence">tous les</label>
<input
id="mwiz-cadence"
v-model.number="reportEveryDays"
type="number"
class="mwiz__input mwiz__input--num"
min="7"
:max="durationDays"
>
<span class="mwiz__unit">jours</span>
</div>
<label class="mwiz__label" for="mwiz-elector">Cercle électeur il nomme, il révoque</label>
<select id="mwiz-elector" v-model="electorCircleId" class="mwiz__input">
<option v-for="c in col.circles" :key="c.id" :value="c.id">{{ c.name }}</option>
</select>
<div class="mwiz__recap">
<p class="mwiz__recap-title">{{ title }}</p>
<p v-if="mission" class="mwiz__recap-line">{{ mission }}</p>
<p class="mwiz__recap-line">
{{ method ? NOMINATION_LABELS[method] : '—' }} ·
{{ durationDays }} jours<template v-if="withReports"> · rapport tous les {{ reportEveryDays }} jours</template>
</p>
<p v-if="domainNames.length || tags.length" class="mwiz__recap-line">
Domaine : {{ [...domainNames, ...tags.map(t => `#${t}`)].join(' · ') }}
</p>
<p class="mwiz__recap-note">
La nomination est une décision elle part au vote du cercle
« {{ electorCircle?.name ?? '—' }} ».
</p>
</div>
<p v-if="error" class="mwiz__error">{{ error }}</p>
</div>
</Transition>
<div class="mwiz__actions">
<button
v-if="step < 2"
type="button"
class="ld-btn"
:disabled="!canNext"
@click="step++"
>
Continuer
<UIcon name="i-lucide-arrow-right" />
</button>
<button
v-else
type="button"
class="ld-btn"
:disabled="!canNext || !method || submitting"
@click="submit"
>
<UIcon name="i-lucide-scale" />
{{ submitting ? 'Ouverture…' : 'Soumettre au cercle électeur' }}
</button>
</div>
</div>
</template>
<style scoped>
.mwiz { max-width: 46rem; margin: 0 auto; width: 100%; display: flex; flex-direction: column; gap: 1.5rem; }
.mwiz__nav { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; }
.mwiz__back {
display: inline-flex; align-items: center; gap: 0.375rem;
font-size: 0.875rem; font-weight: 600; color: var(--mood-text-muted);
background: none; cursor: pointer; text-decoration: none;
}
.mwiz__back:hover { color: var(--mood-text); }
.mwiz__progress { display: flex; align-items: center; gap: 0.375rem; margin-left: auto; flex-wrap: wrap; }
.mwiz__sep { color: var(--mood-text-muted); font-size: 0.75rem; }
.mwiz__pill {
font-size: 0.75rem; font-weight: 600; padding: 0.25rem 0.625rem;
border-radius: var(--r-pill); color: var(--mood-text-muted);
background: var(--mood-surface); white-space: nowrap;
}
.mwiz__pill--active { background: var(--mood-accent); color: var(--mood-accent-text); }
.mwiz__pill--done { background: var(--mood-accent-soft); color: var(--mood-accent); }
.mwiz__step { display: flex; flex-direction: column; gap: 0.75rem; }
.mwiz__header { margin-bottom: 0.5rem; }
.mwiz__title { margin: 0 0 0.25rem; font-size: clamp(1.25rem, 3vw, 1.625rem); font-weight: 800; }
.mwiz__sub { margin: 0; font-size: 0.9375rem; color: var(--mood-text-muted); }
.mwiz__label { font-size: 0.875rem; font-weight: 700; margin-top: 0.375rem; }
.mwiz__label--inline { margin-top: 0; }
.mwiz__input {
width: 100%; padding: 0.6875rem 1rem; font: inherit; font-size: 0.9375rem;
color: var(--mood-text); background: var(--mood-input-bg); border: none;
border-radius: var(--r-input); box-shadow: inset 0 0 0 1px var(--mood-input-border);
}
.mwiz__input:focus { outline: none; box-shadow: inset 0 0 0 2px var(--mood-input-focus); }
.mwiz__input--num { width: 6rem; text-align: center; font-weight: 700; font-variant-numeric: tabular-nums; }
.mwiz__input--tag { width: 11rem; padding: 0.375rem 0.75rem; font-size: 0.875rem; }
.mwiz__row { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; }
.mwiz__slider { flex: 1; min-width: 10rem; accent-color: var(--mood-accent); }
.mwiz__unit { font-size: 0.875rem; color: var(--mood-text-muted); }
.mwiz__chips { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
.mwiz__chip {
display: inline-flex; align-items: center; gap: 0.375rem;
padding: 0.375rem 0.875rem; font-size: 0.875rem; font-weight: 600;
color: var(--mood-text-muted); background: var(--mood-surface);
border-radius: var(--r-pill); box-shadow: var(--shadow-card);
cursor: pointer; transition: transform 0.1s ease;
}
.mwiz__chip:hover { transform: translateY(-1px); }
.mwiz__chip--on { background: var(--mood-accent); color: var(--mood-accent-text); }
.mwiz__chip-x { display: inline-flex; background: none; color: inherit; cursor: pointer; padding: 0; }
.mwiz__check {
display: flex; align-items: center; gap: 0.625rem; margin-top: 0.375rem;
font-size: 0.9375rem; font-weight: 600; cursor: pointer;
}
.mwiz__checkbox { width: 1.125rem; height: 1.125rem; accent-color: var(--mood-accent); }
.mwiz__recap {
margin-top: 0.75rem; padding: 1.125rem 1.25rem; border-radius: var(--r-card);
background: var(--mood-surface); box-shadow: var(--shadow-card);
display: flex; flex-direction: column; gap: 0.375rem;
}
.mwiz__recap-title { margin: 0; font-size: 1.0625rem; font-weight: 800; }
.mwiz__recap-line { margin: 0; font-size: 0.875rem; color: var(--mood-text-muted); }
.mwiz__recap-note { margin: 0.375rem 0 0; font-size: 0.875rem; font-weight: 600; color: var(--mood-accent); }
.mwiz__error { margin: 0; font-size: 0.875rem; font-weight: 600; color: var(--mood-error); }
.mwiz__actions { display: flex; justify-content: flex-end; }
.slide-fade-enter-active, .slide-fade-leave-active { transition: all 0.2s ease; }
.slide-fade-enter-from { opacity: 0; transform: translateX(20px); }
.slide-fade-leave-to { opacity: 0; transform: translateX(-20px); }
</style>