forked from yvv/decision
568 lines
21 KiB
Svelte
568 lines
21 KiB
Svelte
<script lang="ts">
|
|
// The reader of a document (lot U, docs/chantiers/acte-certification/UX-lecteur.md). In
|
|
// place, what is read and decided on what one has before one's eyes: the vote of a version
|
|
// and its other versions, its inertia, the order of a section (handles on its articles).
|
|
// On the right, what is written, or works over several modules: proposing, the basket,
|
|
// the details of a version, the settings of the formula. Short at a glance: the text, each
|
|
// section closed by one line of order; the open proposals folded by section.
|
|
import { goto } from '$app/navigation';
|
|
import { page } from '$app/state';
|
|
import ChainSign from '$lib/components/ChainSign.svelte';
|
|
import DeepList from '$lib/components/DeepList.svelte';
|
|
import DocPanel from '$lib/components/DocPanel.svelte';
|
|
import Guard from '$lib/components/Guard.svelte';
|
|
import Icon from '$lib/components/Icon.svelte';
|
|
import OpeningInfo from '$lib/components/OpeningInfo.svelte';
|
|
import OrderLine from '$lib/components/OrderLine.svelte';
|
|
import Reorder from '$lib/components/Reorder.svelte';
|
|
import TitleBar from '$lib/components/TitleBar.svelte';
|
|
import UnitBlock from '$lib/components/UnitBlock.svelte';
|
|
import VoteFold from '$lib/components/VoteFold.svelte';
|
|
import WaveRule from '$lib/components/WaveRule.svelte';
|
|
import { sections, votableIds } from '$lib/basket';
|
|
import { DocState, panelSearch, shown, TABS, type Entry, type PanelState, type View } from '$lib/doc.svelte';
|
|
import { i18n, LANGS, t } from '$lib/i18n/index.svelte';
|
|
import { readerSectionLabel } from '$lib/labels';
|
|
import { answerPanel } from '$lib/objections';
|
|
import { BasketState } from '$lib/picks.svelte';
|
|
import { session } from '$lib/session.svelte';
|
|
import type { Lang, Proposal, Unit } from '$lib/types';
|
|
import type { Line } from '$lib/vote';
|
|
import { articles, deepSections, type Filter } from '$lib/versions';
|
|
|
|
const doc = new DocState();
|
|
const basket = new BasketState();
|
|
const group = $derived(page.params.group ?? '');
|
|
const slug = $derived(page.params.doc ?? '');
|
|
$effect(() => {
|
|
void doc.load(group, slug);
|
|
});
|
|
// The basket of this document, kept for the tab; versions no longer open to vote
|
|
// leave it, and so does a « for » on a version held back by an objection.
|
|
$effect(() => {
|
|
const v = doc.view;
|
|
if (!v) return;
|
|
basket.load(v.doc.id, votableIds(sections(v.units, v.modules)));
|
|
});
|
|
|
|
// The panel and the unfolded vote live in the URL: a link (or a feedback report) reopens them.
|
|
let prefill = $state<PanelState['prefill']>(null);
|
|
const panel = $derived.by((): PanelState | null => {
|
|
const q = page.url.searchParams;
|
|
const tab = TABS[q.get('onglet') ?? ''];
|
|
if (!tab) return null;
|
|
return {
|
|
unitId: q.get('article'),
|
|
proposalId: q.get('version'),
|
|
tab,
|
|
draftId: q.get('brouillon'),
|
|
section: q.get('section'),
|
|
prefill,
|
|
objectionId: q.get('objection'),
|
|
edit: q.get('modifier') === '1',
|
|
};
|
|
});
|
|
/**
|
|
* The version whose vote is unfolded where it reads (`?vote=`; the old `onglet=vote` too,
|
|
* and the old `onglet=propositions`, whose versions now live inside the vote).
|
|
*/
|
|
const vote = $derived.by(() => {
|
|
const q = page.url.searchParams;
|
|
const onglet = q.get('onglet');
|
|
if (q.get('vote')) return q.get('vote');
|
|
if (onglet === 'vote') return q.get('version');
|
|
if (onglet === 'propositions') {
|
|
const u = doc.unit(q.get('article'));
|
|
return q.get('version') ?? u?.inForce?.id ?? u?.proposals.find((p) => p.status === 'open')?.id ?? null;
|
|
}
|
|
return null;
|
|
});
|
|
|
|
function go(nextPanel: PanelState | null, nextVote: string | null) {
|
|
prefill = nextPanel?.prefill ?? null;
|
|
const search = panelSearch(nextPanel, nextVote);
|
|
void goto(`${page.url.pathname}${search ? `?${search}` : ''}`, {
|
|
replaceState: (!!panel && !!nextPanel) || (!nextPanel && !panel),
|
|
noScroll: true,
|
|
keepFocus: true,
|
|
});
|
|
}
|
|
const openPanel = (next: PanelState | null) => go(next, vote);
|
|
/** Unfolds (or folds) the vote of a version; from the panel, `close` closes it. */
|
|
const toggleVote = (id: string, close = false) => go(close ? null : panel, vote === id && !close ? null : id);
|
|
const openVote = (id: string, close: boolean) => go(close ? null : panel, id);
|
|
|
|
// Reordering a section: handles on its articles, in place (one at a time).
|
|
let reordering = $state<string | null>(null);
|
|
function startReorder(order: Unit) {
|
|
reordering = order.id;
|
|
// Its vote folds: the line now holds the articles with their handles.
|
|
if ([order.inForce, ...order.proposals].some((p) => p?.id === vote)) go(panel, null);
|
|
}
|
|
// An old link « proposer un autre ordre » (onglet=proposer on an order) reorders in place.
|
|
$effect(() => {
|
|
if (panel?.tab !== 'propose') return;
|
|
const u = doc.unit(panel.unitId);
|
|
if (u?.kind !== 'order') return;
|
|
reordering = u.id;
|
|
openPanel(null);
|
|
});
|
|
|
|
// On the Ğ1 chain: one signing at a time, over the page.
|
|
let signing = $state<Line[] | null>(null);
|
|
const sign = (lines: Line[]) => (signing = lines);
|
|
// While a ballot waits for the chain, the page looks again every ten seconds.
|
|
const waitingChain = $derived(
|
|
doc.ordered.some((u) => [u.inForce, ...u.proposals].some((p) => p?.myBallot?.status === 'pending')),
|
|
);
|
|
$effect(() => {
|
|
if (!waitingChain) return;
|
|
const every = setInterval(() => void doc.reload(), 10_000);
|
|
return () => clearInterval(every);
|
|
});
|
|
|
|
let filter = $state<Filter>('all');
|
|
const view = $derived(doc.view);
|
|
const source = $derived<Lang>(view?.doc.sourceLang ?? 'fr');
|
|
const lang = $derived<Lang>(doc.original ? source : i18n.lang);
|
|
const title = $derived(view ? (view.doc.title[lang] ?? view.doc.title.fr ?? '') : '');
|
|
const summary = $derived(view ? (view.doc.summary[lang] ?? view.doc.summary.fr ?? '') : '');
|
|
const nameOf = (id: string) => doc.articleTitle(id, lang, source);
|
|
// The title of the document, a module (ADR 0009): its line under the title.
|
|
const titleUnit = $derived(view?.modules.title ?? null);
|
|
const titleStale = $derived(!!titleUnit?.inForce && shown(titleUnit.inForce, lang, source).stale);
|
|
const VIEWS: View[] = ['inforce', 'wellrated', 'all'];
|
|
|
|
const reading = $derived(doc.mode === 'inforce' || !doc.preview);
|
|
/** The text as it reads, by section, each closed by its order. */
|
|
const textSections = $derived(reading ? doc.textSections : []);
|
|
/** The versions the text shows: their vote unfolds there, never in the list below. */
|
|
const textIds = $derived(
|
|
new Set(
|
|
reading
|
|
? textSections.flatMap((s) => s.entries.map((e) => e.proposal.id))
|
|
: (doc.preview ?? []).map((pu) => pu.proposalId ?? doc.unit(pu.unitId)?.inForce?.id),
|
|
),
|
|
);
|
|
// « Propositions en cours »: the articles (the modules are voted at their place), the seed
|
|
// proposals of the head apart; by section, each closed by its order when the text does not.
|
|
const deep = $derived(articles(doc.open.filter((x) => !x.unit.kind && !textIds.has(x.proposal.id)), filter));
|
|
const textKeys = $derived(new Set(textSections.map((s) => s.section)));
|
|
const deepOrders = $derived(
|
|
filter === 'all' ? (view?.modules.orders ?? []).filter((o) => !o.moot && !textKeys.has(o.section)) : [],
|
|
);
|
|
const sectionKeys = $derived([...new Set((view?.units ?? []).map((u) => u.section))]);
|
|
const deepSecs = $derived(deepSections(deep, deepOrders, sectionKeys));
|
|
const canVote = $derived(!!view?.vote.open);
|
|
// The facts of the head say what the page shows (lot U2): the seed proposals read at the head
|
|
// while nothing is in force, apart; then the proposals of « Propositions en cours » — the
|
|
// articles' versions, never the orders and titles proposed, voted at their module's line.
|
|
const headSeeds = $derived(doc.seedHead.length);
|
|
const openArticles = $derived(doc.open.filter((x) => !x.unit.kind).length - headSeeds);
|
|
|
|
/** The other versions of an article open to the vote, besides the one read. */
|
|
const othersOf = (e: Entry) => e.unit.proposals.filter((p) => p.status === 'open' && p.kind !== 'inertia' && p.id !== e.proposal.id).length;
|
|
|
|
function onEntry(e: Entry, action: 'vote' | 'propose') {
|
|
if (action === 'vote') toggleVote(e.proposal.id);
|
|
// From a seed proposal (nothing in force): its text to start from.
|
|
else openPanel({ unitId: e.unit.id, proposalId: null, tab: 'propose', prefill: e.seed ? { title: e.proposal.title, body: e.proposal.body } : null });
|
|
}
|
|
</script>
|
|
|
|
{#snippet foldOf(unit: Unit, p: Proposal, besides: string[] = [])}
|
|
<VoteFold
|
|
{doc}
|
|
proposal={p}
|
|
{unit}
|
|
{besides}
|
|
onopen={(id) => go(panel, id)}
|
|
onsettings={(id) => openPanel({ unitId: unit.id, proposalId: id, tab: 'settings' })}
|
|
ondetails={(id) => openPanel({ unitId: unit.id, proposalId: id, tab: 'details' })}
|
|
onanswer={(o) => openPanel(answerPanel(o, session.me?.id ?? null))}
|
|
onsign={sign}
|
|
/>
|
|
{/snippet}
|
|
|
|
{#snippet moduleFold(p: Proposal)}
|
|
{@const u = doc.unit(p.unitId)}
|
|
{#if u}{@render foldOf(u, p)}{/if}
|
|
{/snippet}
|
|
|
|
{#snippet reorderOf(o: Unit)}
|
|
<Reorder
|
|
{doc}
|
|
unit={o}
|
|
{nameOf}
|
|
oncancel={() => (reordering = null)}
|
|
ondone={(id) => {
|
|
reordering = null;
|
|
go(panel, id);
|
|
}}
|
|
/>
|
|
{/snippet}
|
|
|
|
{#snippet orderLine(o: Unit)}
|
|
<OrderLine unit={o} {vote} reordering={reordering === o.id} fold={moduleFold} reorder={reorderOf} onvote={(id) => toggleVote(id)} onreorder={() => startReorder(o)} />
|
|
{/snippet}
|
|
|
|
<svelte:head><title>{title ? `${title} · libreDecision` : 'libreDecision'}</title></svelte:head>
|
|
|
|
{#if doc.error}
|
|
<Guard code={doc.error} onretry={() => doc.load(group, slug)} />
|
|
{:else if view}
|
|
<div class="reader" class:with-panel={!!panel}>
|
|
<header class="head">
|
|
<p class="crumbs eyebrow">
|
|
<a href="/g/{view.doc.groupSlug}">{view.doc.groupName}</a><span class="sep" aria-hidden="true">·</span><a href="/g/{view.doc.groupSlug}/docs">{t('nav.docs')}</a>
|
|
</p>
|
|
<h1>{title}</h1>
|
|
{#if titleUnit}
|
|
<TitleBar
|
|
unit={titleUnit}
|
|
stale={titleStale}
|
|
selected={panel?.unitId === titleUnit.id}
|
|
{vote}
|
|
fold={moduleFold}
|
|
onvote={(id) => toggleVote(id)}
|
|
onpropose={() => openPanel({ unitId: titleUnit.id, proposalId: null, tab: 'propose' })}
|
|
/>
|
|
{/if}
|
|
<p class="summary">{summary}</p>
|
|
<p class="facts faint">
|
|
{i18n.plural('docs.inForce', doc.inForce.length)} · {#if headSeeds}{i18n.plural('docs.seeds', headSeeds)}{' · '}{/if}{i18n.plural('docs.open', openArticles)}
|
|
{#if view.doc.publication.state === 'done'} · {t('doc.published')}{:else if view.doc.publication.state === 'pending'} · {t('doc.publish.pending')}{/if}
|
|
· <button class="linkish" type="button" onclick={() => openPanel({ unitId: null, proposalId: null, tab: 'settings' })}>{t('settings.title')}</button>
|
|
</p>
|
|
|
|
<!-- One line of tools — the basket, the language of the text —, then the readings. -->
|
|
<div class="tools">
|
|
{#if canVote}
|
|
<button class="btn small basket-btn" type="button" onclick={() => openPanel({ unitId: null, proposalId: null, tab: 'basket' })}>
|
|
{basket.size ? t('doc.basket.count', { n: basket.size }) : t('doc.basket')}
|
|
</button>
|
|
{#if view.vote.simulated}<span class="tag" title={t('vote.simulated.note')}>{t('vote.simulated.mark')}</span>{/if}
|
|
{/if}
|
|
<div class="seg small langs" role="radiogroup" aria-label={t('doc.lang.label')}>
|
|
{#each LANGS as l (l)}
|
|
<button type="button" role="radio" aria-checked={lang === l} class:on={lang === l} onclick={() => { doc.original = l === source && i18n.lang !== source; if (l !== source) i18n.set(l); }}>{l.toUpperCase()}</button>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
<div class="seg views" role="radiogroup" aria-label={t('doc.view.label')}>
|
|
{#each VIEWS as v (v)}
|
|
<button type="button" role="radio" aria-checked={doc.mode === v} class:on={doc.mode === v} onclick={() => doc.setMode(v)}>{t(`doc.view.${v}`)}</button>
|
|
{/each}
|
|
</div>
|
|
{#if lang !== source}
|
|
<p class="note faint">{t('doc.lang.translation')} <button class="linkish" type="button" onclick={() => (doc.original = true)}>{t('doc.lang.original')}</button></p>
|
|
{/if}
|
|
{#if doc.mode !== 'inforce'}<p class="note preview">{t(`doc.view.${doc.mode}.note`)}</p>{/if}
|
|
</header>
|
|
|
|
<!-- Before the text, out of the vote: the settings of the author at the opening (lot U3); then a
|
|
separation, a soft line of the mood, never a border; then the first article. -->
|
|
<OpeningInfo {view} inForce={doc.inForce.length} />
|
|
<hr class="opening-rule" />
|
|
|
|
<section class="text" aria-label={t('doc.view.inforce')}>
|
|
{#if reading}
|
|
<!-- Nothing in force and nothing to start from: said once. With the Propositions d'amorce
|
|
at the head (ADR 0011), the chapter says it, in Yvv's words. -->
|
|
{#if !doc.inForce.length && !textSections.length}<p class="lead">{t('doc.empty')}</p>{/if}
|
|
{#each textSections as s, i (`${i}:${s.section}`)}
|
|
{@const label = readerSectionLabel(s.section)}
|
|
<div class="text-section">
|
|
{#if label}<p class="section-name">{label}</p>{/if}
|
|
{#if !s.order || reordering !== s.order.id}
|
|
{#each s.entries as e (e.proposal.id)}
|
|
{@const p = e.proposal}
|
|
<UnitBlock
|
|
text={shown(p, lang, source)}
|
|
seed={e.seed ? p : null}
|
|
seedMinimal={!e.seed && p.seedMinimal}
|
|
enteredAt={e.seed ? null : p.enteredAt}
|
|
votes={p.tally.for + p.tally.against}
|
|
rivals={othersOf(e)}
|
|
selected={panel?.unitId === e.unit.id}
|
|
voteFor={p.id}
|
|
voteOpen={vote === p.id}
|
|
onaction={(action) => onEntry(e, action)}
|
|
>
|
|
{#snippet fold()}{@render foldOf(e.unit, p)}{/snippet}
|
|
</UnitBlock>
|
|
{/each}
|
|
{/if}
|
|
{#if s.order && !s.order.moot}{@render orderLine(s.order)}{/if}
|
|
</div>
|
|
{/each}
|
|
{:else}
|
|
{#each doc.preview ?? [] as pu (pu.unitId)}
|
|
{@const u = doc.unit(pu.unitId)}
|
|
{@const id = pu.proposalId ?? u?.inForce?.id ?? null}
|
|
{@const p = doc.proposal(id)}
|
|
<UnitBlock
|
|
text={shown(pu, lang, source)}
|
|
mark={pu.mark}
|
|
selected={panel?.unitId === pu.unitId}
|
|
voteFor={id}
|
|
voteOpen={!!id && vote === id}
|
|
onaction={(action) => {
|
|
if (action === 'vote') {
|
|
if (id) toggleVote(id);
|
|
} else if (u) openPanel({ unitId: u.id, proposalId: null, tab: 'propose' });
|
|
}}
|
|
>
|
|
{#snippet fold()}{#if u && p}{@render foldOf(u, p)}{/if}{/snippet}
|
|
</UnitBlock>
|
|
{/each}
|
|
{#if (doc.preview ?? []).every((x) => x.mark === 'unchanged')}<p class="faint">{t('doc.view.empty')}</p>{/if}
|
|
{/if}
|
|
</section>
|
|
|
|
<WaveRule />
|
|
|
|
<section class="deep" aria-labelledby="deep-title">
|
|
{#if doc.drafts.length}
|
|
<div class="drafts">
|
|
<h2 class="eyebrow">{t('deep.drafts')}</h2>
|
|
<p class="faint small">{t('deep.drafts.note')}</p>
|
|
<ul>
|
|
{#each doc.drafts as { unit, proposal } (proposal.id)}
|
|
<li><button type="button" class="linkish" onclick={() => openPanel({ unitId: unit.inForce ? unit.id : null, proposalId: null, tab: 'propose', draftId: proposal.id })}>{proposal.title}</button></li>
|
|
{/each}
|
|
</ul>
|
|
</div>
|
|
{/if}
|
|
|
|
<div class="deep-head">
|
|
<h2 id="deep-title">{t('deep.title')}</h2>
|
|
<p class="muted">{t('deep.note')}</p>
|
|
<div class="seg small" role="radiogroup" aria-label={t('deep.title')}>
|
|
{#each ['all', 'wellrated', 'mine'] as const as f (f)}
|
|
<button type="button" role="radio" aria-checked={filter === f} class:on={filter === f} onclick={() => (filter = f)}>{t(`deep.filter.${f}`)}</button>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
<DeepList
|
|
sections={deepSecs}
|
|
vote={textIds.has(vote ?? '') ? null : vote}
|
|
{reordering}
|
|
all={filter !== 'all'}
|
|
ontoggle={(_, p) => toggleVote(p.id)}
|
|
fold={foldOf}
|
|
order={orderLine}
|
|
onsection={canVote ? (section) => openPanel({ unitId: null, proposalId: null, tab: 'basket', section }) : undefined}
|
|
/>
|
|
<button class="btn new" type="button" onclick={() => openPanel({ unitId: null, proposalId: null, tab: 'propose' })}><Icon name="plus" size={16} />{t('deep.new')}</button>
|
|
</section>
|
|
</div>
|
|
|
|
{#if panel}
|
|
<DocPanel {doc} {panel} {lang} {basket} onchange={(p) => openPanel(p)} onclose={() => openPanel(null)} onvote={openVote} onsign={sign} />
|
|
{/if}
|
|
{#if signing && view.vote.chain}
|
|
<ChainSign {doc} chain={view.vote.chain} lines={signing} onsent={() => basket.clear()} onclose={() => (signing = null)} />
|
|
{/if}
|
|
{/if}
|
|
|
|
<svelte:window onkeydown={(e) => e.key === 'Escape' && panel && !signing && openPanel(null)} />
|
|
|
|
<style>
|
|
.reader {
|
|
max-width: calc(var(--measure) + 2.6rem);
|
|
margin: 0 auto;
|
|
padding: clamp(2.2rem, 7vh, 4.5rem) 1.3rem 6rem;
|
|
transition: padding 0.2s var(--ease);
|
|
}
|
|
@media (min-width: 1180px) {
|
|
.reader.with-panel {
|
|
margin-right: calc(var(--panel) + 2.5rem);
|
|
}
|
|
}
|
|
.crumbs {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.5rem;
|
|
}
|
|
.crumbs a {
|
|
color: inherit;
|
|
text-decoration: none;
|
|
}
|
|
.crumbs a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
h1 {
|
|
margin-top: 0.8rem;
|
|
font-family: var(--font-text);
|
|
font-weight: 500;
|
|
font-size: clamp(2rem, 1.4rem + 2.4vw, 2.9rem);
|
|
line-height: 1.1;
|
|
letter-spacing: -0.012em;
|
|
font-variation-settings: 'opsz' 60;
|
|
}
|
|
.summary {
|
|
margin-top: 0.9rem;
|
|
font-family: var(--font-text);
|
|
font-style: italic;
|
|
color: var(--ink-soft);
|
|
font-size: 1.06rem;
|
|
line-height: 1.6;
|
|
}
|
|
.facts {
|
|
margin-top: 0.8rem;
|
|
font-size: 0.85rem;
|
|
font-weight: 700;
|
|
}
|
|
.tools {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: 0.6rem;
|
|
margin-top: 1.2rem;
|
|
}
|
|
.langs {
|
|
margin-left: auto;
|
|
}
|
|
.views {
|
|
margin-top: 0.7rem;
|
|
}
|
|
.basket-btn {
|
|
background: var(--water);
|
|
color: var(--on-water);
|
|
}
|
|
.seg {
|
|
display: inline-flex;
|
|
flex-wrap: wrap;
|
|
background: var(--surface-2);
|
|
padding: 3px;
|
|
border-radius: var(--r-pill);
|
|
}
|
|
.seg button {
|
|
border: 0;
|
|
background: transparent;
|
|
border-radius: var(--r-pill);
|
|
padding: 0.35rem 0.85rem;
|
|
min-height: 2.25rem;
|
|
font-weight: 750;
|
|
font-size: 0.86rem;
|
|
color: var(--ink-soft);
|
|
white-space: nowrap;
|
|
}
|
|
.seg.small button {
|
|
padding-inline: 0.65rem;
|
|
font-size: 0.8rem;
|
|
}
|
|
.seg button.on {
|
|
background: var(--surface);
|
|
color: var(--ink);
|
|
box-shadow: var(--shadow);
|
|
}
|
|
.note {
|
|
margin-top: 0.8rem;
|
|
font-size: 0.86rem;
|
|
}
|
|
.note.preview {
|
|
color: var(--moss);
|
|
font-weight: 700;
|
|
}
|
|
.linkish {
|
|
border: 0;
|
|
background: none;
|
|
padding: 0;
|
|
color: var(--water);
|
|
font-weight: 750;
|
|
text-decoration: underline;
|
|
text-underline-offset: 3px;
|
|
}
|
|
.text {
|
|
margin-top: 0;
|
|
}
|
|
/* The separation before the text (Yvv, 2026-11-03): a soft line in the keystone's colour of the
|
|
mood, fading at its ends — drawn by its background, never a border. */
|
|
.opening-rule {
|
|
height: 2px;
|
|
margin: 2.2rem 0 1.6rem;
|
|
border: 0;
|
|
border-radius: 2px;
|
|
background: linear-gradient(
|
|
to right,
|
|
transparent,
|
|
color-mix(in srgb, var(--cle) 45%, transparent) 18%,
|
|
color-mix(in srgb, var(--cle) 45%, transparent) 82%,
|
|
transparent
|
|
);
|
|
}
|
|
.lead {
|
|
margin-bottom: 1.2rem;
|
|
font-size: 0.92rem;
|
|
font-weight: 650;
|
|
color: var(--ink-soft);
|
|
}
|
|
.text-section + .text-section {
|
|
margin-top: 0.6rem;
|
|
}
|
|
/* The name of a section: where one is, without a number — the italic of the text, softly. */
|
|
.section-name {
|
|
margin: 0 0 0.3rem;
|
|
font-family: var(--font-text);
|
|
font-style: italic;
|
|
font-size: 1.12rem;
|
|
color: var(--ink-soft);
|
|
}
|
|
.deep {
|
|
padding-top: 2rem;
|
|
background: linear-gradient(to bottom, var(--water-wash), transparent 22rem);
|
|
margin: 0 -1.3rem;
|
|
padding-inline: 1.3rem;
|
|
border-radius: 0 0 var(--r-card) var(--r-card);
|
|
}
|
|
.deep-head {
|
|
display: grid;
|
|
gap: 0.6rem;
|
|
margin-bottom: 0.6rem;
|
|
}
|
|
.deep-head h2 {
|
|
font-family: var(--font-text);
|
|
font-weight: 500;
|
|
font-size: 1.55rem;
|
|
}
|
|
.deep-head .seg {
|
|
justify-self: start;
|
|
}
|
|
.drafts {
|
|
display: grid;
|
|
gap: 0.3rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
.drafts ul {
|
|
margin: 0.3rem 0 0;
|
|
padding-left: 1.1rem;
|
|
}
|
|
.small {
|
|
font-size: 0.84rem;
|
|
}
|
|
.new {
|
|
margin-top: 1.4rem;
|
|
}
|
|
/* On a phone: the three readings go to the line rather than hide « Si tout passe » off the
|
|
edge (lot U2); each row fills the width, the targets big enough for a finger. */
|
|
@media (max-width: 899px) {
|
|
.seg.views {
|
|
display: flex;
|
|
gap: 2px;
|
|
}
|
|
.seg.views button {
|
|
flex: 1 1 auto;
|
|
}
|
|
.seg button {
|
|
min-height: 2.75rem;
|
|
}
|
|
}
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.reader {
|
|
transition: none;
|
|
}
|
|
}
|
|
</style>
|