541 lines
15 KiB
Svelte
541 lines
15 KiB
Svelte
<script lang="ts">
|
|||
|
|
import type { Bundle, Locale } from '$lib/i18n';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* HostingVillage — scène SVG isométrique « village créole ».
|
||
|
|
* Chaque bâtiment est une instance hébergée OKI (lien externe) :
|
||
|
|
* BOKANTE (Mastodon) → case créole à ka
|
||
|
|
* GADE (PeerTube) → karbay
|
||
|
|
* NIYAJ (Nextcloud) → silo nuage
|
||
|
|
* KUTE (Castopod) → case au panneau « KUTE » + antenne
|
||
|
|
* Fallback accessible : grille de cards HTML (miroir sémantique)
|
||
|
|
* affichée à la place de la scène si prefers-reduced-motion ou ≤ 640 px.
|
||
|
|
* DOM/SVG uniquement, aucun WebGL, aucune dépendance (playbook P3).
|
||
|
|
*/
|
||
|
|
|
||
|
|
let { t, locale }: { t: Bundle; locale: Locale } = $props();
|
||
|
|
|
||
|
|
interface HostedService {
|
||
|
|
name: string;
|
||
|
|
platform: string;
|
||
|
|
description: string;
|
||
|
|
url: string;
|
||
|
|
linkText: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Secours si le bundle ne fournit pas l'entrée (contenu legacy stable). */
|
||
|
|
const DEFAULTS: Record<string, HostedService> = {
|
||
|
|
BOKANTE: {
|
||
|
|
name: 'BOKANTE',
|
||
|
|
platform: 'Mastodon',
|
||
|
|
description: '',
|
||
|
|
url: 'https://bokante.o-k-i.net',
|
||
|
|
linkText: 'BOKANTE'
|
||
|
|
},
|
||
|
|
GADE: {
|
||
|
|
name: 'GADE',
|
||
|
|
platform: 'PeerTube',
|
||
|
|
description: '',
|
||
|
|
url: 'https://gade.o-k-i.net',
|
||
|
|
linkText: 'GADE'
|
||
|
|
},
|
||
|
|
NIYAJ: {
|
||
|
|
name: 'NIYAJ',
|
||
|
|
platform: 'Nextcloud',
|
||
|
|
description: '',
|
||
|
|
url: 'https://niyaj.o-k-i.net',
|
||
|
|
linkText: 'NIYAJ'
|
||
|
|
},
|
||
|
|
KUTE: {
|
||
|
|
name: 'KUTE',
|
||
|
|
platform: 'Castopod',
|
||
|
|
description: '',
|
||
|
|
url: 'https://kute.o-k-i.net/',
|
||
|
|
linkText: 'KUTE'
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Les instances de la section « Services hébergés » vivent dans t.services.items
|
||
|
|
let items = $derived((t.services?.items ?? []) as HostedService[]);
|
||
|
|
|
||
|
|
function svc(name: string): HostedService {
|
||
|
|
return items.find((i) => i.name?.toUpperCase() === name) ?? DEFAULTS[name];
|
||
|
|
}
|
||
|
|
|
||
|
|
let bokante = $derived(svc('BOKANTE'));
|
||
|
|
let gade = $derived(svc('GADE'));
|
||
|
|
let niyaj = $derived(svc('NIYAJ'));
|
||
|
|
let kute = $derived(svc('KUTE'));
|
||
|
|
|
||
|
|
let sceneLabel = $derived(
|
||
|
|
locale === 'en'
|
||
|
|
? 'Isometric Creole village of OKI hosted services: each building is a link to an instance.'
|
||
|
|
: 'Village créole isométrique des services hébergés OKI : chaque bâtiment est un lien vers une instance.'
|
||
|
|
);
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<div class="hosting-village">
|
||
|
|
<!-- ── Scène isométrique (masquée si reduced-motion ou ≤ 640 px) ── -->
|
||
|
|
<div class="village-scene">
|
||
|
|
<svg viewBox="0 0 960 540" width="100%" role="group" aria-label={sceneLabel}>
|
||
|
|
<!-- Cadre nuit -->
|
||
|
|
<rect class="sky" x="0" y="0" width="960" height="540" rx="6" />
|
||
|
|
<!-- Étoiles angulaires -->
|
||
|
|
<rect class="star" x="120" y="60" width="3" height="3" />
|
||
|
|
<rect class="star" x="210" y="118" width="3" height="3" />
|
||
|
|
<rect class="star" x="700" y="58" width="3" height="3" />
|
||
|
|
<rect class="star" x="822" y="138" width="3" height="3" />
|
||
|
|
<rect class="star" x="902" y="66" width="3" height="3" />
|
||
|
|
<rect class="star" x="64" y="182" width="3" height="3" />
|
||
|
|
<rect class="star" x="770" y="206" width="3" height="3" />
|
||
|
|
<rect class="star" x="420" y="64" width="3" height="3" />
|
||
|
|
|
||
|
|
<!-- Rivage puis sol en losange (arêtes à 30°) -->
|
||
|
|
<polygon class="shore" points="480,133 860,352 480,571 100,352" />
|
||
|
|
<polygon class="ground" points="480,138 830,340 480,542 130,340" />
|
||
|
|
<!-- Place centrale + flambeaux -->
|
||
|
|
<polygon class="plaza" points="480,321 600,390 480,459 360,390" />
|
||
|
|
<polygon class="win" points="420,386.5 423.5,390 420,393.5 416.5,390" />
|
||
|
|
<polygon class="win win-delay" points="540,386.5 543.5,390 540,393.5 536.5,390" />
|
||
|
|
<polygon class="win" points="480,344.5 483.5,348 480,351.5 476.5,348" />
|
||
|
|
|
||
|
|
<!-- ══ NIYAJ — Nextcloud : silo nuage (arrière-plan, gauche) ══ -->
|
||
|
|
<a
|
||
|
|
class="bldg"
|
||
|
|
href={niyaj.url}
|
||
|
|
target="_blank"
|
||
|
|
rel="noopener noreferrer"
|
||
|
|
aria-label="{niyaj.name} — {niyaj.platform}"
|
||
|
|
>
|
||
|
|
<g class="bldg-art">
|
||
|
|
<path
|
||
|
|
class="silo-body"
|
||
|
|
d="M234,290 L234,190 L306,190 L306,290 A36,20.8 0 0 1 234,290 Z"
|
||
|
|
/>
|
||
|
|
<path class="roof-or" d="M234,190 A36,32 0 0 1 306,190 Z" />
|
||
|
|
<line class="roof-line" x1="234" y1="190" x2="306" y2="190" />
|
||
|
|
<rect class="win" x="250" y="218" width="10" height="10" />
|
||
|
|
<rect class="win win-delay" x="272" y="246" width="10" height="10" />
|
||
|
|
<rect class="door" x="260" y="258" width="18" height="32" />
|
||
|
|
<!-- Nuage angulaire au-dessus du silo -->
|
||
|
|
<rect class="cloud" x="244" y="134" width="52" height="12" />
|
||
|
|
<rect class="cloud" x="254" y="122" width="30" height="12" />
|
||
|
|
<rect class="cloud" x="268" y="112" width="18" height="10" />
|
||
|
|
<rect class="cloud" x="258" y="150" width="3" height="5" />
|
||
|
|
<rect class="cloud" x="276" y="148" width="3" height="5" />
|
||
|
|
<path
|
||
|
|
class="bldg-outline"
|
||
|
|
d="M234,190 A36,32 0 0 1 306,190 L306,290 A36,20.8 0 0 1 234,290 Z"
|
||
|
|
/>
|
||
|
|
</g>
|
||
|
|
<g class="bldg-label">
|
||
|
|
<rect class="label-plaque" x="186" y="70" width="168" height="24" rx="3" />
|
||
|
|
<text class="label-text" x="270" y="87">{niyaj.name} — {niyaj.platform}</text>
|
||
|
|
</g>
|
||
|
|
</a>
|
||
|
|
|
||
|
|
<!-- ══ GADE — PeerTube : karbay (arrière-plan, droite) ══ -->
|
||
|
|
<a
|
||
|
|
class="bldg"
|
||
|
|
href={gade.url}
|
||
|
|
target="_blank"
|
||
|
|
rel="noopener noreferrer"
|
||
|
|
aria-label="{gade.name} — {gade.platform}"
|
||
|
|
>
|
||
|
|
<g class="bldg-art">
|
||
|
|
<rect class="post" x="657" y="188" width="6" height="72" />
|
||
|
|
<polygon class="deck-top" points="660,262 729,302 660,342 591,302" />
|
||
|
|
<polygon class="deck-left" points="591,302 660,342 660,350 591,310" />
|
||
|
|
<polygon class="deck-right" points="660,342 729,302 729,310 660,350" />
|
||
|
|
<rect class="post" x="570" y="238" width="6" height="72" />
|
||
|
|
<rect class="post" x="744" y="238" width="6" height="72" />
|
||
|
|
<rect class="post" x="657" y="288" width="6" height="72" />
|
||
|
|
<polygon class="roof-vert-dark" points="566,238 660,292 660,132" />
|
||
|
|
<polygon class="roof-vert" points="660,292 754,238 660,132" />
|
||
|
|
<line class="roof-line" x1="660" y1="132" x2="660" y2="292" />
|
||
|
|
<!-- Lanterne suspendue -->
|
||
|
|
<line class="lantern-cord" x1="640" y1="262" x2="640" y2="274" />
|
||
|
|
<rect class="win" x="635" y="274" width="10" height="12" />
|
||
|
|
<path
|
||
|
|
class="bldg-outline"
|
||
|
|
d="M660,132 L754,238 L750,238 L750,310 L729,310 L660,350 L591,310 L570,310 L570,238 L566,238 Z"
|
||
|
|
/>
|
||
|
|
</g>
|
||
|
|
<g class="bldg-label">
|
||
|
|
<rect class="label-plaque" x="576" y="92" width="168" height="24" rx="3" />
|
||
|
|
<text class="label-text" x="660" y="109">{gade.name} — {gade.platform}</text>
|
||
|
|
</g>
|
||
|
|
</a>
|
||
|
|
|
||
|
|
<!-- ══ BOKANTE — Mastodon : case créole à ka (avant-plan, gauche) ══ -->
|
||
|
|
<a
|
||
|
|
class="bldg"
|
||
|
|
href={bokante.url}
|
||
|
|
target="_blank"
|
||
|
|
rel="noopener noreferrer"
|
||
|
|
aria-label="{bokante.name} — {bokante.platform}"
|
||
|
|
>
|
||
|
|
<g class="bldg-art">
|
||
|
|
<polygon class="wall-left" points="312,430 390,475 390,420 312,375" />
|
||
|
|
<polygon class="wall-right" points="390,475 468,430 468,375 390,420" />
|
||
|
|
<line class="lamella" x1="338" y1="390" x2="338" y2="445" />
|
||
|
|
<line class="lamella" x1="364" y1="405" x2="364" y2="460" />
|
||
|
|
<line class="lamella" x1="410" y1="409" x2="410" y2="464" />
|
||
|
|
<line class="lamella" x1="429" y1="397.5" x2="429" y2="452.5" />
|
||
|
|
<line class="lamella" x1="448" y1="386" x2="448" y2="441" />
|
||
|
|
<polygon class="win" points="330,404 348,414 348,428 330,418" />
|
||
|
|
<polygon class="win win-delay" points="430,410 446,401 446,415 430,424" />
|
||
|
|
<polygon class="door" points="398,470 416,460 416,430 398,440" />
|
||
|
|
<rect class="door-knob" x="410" y="444" width="3" height="3" />
|
||
|
|
<polygon class="roof-rouge-dark" points="293,375 390,431 390,283" />
|
||
|
|
<polygon class="roof-rouge" points="390,431 487,375 390,283" />
|
||
|
|
<line class="roof-line" x1="390" y1="283" x2="390" y2="431" />
|
||
|
|
<!-- Ka (tambour gwoka) devant la case -->
|
||
|
|
<path class="drum" d="M405,468 L405,490 A10,5.8 0 0 0 425,490 L425,468 Z" />
|
||
|
|
<ellipse class="drum-top" cx="415" cy="468" rx="10" ry="5.8" />
|
||
|
|
<path
|
||
|
|
class="bldg-outline"
|
||
|
|
d="M390,283 L487,375 L468,375 L468,430 L390,475 L312,430 L312,375 L293,375 Z"
|
||
|
|
/>
|
||
|
|
</g>
|
||
|
|
<g class="bldg-label">
|
||
|
|
<rect class="label-plaque" x="316" y="232" width="168" height="24" rx="3" />
|
||
|
|
<text class="label-text" x="400" y="249">{bokante.name} — {bokante.platform}</text>
|
||
|
|
</g>
|
||
|
|
</a>
|
||
|
|
|
||
|
|
<!-- ══ KUTE — Castopod : case au panneau « KUTE » + antenne (avant-plan, droite) ══ -->
|
||
|
|
<a
|
||
|
|
class="bldg"
|
||
|
|
href={kute.url}
|
||
|
|
target="_blank"
|
||
|
|
rel="noopener noreferrer"
|
||
|
|
aria-label="{kute.name} — {kute.platform}"
|
||
|
|
>
|
||
|
|
<g class="bldg-art">
|
||
|
|
<polygon class="wall-left" points="544,440 600,478 600,432 544,394" />
|
||
|
|
<polygon class="wall-right" points="600,478 676,440 676,394 600,432" />
|
||
|
|
<line class="lamella" x1="572" y1="413" x2="572" y2="459" />
|
||
|
|
<line class="lamella" x1="625" y1="419" x2="625" y2="465" />
|
||
|
|
<line class="lamella" x1="650" y1="407" x2="650" y2="453" />
|
||
|
|
<polygon class="win" points="560,414 574,422 574,434 560,426" />
|
||
|
|
<polygon class="door" points="608,474 624,465 624,431 608,440" />
|
||
|
|
<!-- Panneau « KUTE » sur la façade gauche -->
|
||
|
|
<polygon class="sign" points="548,404 592,429 592,443 548,418" />
|
||
|
|
<text
|
||
|
|
class="sign-text"
|
||
|
|
transform="matrix(1,0.577,0,1,552,416)"
|
||
|
|
x="0"
|
||
|
|
y="0">KUTE</text
|
||
|
|
>
|
||
|
|
<polygon class="roof-rouge-dark" points="517,384 600,432 600,304" />
|
||
|
|
<polygon class="roof-rouge" points="600,432 683,384 600,304" />
|
||
|
|
<line class="roof-line" x1="600" y1="304" x2="600" y2="432" />
|
||
|
|
<!-- Antenne d'émission (podcast) -->
|
||
|
|
<line class="antenna" x1="600" y1="304" x2="600" y2="284" />
|
||
|
|
<line class="antenna-tick" x1="600" y1="284" x2="593" y2="276" />
|
||
|
|
<line class="antenna-tick" x1="600" y1="284" x2="607" y2="276" />
|
||
|
|
<line class="antenna-tick" x1="588" y1="270" x2="582" y2="262" />
|
||
|
|
<line class="antenna-tick" x1="612" y1="270" x2="618" y2="262" />
|
||
|
|
<path
|
||
|
|
class="bldg-outline"
|
||
|
|
d="M600,304 L683,384 L676,394 L676,440 L600,478 L544,440 L544,394 L517,384 Z"
|
||
|
|
/>
|
||
|
|
</g>
|
||
|
|
<g class="bldg-label">
|
||
|
|
<rect class="label-plaque" x="516" y="496" width="168" height="24" rx="3" />
|
||
|
|
<text class="label-text" x="600" y="513">{kute.name} — {kute.platform}</text>
|
||
|
|
</g>
|
||
|
|
</a>
|
||
|
|
</svg>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- ── Fallback : grille de cards (miroir sémantique de la scène) ── -->
|
||
|
|
<ul class="village-fallback" role="list">
|
||
|
|
{#each items as item (item.name)}
|
||
|
|
<li class="card vf-card">
|
||
|
|
<div class="vf-head">
|
||
|
|
<h3>{item.name}</h3>
|
||
|
|
<span class="tag">{item.platform}</span>
|
||
|
|
</div>
|
||
|
|
<p class="vf-desc">{item.description}</p>
|
||
|
|
<a class="vf-link" href={item.url} target="_blank" rel="noopener noreferrer"
|
||
|
|
>{item.linkText}</a
|
||
|
|
>
|
||
|
|
</li>
|
||
|
|
{/each}
|
||
|
|
</ul>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<style>
|
||
|
|
.hosting-village {
|
||
|
|
margin-top: var(--space-4);
|
||
|
|
}
|
||
|
|
|
||
|
|
.village-scene svg {
|
||
|
|
display: block;
|
||
|
|
width: 100%;
|
||
|
|
height: auto;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Nuit, sol, végétation minérale ── */
|
||
|
|
.sky {
|
||
|
|
fill: color-mix(in srgb, var(--noir-profond) 45%, var(--noir-oki));
|
||
|
|
stroke: var(--line);
|
||
|
|
stroke-width: 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
.star {
|
||
|
|
fill: var(--blanc-creme);
|
||
|
|
opacity: 0.25;
|
||
|
|
}
|
||
|
|
|
||
|
|
.shore {
|
||
|
|
fill: var(--gris-sombre);
|
||
|
|
opacity: 0.35;
|
||
|
|
}
|
||
|
|
|
||
|
|
.ground {
|
||
|
|
fill: var(--noir-profond);
|
||
|
|
stroke: var(--line);
|
||
|
|
stroke-width: 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
.plaza {
|
||
|
|
fill: rgba(255, 255, 255, 0.04);
|
||
|
|
stroke: var(--line);
|
||
|
|
stroke-width: 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Matériaux des bâtiments ── */
|
||
|
|
.wall-left {
|
||
|
|
fill: color-mix(in srgb, var(--gris-sombre) 60%, var(--noir-oki));
|
||
|
|
}
|
||
|
|
|
||
|
|
.wall-right {
|
||
|
|
fill: var(--gris-sombre);
|
||
|
|
}
|
||
|
|
|
||
|
|
.lamella {
|
||
|
|
stroke: rgba(255, 255, 255, 0.06);
|
||
|
|
stroke-width: 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
.door {
|
||
|
|
fill: var(--noir-oki);
|
||
|
|
}
|
||
|
|
|
||
|
|
.door-knob {
|
||
|
|
fill: var(--or-oki);
|
||
|
|
opacity: 0.7;
|
||
|
|
}
|
||
|
|
|
||
|
|
.silo-body {
|
||
|
|
fill: color-mix(in srgb, var(--gris-sombre) 75%, var(--noir-oki));
|
||
|
|
}
|
||
|
|
|
||
|
|
.deck-top {
|
||
|
|
fill: var(--gris-sombre);
|
||
|
|
}
|
||
|
|
|
||
|
|
.deck-left {
|
||
|
|
fill: color-mix(in srgb, var(--gris-sombre) 60%, var(--noir-oki));
|
||
|
|
}
|
||
|
|
|
||
|
|
.deck-right {
|
||
|
|
fill: color-mix(in srgb, var(--gris-sombre) 80%, var(--noir-oki));
|
||
|
|
}
|
||
|
|
|
||
|
|
.post {
|
||
|
|
fill: color-mix(in srgb, var(--gris-sombre) 50%, var(--noir-oki));
|
||
|
|
}
|
||
|
|
|
||
|
|
.drum {
|
||
|
|
fill: color-mix(in srgb, var(--gris-sombre) 55%, var(--noir-oki));
|
||
|
|
}
|
||
|
|
|
||
|
|
.drum-top {
|
||
|
|
fill: var(--noir-profond);
|
||
|
|
stroke: color-mix(in srgb, var(--or-oki) 55%, transparent);
|
||
|
|
stroke-width: 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
.cloud {
|
||
|
|
fill: var(--blanc-creme);
|
||
|
|
opacity: 0.15;
|
||
|
|
}
|
||
|
|
|
||
|
|
.sign {
|
||
|
|
fill: var(--noir-oki);
|
||
|
|
stroke: color-mix(in srgb, var(--or-oki) 45%, transparent);
|
||
|
|
stroke-width: 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
.sign-text {
|
||
|
|
font-family: var(--font-display);
|
||
|
|
font-size: 12px;
|
||
|
|
font-weight: 700;
|
||
|
|
letter-spacing: 0.08em;
|
||
|
|
fill: var(--blanc-creme);
|
||
|
|
}
|
||
|
|
|
||
|
|
.antenna {
|
||
|
|
stroke: var(--blanc-creme);
|
||
|
|
stroke-width: 2;
|
||
|
|
opacity: 0.5;
|
||
|
|
}
|
||
|
|
|
||
|
|
.antenna-tick {
|
||
|
|
stroke: var(--or-oki);
|
||
|
|
stroke-width: 1.5;
|
||
|
|
opacity: 0.75;
|
||
|
|
}
|
||
|
|
|
||
|
|
.lantern-cord {
|
||
|
|
stroke: var(--line);
|
||
|
|
stroke-width: 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Toits : couleurs du drapeau, un accent par bâtiment ── */
|
||
|
|
.roof-rouge {
|
||
|
|
fill: var(--rouge-oki);
|
||
|
|
}
|
||
|
|
|
||
|
|
.roof-rouge-dark {
|
||
|
|
fill: color-mix(in srgb, var(--rouge-oki) 55%, var(--noir-oki));
|
||
|
|
}
|
||
|
|
|
||
|
|
.roof-vert {
|
||
|
|
fill: var(--vert-oki);
|
||
|
|
}
|
||
|
|
|
||
|
|
.roof-vert-dark {
|
||
|
|
fill: color-mix(in srgb, var(--vert-oki) 55%, var(--noir-oki));
|
||
|
|
}
|
||
|
|
|
||
|
|
.roof-or {
|
||
|
|
fill: var(--or-oki);
|
||
|
|
}
|
||
|
|
|
||
|
|
.roof-line {
|
||
|
|
stroke: rgba(0, 0, 0, 0.35);
|
||
|
|
stroke-width: 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Fenêtres lumineuses : scintillement subtil,
|
||
|
|
coupé par la gate reduced-motion globale de base.css ── */
|
||
|
|
.win {
|
||
|
|
fill: var(--or-clair);
|
||
|
|
opacity: 0.82;
|
||
|
|
animation: win-glow 4.5s ease-in-out infinite alternate;
|
||
|
|
}
|
||
|
|
|
||
|
|
.win-delay {
|
||
|
|
animation-delay: -2.2s;
|
||
|
|
}
|
||
|
|
|
||
|
|
@keyframes win-glow {
|
||
|
|
from {
|
||
|
|
opacity: 0.5;
|
||
|
|
}
|
||
|
|
to {
|
||
|
|
opacity: 0.95;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Interaction : liseré or + label + léger soulèvement ── */
|
||
|
|
.bldg {
|
||
|
|
cursor: pointer;
|
||
|
|
}
|
||
|
|
|
||
|
|
.bldg-art {
|
||
|
|
transition: transform var(--dur-tanbou) var(--ease-ka);
|
||
|
|
}
|
||
|
|
|
||
|
|
.bldg:hover .bldg-art,
|
||
|
|
.bldg:focus-visible .bldg-art {
|
||
|
|
transform: translateY(-5px);
|
||
|
|
}
|
||
|
|
|
||
|
|
.bldg-outline {
|
||
|
|
fill: none;
|
||
|
|
stroke: var(--or-oki);
|
||
|
|
stroke-width: 2.5;
|
||
|
|
stroke-linejoin: miter;
|
||
|
|
opacity: 0;
|
||
|
|
transition: opacity var(--dur-tanbou) var(--ease-ka);
|
||
|
|
}
|
||
|
|
|
||
|
|
.bldg:hover .bldg-outline,
|
||
|
|
.bldg:focus-visible .bldg-outline {
|
||
|
|
opacity: 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
.bldg-label {
|
||
|
|
opacity: 0;
|
||
|
|
transition: opacity var(--dur-tanbou) var(--ease-ka);
|
||
|
|
}
|
||
|
|
|
||
|
|
.bldg:hover .bldg-label,
|
||
|
|
.bldg:focus-visible .bldg-label {
|
||
|
|
opacity: 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
.label-plaque {
|
||
|
|
fill: rgba(13, 13, 13, 0.88);
|
||
|
|
stroke: var(--or-oki);
|
||
|
|
stroke-width: 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
.label-text {
|
||
|
|
font-family: var(--font-display);
|
||
|
|
font-size: 13px;
|
||
|
|
font-weight: 700;
|
||
|
|
letter-spacing: 0.05em;
|
||
|
|
text-transform: uppercase;
|
||
|
|
text-anchor: middle;
|
||
|
|
fill: var(--or-oki);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Fallback : miroir sémantique, un seul actif à la fois ── */
|
||
|
|
.village-fallback {
|
||
|
|
display: none;
|
||
|
|
list-style: none;
|
||
|
|
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||
|
|
gap: var(--space-3);
|
||
|
|
}
|
||
|
|
|
||
|
|
.vf-head {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: space-between;
|
||
|
|
gap: var(--space-2);
|
||
|
|
flex-wrap: wrap;
|
||
|
|
margin-bottom: var(--space-2);
|
||
|
|
}
|
||
|
|
|
||
|
|
.vf-head h3 {
|
||
|
|
font-size: 1.1rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.vf-desc {
|
||
|
|
color: var(--muted);
|
||
|
|
font-size: 0.95rem;
|
||
|
|
margin-bottom: var(--space-2);
|
||
|
|
}
|
||
|
|
|
||
|
|
.vf-link {
|
||
|
|
font-weight: 600;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (prefers-reduced-motion: reduce), (max-width: 640px) {
|
||
|
|
.village-scene {
|
||
|
|
display: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.village-fallback {
|
||
|
|
display: grid;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
</style>
|