tawasuyu

tawasuyu/chasqui

Sovereign P2P peer discovery + transport (DHT, relay, NAT traversal) in Rust.

chore: refresco desde el monorepo (pin tawasuyu.git v0.2.0)

Regenerado con scripts/actualizar-standalone.py: código del dominio al día,
git-deps repineadas a v0.2.0 y doble-fuente resuelta con [patch] al source git.
cargo check --workspace verde.

bb86cfaeae474682bae8a1b3349ac82932fb39e1 · por Sergio · (hace 3 meses) · padre 958e62665 · ver el árbol en este commit

Delta semántico · 141

02_ruway/chasqui/card-handshake/src/health.rs

02_ruway/chasqui/card-handshake/src/lib.rs

02_ruway/chasqui/card-handshake/tests/handshake.rs

02_ruway/chasqui/chasqui-broker-explorer-llimphi/examples/chasqui_showreel.rs

02_ruway/chasqui/chasqui-broker-explorer-llimphi/src/main.rs

02_ruway/chasqui/chasqui-card/src/intension.rs

02_ruway/chasqui/chasqui-card/src/lib.rs

02_ruway/chasqui/chasqui-core/src/bin/nouser.rs

02_ruway/chasqui/chasqui-core/src/cluster.rs

02_ruway/chasqui/chasqui-core/src/db.rs

02_ruway/chasqui/chasqui-core/src/edit.rs

02_ruway/chasqui/chasqui-core/src/grafo.rs

02_ruway/chasqui/chasqui-core/src/lib.rs

02_ruway/chasqui/chasqui-explorer-llimphi/src/main.rs

02_ruway/chasqui/chasqui-nous-real/src/cache.rs

  • cambió por dentro fn sha

02_ruway/chasqui/chasqui-nous-real/src/embeddings.rs

02_ruway/chasqui/chasqui-nous-real/src/main.rs

  • cambió por dentro fn main

39 archivos · +4681 −1120

diff --git a/02_ruway/chasqui/ARQUITECTURA.md b/02_ruway/chasqui/ARQUITECTURA.md
index bddc6a5..b7f21d3 100644
--- a/02_ruway/chasqui/ARQUITECTURA.md
+++ b/02_ruway/chasqui/ARQUITECTURA.md
@@ -185,7 +185,7 @@ DEUDA / ASPIRA_A:
   #5 COHERENCIA cross-dominio: rimay/shuma/arje aún no integrados a fondo con nouser.
 
 NORTE_ARQUITECTÓNICO:
-  Dos planos que convergen. PLANO DE TIPOS (Brahman): "decláme qué consumís y te encuentro quién lo produce,
+  Dos planos que convergen. PLANO DE TIPOS (Brahman): "decláme qué consumes y te encuentro quién lo produce,
   determinista y observable, sin acoplar módulos". PLANO DE DATOS (Nouser): "dame una carpeta y te devuelvo su
   estructura semántica latente (Mónadas) con embeddings intercambiables".
   El destino es que el broker deje de ser local (Unix socket) y, sobre transporte minga/libp2p, haga discovery
diff --git a/02_ruway/chasqui/LEEME.md b/02_ruway/chasqui/LEEME.md
index e86434b..08c3daa 100644
--- a/02_ruway/chasqui/LEEME.md
+++ b/02_ruway/chasqui/LEEME.md
@@ -1,41 +1,43 @@
 # chasqui
 
-> `chasqui` (quechua: *mensajero del camino del inca*). Broker de mensajería + bus tipado.
+> `chasqui` (quechua: *mensajero del camino del inca*). Broker de tipos + Mónadas semánticas.
 
-Sistema nervioso del monorepo. Apps publican y se suscriben a topics tipados; el broker rutea y persiste. Backend `nous` con dos implementaciones: `mock` (in-process para tests) y `real` (TCP + binary). Cada mensaje lleva su schema, fail-closed si el receptor no lo conoce.
+Dominio dual (ver `ARQUITECTURA.md`, el doc técnico autoritativo). **Brahman**: broker de tipos determinista — los módulos (Cards) declaran flujos tipados de entrada/salida y el broker matchea consumer↔producer (matching exact + structural, prioridades, sesgos por contexto) sin mover datos. **Nouser**: inteligencia de datos — escanea directorios en Mónadas (clusters semánticos de archivos) enriquecidas por proveedores de embeddings intercambiables tras el contrato `nous`: `mock` (32d determinista) y `real` (ONNX 384d). El discovery de consumidores es local-first con fallback DHT (`card-sidecar::discovery::resolve_provider`), más connect-and-consume remoto por libp2p (`consume_remote`).
 
 ## Instalación
 
 ```sh
-# arrancar el broker
-cargo run --release -p chasqui-broker
-
-# explorer (ver topics + mensajes en vivo)
-cargo run --release -p chasqui-broker-explorer-llimphi
-cargo run --release -p chasqui-explorer-llimphi
+cargo run --release -p chasqui-core --bin chasqui        # CLI Nouser: scan|show|json|daemon|attract
+cargo run --release -p chasqui-broker-explorer-llimphi   # UI probe del broker
+cargo run --release -p chasqui-explorer-llimphi          # UI explorador de Mónadas
 ```
 
+El broker Brahman es una librería hospedada por el init de arje (`arje-zero`), no un binario standalone.
+
 ## Compatibilidad
 
-- **Linux / macOS / Windows** — broker + clientes en Rust nativo.
-- **Wawa** — broker corre como app del kernel (`apps/`).
-- TCP localhost por default; sockets Unix opcionales.
+- **Linux / macOS** — Rust nativo; handshake sobre sockets Unix.
+- **Remoto** — handshake sobre stream libp2p (`card-net`): relay + dcutr + autonat, el NAT ya no bloquea.
+- El broker vive en la memoria del Init — efímero por diseño, sin snapshot/recover.
 
 ## Crates
 
 | Crate | Rol |
 |---|---|
-| [`chasqui-core`](chasqui-core/README.md) | Tipos: Topic, Message, Schema, Subscription. |
-| [`chasqui-broker`](chasqui-broker/README.md) | Binario del broker. |
-| [`chasqui-nous`](chasqui-nous/README.md) | Trait del transport. |
-| [`chasqui-nous-mock`](chasqui-nous-mock/README.md) | Transport in-process para tests. |
-| [`chasqui-nous-real`](chasqui-nous-real/README.md) | Transport TCP/Unix binario. |
-| [`chasqui-card`](chasqui-card/README.md) | Card escritorio (estado del broker). |
-| [`chasqui-broker-explorer-llimphi`](chasqui-broker-explorer-llimphi/README.md) | UI: topics + suscriptores activos. |
-| [`chasqui-explorer-llimphi`](chasqui-explorer-llimphi/README.md) | UI: log de mensajes en vivo. |
+| [`chasqui-broker`](chasqui-broker/README.md) | Brahman: librería de matching de tipos (Exact/Structural, prioridades, contextos). |
+| [`card-handshake`](card-handshake/) | Handshake Init↔módulo: Unix socket local, stream libp2p remoto. |
+| [`card-sidecar`](card-sidecar/) | Mantiene viva la sesión + discovery (`resolve_provider`, `consume_remote`). |
+| [`card-admin`](card-admin/) | Snapshot del estado del broker (sesiones + matches) — `brahman-status`. |
+| [`chasqui-core`](chasqui-core/README.md) | Nouser: scanner, clustering determinista, MonadDb, CLI `chasqui`. |
+| [`chasqui-card`](chasqui-card/README.md) | Manifiesto de Mónada + cliente de query (`resolve_monad`). |
+| [`chasqui-nous`](chasqui-nous/README.md) | Contrato Nous: JSON line-delimited sobre Unix socket. |
+| [`chasqui-nous-mock`](chasqui-nous-mock/README.md) | Proveedor de pseudo-embeddings 32d determinista. |
+| [`chasqui-nous-real`](chasqui-nous-real/README.md) | Proveedor de embeddings 384d ONNX (feature `embeddings`). |
+| [`chasqui-broker-explorer-llimphi`](chasqui-broker-explorer-llimphi/README.md) | UI probe del broker: estado + timeline de matches. |
+| [`chasqui-explorer-llimphi`](chasqui-explorer-llimphi/README.md) | UI exploradora de Mónadas con búsqueda semántica. |
 
 ## Consideraciones
 
-- **Schema-first.** Sin schema declarado, ningún mensaje pasa.
-- **Persistencia opt-in** por topic; los topics efímeros viven sólo en memoria.
-- **No es Kafka.** Diseñado para el monorepo, no para volumen de producción interplanetaria.
+- **El broker matchea tipos, no mueve datos.** Cada módulo abre su propio data plane (`service_socket`).
+- **Efímero por diseño.** El broker es el registro en memoria del Init de qué está vivo *ahora* — no es deuda de persistencia.
+- **No es un bus pub/sub.** Esa aspiración original migró al dominio Ayni; chasqui no transporta mensajes app↔app en tiempo real.
diff --git a/02_ruway/chasqui/README.md b/02_ruway/chasqui/README.md
index 2366596..524ca75 100644
--- a/02_ruway/chasqui/README.md
+++ b/02_ruway/chasqui/README.md
@@ -1,38 +1,43 @@
 # chasqui
 
-> `chasqui` (Quechua: *messenger of the Inca road*). Message broker + typed bus.
+> `chasqui` (Quechua: *messenger of the Inca road*). Type broker + semantic Monads.
 
-Nervous system of the monorepo. Apps publish and subscribe to typed topics; the broker routes and persists. `nous` backend with two impls: `mock` (in-process for tests) and `real` (binary TCP). Every message carries its schema, fail-closed if the receiver doesn't know it.
+Dual domain (see `ARQUITECTURA.md`, the authoritative technical doc). **Brahman**: a deterministic type broker — modules (Cards) declare typed input/output flows and the broker matches consumer↔producer (exact + structural matching, priorities, per-context biases) without moving any data. **Nouser**: data intelligence — scans directories into Monads (semantic file clusters) enriched by swappable embedding providers behind the `nous` contract: `mock` (deterministic 32d) and `real` (ONNX 384d). Consumer discovery is local-first with DHT fallback (`card-sidecar::discovery::resolve_provider`), plus remote connect-and-consume over libp2p (`consume_remote`).
 
 ## Install
 
 ```sh
-cargo run --release -p chasqui-broker
-cargo run --release -p chasqui-broker-explorer-llimphi
-cargo run --release -p chasqui-explorer-llimphi
+cargo run --release -p chasqui-core --bin chasqui        # Nouser CLI: scan|show|json|daemon|attract
+cargo run --release -p chasqui-broker-explorer-llimphi   # broker probe UI
+cargo run --release -p chasqui-explorer-llimphi          # Monad explorer UI
 ```
 
+The Brahman broker is a library hosted by arje's init (`arje-zero`), not a standalone binary.
+
 ## Compatibility
 
-- **Linux / macOS / Windows** — broker + clients in native Rust.
-- **Wawa** — broker runs as a kernel app.
-- TCP localhost by default; Unix sockets optional.
+- **Linux / macOS** — native Rust; handshake over Unix sockets.
+- **Remote** — handshake over libp2p stream (`card-net`): relay + dcutr + autonat, NAT no longer blocks.
+- The broker lives in the Init's memory — ephemeral by design, no snapshot/recover.
 
 ## Crates
 
 | Crate | Role |
 |---|---|
-| [`chasqui-core`](chasqui-core/README.md) | Topic, Message, Schema, Subscription. |
-| [`chasqui-broker`](chasqui-broker/README.md) | Broker binary. |
-| [`chasqui-nous`](chasqui-nous/README.md) | Transport trait. |
-| [`chasqui-nous-mock`](chasqui-nous-mock/README.md) | In-process transport. |
-| [`chasqui-nous-real`](chasqui-nous-real/README.md) | Binary TCP/Unix transport. |
-| [`chasqui-card`](chasqui-card/README.md) | Desktop card. |
-| [`chasqui-broker-explorer-llimphi`](chasqui-broker-explorer-llimphi/README.md) | Topics + active subscribers UI. |
-| [`chasqui-explorer-llimphi`](chasqui-explorer-llimphi/README.md) | Live message log UI. |
+| [`chasqui-broker`](chasqui-broker/README.md) | Brahman: type-matching library (Exact/Structural, priorities, contexts). |
+| [`card-handshake`](card-handshake/) | Init↔module handshake: Unix socket local, libp2p stream remote. |
+| [`card-sidecar`](card-sidecar/) | Keeps the session alive + discovery (`resolve_provider`, `consume_remote`). |
+| [`card-admin`](card-admin/) | Broker state snapshot (sessions + matches) — `brahman-status`. |
+| [`chasqui-core`](chasqui-core/README.md) | Nouser: scanner, deterministic clustering, MonadDb, `chasqui` CLI. |
+| [`chasqui-card`](chasqui-card/README.md) | Monad manifest + query client (`resolve_monad`). |
+| [`chasqui-nous`](chasqui-nous/README.md) | Nous contract: JSON line-delimited over Unix socket. |
+| [`chasqui-nous-mock`](chasqui-nous-mock/README.md) | Deterministic 32d pseudo-embeddings provider. |
+| [`chasqui-nous-real`](chasqui-nous-real/README.md) | 384d ONNX embeddings provider (`embeddings` feature). |
+| [`chasqui-broker-explorer-llimphi`](chasqui-broker-explorer-llimphi/README.md) | Broker probe UI: status + match timeline. |
+| [`chasqui-explorer-llimphi`](chasqui-explorer-llimphi/README.md) | Monad explorer UI with semantic search. |
 
 ## Considerations
 
-- **Schema-first.** No schema declared, no message through.
-- **Persistence opt-in** per topic; ephemeral topics live in memory only.
-- **Not Kafka.** Designed for the monorepo, not interplanetary production volume.
+- **The broker matches types, it does not move data.** Each module opens its own data plane (`service_socket`).
+- **Ephemeral by design.** The broker is the Init's in-memory registry of what's alive *now* — no persistence debt.
+- **Not a pub/sub bus.** That original aspiration migrated to the Ayni domain; chasqui carries no app↔app messages in real time.
diff --git a/02_ruway/chasqui/card-admin/LEEME.md b/02_ruway/chasqui/card-admin/LEEME.md
new file mode 100644
index 0000000..8976b05
--- /dev/null
+++ b/02_ruway/chasqui/card-admin/LEEME.md
@@ -0,0 +1,18 @@
+# card-admin
+
+*Read this in English: [README.md](README.md).*
+
+`brahman-admin` — observabilidad del broker.
+
+Expone un Unix socket separado (no se mezcla con el handshake) en el
+que cada conexión recibe un `StatusSnapshot` JSON y se cierra. Es
+single-shot por conexión: pensado para herramientas como
+`brahman-status`, dashboards y health-checks.
+
+Wire format: una línea JSON por conexión, terminada en `\n`. Esto
+hace trivial inspeccionar con `nc` o `socat` además del cliente
+tipado de este crate.
+
+---
+
+Parte de **chasqui** — ver [chasqui](../LEEME.md).
diff --git a/02_ruway/chasqui/card-admin/README.md b/02_ruway/chasqui/card-admin/README.md
new file mode 100644
index 0000000..3081696
--- /dev/null
+++ b/02_ruway/chasqui/card-admin/README.md
@@ -0,0 +1,11 @@
+# card-admin
+
+`brahman-admin` — the broker's observability.
+
+It exposes a separate Unix socket (not mixed with the handshake) where each
+connection receives a JSON `StatusSnapshot` and is closed. It is single-shot per
+connection: meant for tools that poll.
+
+---
+
+Part of **chasqui** — see [chasqui](../README.md).
diff --git a/02_ruway/chasqui/card-handshake/LEEME.md b/02_ruway/chasqui/card-handshake/LEEME.md
new file mode 100644
index 0000000..aa4da68
--- /dev/null
+++ b/02_ruway/chasqui/card-handshake/LEEME.md
@@ -0,0 +1,23 @@
+# card-handshake
+
+*Read this in English: [README.md](README.md).*
+
+`brahman-handshake` — protocolo runtime Init↔módulo sobre Unix socket.
+
+Implementa la versión concreta de `shared_wit/protocol.wit` (handshake +
+lifecycle): un servidor que vive en el Init (o un Admin proxy) y clientes
+que son los módulos Brahman. Cada conexión arranca con un `Hello` que
+lleva una `card_core::Card`; el servidor valida la Card, deriva el
+`TrustLevel`, emite un `HelloAck` con `session-id` ULID, y a partir de
+ahí acepta `Ping`/`Farewell`.
+
+Wire format: frames length-prefixed (4 bytes LE) con cuerpo
+`postcard`-codificado. Compacto, rápido y reversible.
+
+Esto NO es la implementación WIT/WASM (que generaría wit-bindgen). Es la
+implementación nativa Rust↔Rust que cubre el caso común antes de que los
+módulos WASM consuman el mismo contrato vía ABI generada.
+
+---
+
+Parte de **chasqui** — ver [chasqui](../LEEME.md).
diff --git a/02_ruway/chasqui/card-handshake/README.md b/02_ruway/chasqui/card-handshake/README.md
new file mode 100644
index 0000000..ec26a86
--- /dev/null
+++ b/02_ruway/chasqui/card-handshake/README.md
@@ -0,0 +1,11 @@
+# card-handshake
+
+`brahman-handshake` — the runtime Init↔module protocol over a Unix socket.
+
+It implements the concrete version of `shared_wit/protocol.wit` (handshake +
+lifecycle): a server living in the Init (or an Admin proxy) and clients that are the
+Brahman modules. Every connection starts with a `Hello`.
+
+---
+
+Part of **chasqui** — see [chasqui](../README.md).
diff --git a/02_ruway/chasqui/card-handshake/src/client.rs b/02_ruway/chasqui/card-handshake/src/client.rs
index d93dd33..941c7f3 100644
--- a/02_ruway/chasqui/card-handshake/src/client.rs
+++ b/02_ruway/chasqui/card-handshake/src/client.rs
@@ -82,7 +82,7 @@ where
 {
     /// Constructor genérico sobre un stream ya abierto, **sin firma**.
     /// Apto para path Unix (donde SO_PEERCRED del kernel ya autentica)
-    /// o tests in-memory. Para libp2p remoto usá
+    /// o tests in-memory. Para libp2p remoto usa
     /// [`connect_with_stream_signed`](Self::connect_with_stream_signed) —
     /// el server libp2p rechaza Hello sin firma.
     pub async fn connect_with_stream(
diff --git a/02_ruway/chasqui/card-handshake/src/health.rs b/02_ruway/chasqui/card-handshake/src/health.rs
new file mode 100644
index 0000000..18318ee
--- /dev/null
+++ b/02_ruway/chasqui/card-handshake/src/health.rs
@@ -0,0 +1,91 @@
+//! Observación de la **salud del broker por polling**: diff entre snapshots de
+//! matches tomados en ticks sucesivos.
+//!
+//! A diferencia de [`crate::messages::MatchEvent`] (push del server al consumer,
+//! tipo de protocolo serializable), esto es la observación que arma un monitor
+//! que **consulta** la `MatchList` cada N segundos y deriva qué apareció
+//! (`Available`) y qué desapareció (`Lost`) entre ticks. Vivía atrapado en el
+//! frontend `chasqui-broker-explorer-llimphi`; baja aquí —junto a `MatchList` y
+//! `MatchEventKind`— para que cualquier monitor (UI, CLI) lo reuse (Regla 2).
+
+use std::collections::HashSet;
+
+use chasqui_broker::MatchStrategy;
+
+use crate::messages::{MatchEventKind, MatchList, SessionId};
+
+/// Clave estable de un match: `(sesión_consumer, flow_consumer, sesión_producer,
+/// flow_producer)`. Identifica un match entre ticks para detectar altas y bajas.
+pub type MatchKey = (SessionId, String, SessionId, String);
+
+/// Una entrada del timeline de salud: un alta/baja observada al diferenciar dos
+/// snapshots, con el momento en que el monitor la vio.
+#[derive(Clone, Debug)]
+pub struct TimelineEntry {
+    pub at: std::time::SystemTime,
+    pub kind: MatchEventKind,
+    pub consumer_label: String,
+    pub consumer_flow: String,
+    pub producer_label: String,
+    pub producer_flow: String,
+    pub via: MatchStrategy,
+    pub pinned: bool,
+}
+
+/// Diff puro entre snapshots de matches. Devuelve la lista de entries nuevas
+/// (Available + Lost) en orden Available-primero, y el set actualizado de keys.
+pub fn diff_matches(
+    last_keys: &HashSet<MatchKey>,
+    list: &MatchList,
+) -> (Vec<TimelineEntry>, HashSet<MatchKey>) {
+    let now = std::time::SystemTime::now();
+    let current_keys: HashSet<MatchKey> = list
+        .matches
+        .iter()
+        .map(|m| {
+            (
+                m.consumer.session,
+                m.consumer.flow_name.clone(),
+                m.producer.session,
+                m.producer.flow_name.clone(),
+            )
+        })
+        .collect();
+
+    let mut entries = Vec::new();
+    for m in &list.matches {
+        let key = (
+            m.consumer.session,
+            m.consumer.flow_name.clone(),
+            m.producer.session,
+            m.producer.flow_name.clone(),
+        );
+        if !last_keys.contains(&key) {
+            entries.push(TimelineEntry {
+                at: now,
+                kind: MatchEventKind::Available,
+                consumer_label: m.consumer_label.clone(),
+                consumer_flow: m.consumer.flow_name.clone(),
+                producer_label: m.producer_label.clone(),
+                producer_flow: m.producer.flow_name.clone(),
+                via: m.via,
+                pinned: m.pinned,
+            });
+        }
+    }
+    for key in last_keys.iter() {
+        if !current_keys.contains(key) {
+            entries.push(TimelineEntry {
+                at: now,
+                kind: MatchEventKind::Lost,
+                consumer_label: String::new(),
+                consumer_flow: key.1.clone(),
+                producer_label: String::new(),
+                producer_flow: key.3.clone(),
+                via: MatchStrategy::Exact,
+                pinned: false,
+            });
+        }
+    }
+    (entries, current_keys)
+}
diff --git a/02_ruway/chasqui/card-handshake/src/identity.rs b/02_ruway/chasqui/card-handshake/src/identity.rs
index ac2a46c..a51e396 100644
--- a/02_ruway/chasqui/card-handshake/src/identity.rs
+++ b/02_ruway/chasqui/card-handshake/src/identity.rs
@@ -131,7 +131,7 @@ pub struct SessionCert {
     /// format libp2p protobuf.
     pub session_pubkey: Vec<u8>,
     /// Public key de la master identity, en format libp2p protobuf.
-    /// El verificador deriva el `master_peer_id` desde acá.
+    /// El verificador deriva el `master_peer_id` desde aquí.
     pub master_pubkey: Vec<u8>,
     /// Expiración en milisegundos desde UNIX_EPOCH. Tras esto, el
     /// cert no es válido y el nodo debe re-emitirse uno nuevo
diff --git a/02_ruway/chasqui/card-handshake/src/lib.rs b/02_ruway/chasqui/card-handshake/src/lib.rs
index c744925..28191e3 100644
--- a/02_ruway/chasqui/card-handshake/src/lib.rs
+++ b/02_ruway/chasqui/card-handshake/src/lib.rs
@@ -18,6 +18,7 @@
 #![warn(rust_2018_idioms)]
 
 pub mod codec;
+pub mod health;
 pub mod identity;
 pub mod messages;
 pub mod server;
diff --git a/02_ruway/chasqui/card-handshake/tests/handshake.rs b/02_ruway/chasqui/card-handshake/tests/handshake.rs
index 3c716b6..240590f 100644
--- a/02_ruway/chasqui/card-handshake/tests/handshake.rs
+++ b/02_ruway/chasqui/card-handshake/tests/handshake.rs
@@ -37,6 +37,8 @@ fn sample_card(label: &str) -> Card {
             namespaces: NamespaceSet::default(),
             rlimits: ResourceLimits::default(),
             cpu_affinity: None,
+            run_as: None,
+            mounts: card_core::MountPlan::default(),
         },
         payload: Payload::Virtual,
         supervision: Supervision::OneShot,
@@ -224,6 +226,8 @@ fn card_with_flows(label: &str, input: Vec<Flow>, output: Vec<Flow>) -> Card {
             namespaces: NamespaceSet::default(),
             rlimits: ResourceLimits::default(),
             cpu_affinity: None,
+            run_as: None,
+            mounts: card_core::MountPlan::default(),
         },
         payload: Payload::Virtual,
         supervision: Supervision::OneShot,
diff --git a/02_ruway/chasqui/card-sidecar/LEEME.md b/02_ruway/chasqui/card-sidecar/LEEME.md
new file mode 100644
index 0000000..6b73ff2
--- /dev/null
+++ b/02_ruway/chasqui/card-sidecar/LEEME.md
@@ -0,0 +1,22 @@
+# card-sidecar
+
+*Read this in English: [README.md](README.md).*
+
+`brahman-sidecar` — boilerplate del cliente brahman extraído.
+
+Cualquier módulo que quiera presentarse al Init brahman pero que tenga
+su propio runtime (GPUI, current_thread tokio, std-thread loop, etc.)
+puede llamar `spawn` con su `card_core::Card`. Eso arma un
+thread aparte con un runtime tokio current_thread, conecta al Init,
+y mantiene la sesión viva con pings periódicos.
+
+Si el Init no está disponible, el thread loggea y termina — el módulo
+sigue funcionando standalone.
+
+Errores de conexión / ping se loggean vía `tracing::warn!`. Si quieres
+capturar la salida del thread (por ejemplo para test), usa
+`spawn_with_handle` que devuelve un `JoinHandle`.
+
+---
+
+Parte de **chasqui** — ver [chasqui](../LEEME.md).
diff --git a/02_ruway/chasqui/card-sidecar/README.md b/02_ruway/chasqui/card-sidecar/README.md
new file mode 100644
index 0000000..d0b7391
--- /dev/null
+++ b/02_ruway/chasqui/card-sidecar/README.md
@@ -0,0 +1,11 @@
+# card-sidecar
+
+`brahman-sidecar` — the brahman client's boilerplate, extracted.
+
+Any module that wants to present itself to the brahman Init but has its own runtime
+(a current_thread tokio, an std-thread loop, and so on) can call `spawn` with its
+`card_core::Card`.
+
+---
+
+Part of **chasqui** — see [chasqui](../README.md).
diff --git a/02_ruway/chasqui/card-sidecar/src/discovery.rs b/02_ruway/chasqui/card-sidecar/src/discovery.rs
index 6f9fb1b..ae4c873 100644
--- a/02_ruway/chasqui/card-sidecar/src/discovery.rs
+++ b/02_ruway/chasqui/card-sidecar/src/discovery.rs
@@ -3,7 +3,7 @@
 //! sockets ni reimplementar el patrón a mano.
 //!
 //! Es la generalización de `discover_producer_socket` del CLI
-//! `chasqui attract --remote`: declarás el `TypeRef` que querés
+//! `chasqui attract --remote`: declaras el `TypeRef` que quieres
 //! consumir y el broker te empuja un `MatchEvent::Available` con el
 //! `producer_service_socket` del primer proveedor matched.
 //!
@@ -18,7 +18,7 @@
 //!    `current_thread`.
 //!
 //! Quién elige al proveedor es el broker, no este módulo. Si el
-//! broker tiene `priority_contexts` activo, podés cambiar de
+//! broker tiene `priority_contexts` activo, puedes cambiar de
 //! proveedor sin tocar el consumer; el matching dinámico se respeta.
 
 use std::path::PathBuf;
@@ -70,8 +70,8 @@ pub enum ConsumerError {
 }
 
 /// Construye una Card mínima de consumer que declara un input flow
-/// con el `TypeRef::Primitive { name }` solicitado. Usá esto para
-/// el caso común; si necesitás algo más rico (output flows,
+/// con el `TypeRef::Primitive { name }` solicitado. Usa esto para
+/// el caso común; si necesitas algo más rico (output flows,
 /// permissions, references) construí la Card a mano y pasala a
 /// [`await_provider`] directamente.
 pub fn build_consumer_card(
diff --git a/02_ruway/chasqui/card-sidecar/src/lib.rs b/02_ruway/chasqui/card-sidecar/src/lib.rs
index 1c86df8..5644527 100644
--- a/02_ruway/chasqui/card-sidecar/src/lib.rs
+++ b/02_ruway/chasqui/card-sidecar/src/lib.rs
@@ -9,8 +9,8 @@
 //! Si el Init no está disponible, el thread loggea y termina — el módulo
 //! sigue funcionando standalone.
 //!
-//! Errores de conexión / ping se loggean vía `tracing::warn!`. Si querés
-//! capturar la salida del thread (por ejemplo para test), usá
+//! Errores de conexión / ping se loggean vía `tracing::warn!`. Si quieres
+//! capturar la salida del thread (por ejemplo para test), usa
 //! [`spawn_with_handle`] que devuelve un `JoinHandle`.
 
 #![forbid(unsafe_code)]
@@ -65,7 +65,7 @@ impl SidecarConfig {
     }
 }
 
-/// Spawn fire-and-forget agnóstico. Para módulos conscientes usá
+/// Spawn fire-and-forget agnóstico. Para módulos conscientes usa
 /// [`spawn_conscious`] o [`spawn_with_handle`] con un `SidecarConfig`
 /// personalizado.
 pub fn spawn(card: Card) {
diff --git a/02_ruway/chasqui/chasqui-broker-explorer-llimphi/Cargo.toml b/02_ruway/chasqui/chasqui-broker-explorer-llimphi/Cargo.toml
index a49c7d5..f727dea 100644
--- a/02_ruway/chasqui/chasqui-broker-explorer-llimphi/Cargo.toml
+++ b/02_ruway/chasqui/chasqui-broker-explorer-llimphi/Cargo.toml
@@ -20,9 +20,19 @@ llimphi-widget-banner = { workspace = true }
 llimphi-widget-stat-card = { workspace = true }
 llimphi-widget-menubar = { workspace = true }
 llimphi-widget-context-menu = { workspace = true }
+llimphi-widget-skeleton = { workspace = true }
+llimphi-widget-empty = { workspace = true }
+llimphi-widget-toast = { workspace = true }
+llimphi-icons = { workspace = true }
 llimphi-motion = { workspace = true }
 app-bus = { workspace = true }
+rimay-localize = { workspace = true }
 
 [[bin]]
 name = "chasqui-broker-explorer-llimphi"
 path = "src/main.rs"
+
+[dev-dependencies]
+# Sólo para el example `chasqui_showreel` (volcado headless a PNG, sin winit).
+pollster = { workspace = true }
+png = { workspace = true }
diff --git a/02_ruway/chasqui/chasqui-broker-explorer-llimphi/examples/chasqui_showreel.rs b/02_ruway/chasqui/chasqui-broker-explorer-llimphi/examples/chasqui_showreel.rs
new file mode 100644
index 0000000..cff68d2
--- /dev/null
+++ b/02_ruway/chasqui/chasqui-broker-explorer-llimphi/examples/chasqui_showreel.rs
@@ -0,0 +1,841 @@
+//! **Showreel** de `chasqui` — la capa de red / broker P2P soberano sobre
+//! Cards (brahman). NO es eye-candy abstracto: cada frame reconstruye el
+//! **chrome real** del probe del broker (`chasqui-broker-explorer-llimphi`):
+//! menubar + app-header con la línea de probe + banner de salud + tres
+//! `stat_card` (Estado · Sesiones activas · Timeline de matches) + las filas
+//! del timeline formateadas con `format_timeline_entry` REAL — todo con los
+//! MISMOS widgets que pinta la app en producción (`menubar_view`,
+//! `app_header`, `banner_view`, `stat_card_view`).
+//!
+//! El **estado** se deriva del tiempo normalizado `t∈[0,1]`: el chrome hace
+//! slide-in; la salud del broker recorre DOWN → UP/NO PROVIDER → UP/PROVIDER;
+//! las sesiones (peers) entran con stagger en la card de sesiones; y el
+//! timeline de matches se llena fila a fila — consumidor ← productor
+//! apareciendo en vivo, que es la imagen de "peers que se enganchan por flow".
+//! Cierra con el wordmark **chasqui**.
+//!
+//! Render headless y determinista (sin reloj, sin runtime, sin winit): frame
+//! `i` de `N` → `t = i/(N-1)` → View → layout (taffy + parley) → vello::Scene
+//! → wgpu → PNG. Idéntico al eventloop.
+//!
+//! ```text
+//! cargo run -p chasqui-broker-explorer-llimphi --example chasqui_showreel \
+//!     --release -- [out_dir] [n_frames] [W] [H]
+//! ```
+//! Defaults: `out_dir=showreel_frames_chasqui`, `n_frames=300`, `W=1600`, `H=900`.
+#![allow(dead_code)]
+
+use std::fs::{create_dir_all, File};
+use std::io::BufWriter;
+use std::sync::Arc;
+use std::time::{Duration, SystemTime, UNIX_EPOCH};
+
+use app_bus::{AppMenu, Menu, MenuItem};
+use card_handshake::health::TimelineEntry;
+use card_handshake::messages::MatchEventKind;
+use chasqui_broker::MatchStrategy;
+use llimphi_motion::motion;
+use llimphi_theme::Theme;
+use llimphi_ui::llimphi_hal::{wgpu, Hal};
+use llimphi_ui::llimphi_layout::taffy::{
+    self,
+    prelude::{length, percent, Dimension, FlexDirection, Size, Style},
+    style::Position,
+    AlignItems, Rect,
+};
+use llimphi_ui::llimphi_layout::LayoutTree;
+use llimphi_ui::llimphi_raster::peniko::{self, Color};
+use llimphi_ui::llimphi_raster::vello::kurbo::{Affine, BezPath, Circle, Point, Stroke};
+use llimphi_ui::llimphi_raster::{vello, Renderer};
+use llimphi_ui::llimphi_text::{draw_layout_brush_xf, measurement, Alignment, Typesetter};
+use llimphi_ui::{measure_text_node, mount, paint, Mounted, PaintRect, View};
+
+use llimphi_widget_app_header::{app_header, AppHeaderPalette};
+use llimphi_widget_banner::{banner_view, BannerKind};
+use llimphi_widget_menubar::{menubar_view, MenuBarSpec, DEFAULT_HEIGHT as MENU_H};
+use llimphi_widget_stat_card::{stat_card_view, StatCardPalette};
+
+const FMT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
+
+/// Msg fantasma: el showreel no despacha eventos, pero los widgets reales
+/// exigen un Msg `Clone + 'static`.
+#[derive(Clone)]
+enum Msg {
+    Nada,
+}
+
+// ───────────────────────── utilidades ─────────────────────────
+
+fn with_alpha(c: Color, a: f32) -> Color {
+    let [r, g, b, _] = c.components;
+    Color::new([r, g, b, a.clamp(0.0, 1.0)])
+}
+
+fn lerp(a: f64, b: f64, t: f64) -> f64 {
+    a + (b - a) * t
+}
+
+/// Reescala `t` desde el subintervalo `[lo,hi]` a `[0,1]`, clampado.
+fn seg(t: f32, lo: f32, hi: f32) -> f32 {
+    ((t - lo) / (hi - lo)).clamp(0.0, 1.0)
+}
+
+// ───────────────────────── skin ─────────────────────────
+
+#[derive(Clone)]
+struct Skin {
+    theme: Theme,
+    accent: Color,
+    bg: Color,
+    fg: Color,
+    fg_muted: Color,
+    // Acentos por estado del probe (espejo de la app real).
+    accent_up: Color,
+    accent_partial: Color,
+    accent_down: Color,
+    accent_pending: Color,
+}
+
+// ───────────────────────── datos sintéticos (forma real) ─────────────────────────
+
+/// Una "sesión" (peer) registrada en el broker, con sus flows in/out — la
+/// misma forma que `SessionEntry` produce en la card de sesiones real.
+struct Peer {
+    label: &'static str,
+    inputs: &'static [&'static str],
+    outputs: &'static [&'static str],
+    conscious: bool,
+}
+
+const PEERS: &[Peer] = &[
+    Peer {
+        label: "chasqui-nous",
+        inputs: &["doc-ingest:json"],
+        outputs: &["monad-list:json", "event-log:tail"],
+        conscious: true,
+    },
+    Peer {
+        label: "pluma-app",
+        inputs: &["monad-list:json"],
+        outputs: &["doc-ingest:json"],
+        conscious: false,
+    },
+    Peer {
+        label: "nahual-shell",
+        inputs: &["monad-list:json", "thumb:png"],
+        outputs: &[],
+        conscious: false,
+    },
+    Peer {
+        label: "shuma-term",
+        inputs: &["event-log:tail"],
+        outputs: &["cmd-run:json"],
+        conscious: false,
+    },
+    Peer {
+        label: "khipu",
+        inputs: &[],
+        outputs: &["thumb:png"],
+        conscious: true,
+    },
+];
+
+/// Formatea una entrada de sesión igual que la app real (label · in/out · wit).
+fn fmt_peer(p: &Peer) -> String {
+    format!(
+        "{}  ·  in:[{}]  out:[{}]{}",
+        p.label,
+        p.inputs.join(","),
+        p.outputs.join(","),
+        if p.conscious { "  (wit)" } else { "" }
+    )
+}
+
+/// Matches que el broker va "descubriendo" — consumidor ← productor por flow.
+/// `(consumer_label, flow, producer_label, flow, via, pinned)`.
+struct MatchSeed {
+    consumer_label: &'static str,
+    flow: &'static str,
+    producer_label: &'static str,
+    via: MatchStrategy,
+    pinned: bool,
+}
+
+const MATCHES: &[MatchSeed] = &[
+    MatchSeed {
+        consumer_label: "pluma-app",
+        flow: "monad-list:json",
+        producer_label: "chasqui-nous",
+        via: MatchStrategy::Exact,
+        pinned: true,
+    },
+    MatchSeed {
+        consumer_label: "nahual-shell",
+        flow: "monad-list:json",
+        producer_label: "chasqui-nous",
+        via: MatchStrategy::Exact,
+        pinned: false,
+    },
+    MatchSeed {
+        consumer_label: "chasqui-nous",
+        flow: "doc-ingest:json",
+        producer_label: "pluma-app",
+        via: MatchStrategy::Exact,
+        pinned: false,
+    },
+    MatchSeed {
+        consumer_label: "nahual-shell",
+        flow: "thumb:png",
+        producer_label: "khipu",
+        via: MatchStrategy::Structural,
+        pinned: false,
+    },
+    MatchSeed {
+        consumer_label: "shuma-term",
+        flow: "event-log:tail",
+        producer_label: "chasqui-nous",
+        via: MatchStrategy::Exact,
+        pinned: false,
+    },
+];
+
+/// Convierte un seed en `TimelineEntry` real (kind=Available) con un `at`
+/// determinista escalonado, para reusar el formateo idéntico al de la app.
+fn timeline_entry(seed: &MatchSeed, idx: usize) -> TimelineEntry {
+    // Base fija (no usa el reloj real → determinista): 14:32:10 + idx*7s.
+    let base = UNIX_EPOCH + Duration::from_secs(52_330 + idx as u64 * 7);
+    TimelineEntry {
+        at: base,
+        kind: MatchEventKind::Available,
+        consumer_label: seed.consumer_label.to_string(),
+        consumer_flow: seed.flow.to_string(),
+        producer_label: seed.producer_label.to_string(),
+        producer_flow: seed.flow.to_string(),
+        via: seed.via,
+        pinned: seed.pinned,
+    }
+}
+
+/// Espejo EXACTO de `format_timeline_entry` del binario `src/main.rs` —
+/// no podemos importarlo (es un binario sin lib), así que lo replicamos
+/// carácter a carácter para que las filas se vean idénticas a la app.
+fn format_timeline_entry(e: &TimelineEntry) -> String {
+    let secs_today = e
+        .at
+        .duration_since(SystemTime::UNIX_EPOCH)
+        .map(|d| d.as_secs() % 86_400)
+        .unwrap_or(0);
+    let h = secs_today / 3600;
+    let m = (secs_today % 3600) / 60;
+    let s = secs_today % 60;
+    let kind = match e.kind {
+        MatchEventKind::Available => "+",
+        MatchEventKind::Lost => "-",
+    };
+    let pinned = if e.pinned { " (pinned)" } else { "" };
+    match e.kind {
+        MatchEventKind::Available => format!(
+            "{:02}:{:02}:{:02} {} {}.{} ← {}.{} [{:?}]{}",
+            h,
+            m,
+            s,
+            kind,
+            e.consumer_label,
+            e.consumer_flow,
+            e.producer_label,
+            e.producer_flow,
+            e.via,
+            pinned,
+        ),
+        MatchEventKind::Lost => format!(
+            "{:02}:{:02}:{:02} {} ?.{} ← ?.{} (lost)",
+            h, m, s, kind, e.consumer_flow, e.producer_flow,
+        ),
+    }
+}
+
+// ───────────────────────── chrome real ─────────────────────────
+
+/// Menú real del probe (espejo de `app_menu` del binario).
+fn app_menu() -> AppMenu {
+    AppMenu::new()
+        .menu(
+            Menu::new("Archivo")
+                .item(MenuItem::new("Refrescar probe", "file.refresh").shortcut("Ctrl+R"))
+                .item(MenuItem::new("Limpiar timeline", "file.clear"))
+                .item(MenuItem::new("Salir", "file.quit").shortcut("Ctrl+Q").separated()),
+        )
+        .menu(
+            Menu::new("Ver")
+                .item(MenuItem::new("Reconectar", "view.reconnect"))
+                .item(MenuItem::new("Cambiar tema", "view.theme").separated()),
+        )
+        .menu(Menu::new("Ayuda").item(MenuItem::new("Acerca de", "help.about")))
+}
+
+fn menubar(cw: f64, ch: f64, s: &Skin) -> View<Msg> {
+    let menu = app_menu();
+    menubar_view(&MenuBarSpec {
+        menu: &menu,
+        open: None,
+        theme: &s.theme,
+        viewport: (cw as f32, ch as f32),
+        height: MENU_H,
+        on_open: Arc::new(|_| Msg::Nada),
+        on_command: Arc::new(|_: &str| Msg::Nada),
+    })
+}
+
+/// Fase de salud del broker derivada de `t` — recorre los tres estados
+/// reales que el probe reporta.
+#[derive(Clone, Copy, PartialEq)]
+enum Health {
+    Pending,
+    Down,
+    UpNoProvider,
+    UpProvider,
+}
+
+fn health_at(t: f32) -> Health {
+    if t < 0.20 {
+        Health::Pending
+    } else if t < 0.34 {
+        Health::Down
+    } else if t < 0.46 {
+        Health::UpNoProvider
+    } else {
+        Health::UpProvider
+    }
+}
+
+fn health_banner(h: Health) -> Option<View<Msg>> {
+    match h {
+        Health::Pending => None,
+        Health::Down => Some(banner_view::<Msg>(
+            BannerKind::Error,
+            "Broker DOWN — connect failed (init socket no escucha)".to_string(),
+        )),
+        Health::UpNoProvider => Some(banner_view::<Msg>(
+            BannerKind::Warning,
+            "Broker UP, sin provider para el flow".to_string(),
+        )),
+        Health::UpProvider => Some(banner_view::<Msg>(
+            BannerKind::Success,
+            "Broker UP, provider matcheado".to_string(),
+        )),
+    }
+}
+
+fn state_card_params(h: Health, s: &Skin) -> (Color, String, String) {
+    match h {
+        Health::Pending => (
+            s.accent_pending,
+            "PENDING".into(),
+            "esperando primer probe…".into(),
+        ),
+        Health::Down => (
+            s.accent_down,
+            "DOWN".into(),
+            "connect failed: init socket no responde".into(),
+        ),
+        Health::UpNoProvider => (
+            s.accent_partial,
+            "UP / NO PROVIDER".into(),
+            "broker reachable; sin productor para flow `monad-list:json`".into(),
+        ),
+        Health::UpProvider => (
+            s.accent_up,
+            "UP / PROVIDER".into(),
+            "flow `monad-list:json` matcheado en chasqui-nous.sock".into(),
+        ),
+    }
+}
+
+/// Fila del timeline (espejo de la fila real en `view()`): texto monoespaciado
+/// con la entry formateada, la última resaltada como "recién llegada".
+fn timeline_row(text: String, selected: bool, s: &Skin) -> View<Msg> {
+    let mut row = View::new(Style {
+        size: Size {
+            width: percent(1.0_f32),
+            height: length(20.0_f32),
+        },
+        padding: Rect {
+            left: length(10.0_f32),
+            right: length(8.0_f32),
+            top: length(0.0_f32),
+            bottom: length(0.0_f32),
+        },
+        align_items: Some(AlignItems::Center),
+        flex_shrink: 0.0,
+        ..Default::default()
+    })
+    .text_aligned(text, 12.0, s.theme.fg_text, Alignment::Start);
+    if selected {
+        row = row.fill(s.theme.bg_selected);
+    }
+    row
+}
+
+// ───────────────────────── overlays vector (cold-open + wordmark) ─────────────────────────
+
+fn signature_path(cw: f64, ch: f64) -> BezPath {
+    let cx = cw / 2.0;
+    let cy = ch / 2.0;
+    let mut p = BezPath::new();
+    p.move_to((cx - 360.0, cy + 40.0));
+    p.curve_to(
+        (cx - 150.0, cy - 220.0),
+        (cx + 150.0, cy + 220.0),
+        (cx + 360.0, cy - 40.0),
+    );
+    p
+}
+
+fn trim_path(full: &BezPath, prog: f64) -> (BezPath, Point) {
+    use vello::kurbo::ParamCurve;
+    let prog = prog.clamp(0.0, 1.0);
+    let mut cubic = None;
+    let mut start = Point::ZERO;
+    for el in full.elements() {
+        match el {
+            vello::kurbo::PathEl::MoveTo(p) => start = *p,
+            vello::kurbo::PathEl::CurveTo(c1, c2, p) => {
+                cubic = Some(vello::kurbo::CubicBez::new(start, *c1, *c2, *p));
+            }
+            _ => {}
+        }
+    }
+    let mut out = BezPath::new();
+    let mut head = start;
+    if let Some(cb) = cubic {
+        out.move_to(cb.p0);
+        let steps = 96;
+        for i in 1..=steps {
+            let u = (i as f64 / steps as f64) * prog;
+            let pt = cb.eval(u);
+            out.line_to(pt);
+            head = pt;
+        }
+    }
+    (out, head)
+}
+
+fn draw_overlays(scene: &mut vello::Scene, ts: &mut Typesetter, t: f32, cw: f64, ch: f64, s: &Skin) {
+    // ── COLD OPEN (0–11%) ──────────────────────────────────────────
+    let b1 = seg(t, 0.0, 0.10);
+    let line_vis = 1.0 - seg(t, 0.10, 0.17);
+    if line_vis > 0.001 {
+        let path = signature_path(cw, ch);
+        let draw_on = motion::ease_out_cubic(seg(t, 0.01, 0.11)) as f64;
+        let (trimmed, head) = trim_path(&path, draw_on);
+        let line_col = with_alpha(s.accent, 0.9 * line_vis);
+        scene.stroke(&Stroke::new(2.0), Affine::IDENTITY, line_col, None, &trimmed);
+        let pop = motion::ease_out_back(b1);
+        let r = (4.0 + 7.0 * pop as f64).max(0.0);
+        let dot_a = (b1 * line_vis).clamp(0.0, 1.0);
+        scene.fill(
+            peniko::Fill::NonZero,
+            Affine::IDENTITY,
+            with_alpha(s.accent, 0.18 * dot_a),
+            None,
+            &Circle::new(head, r * 3.2),
+        );
+        scene.fill(
+            peniko::Fill::NonZero,
+            Affine::IDENTITY,
+            with_alpha(s.accent, dot_a),
+            None,
+            &Circle::new(head, r),
+        );
+    }
+
+    // ── WORDMARK (84–100%) ─────────────────────────────────────────
+    let word_in = seg(t, 0.86, 0.96);
+    let word_a = motion::ease_out_cubic(word_in);
+    if word_a > 0.001 {
+        let size = 140.0_f32;
+        let layout = ts.layout(
+            "chasqui", size, None, Alignment::Start, 1.0, false, None, 800.0, false, false, 0.0, 0.0,
+        );
+        let m = measurement(&layout);
+        let rise = lerp(24.0, 0.0, word_a as f64);
+        let ox = (cw - m.width as f64) / 2.0;
+        let oy = (ch - m.height as f64) / 2.0 - 18.0 + rise;
+        let brush = peniko::Brush::Solid(with_alpha(s.fg, word_a));
+        draw_layout_brush_xf(scene, &layout, &brush, Affine::translate((ox, oy)));
+
+        let sub_a = motion::ease_out_cubic(seg(t, 0.90, 1.0));
+        if sub_a > 0.001 {
+            let ssz = 26.0_f32;
+            let sub = ts.layout(
+                "sovereign P2P networking, in Rust",
+                ssz,
+                None,
+                Alignment::Start,
+                1.0,
+                false,
+                None,
+                400.0,
+                false,
+                false, 0.0, 0.0,
+            );
+            let sm = measurement(&sub);
+            let dot_r = 6.0;
+            let block_w = sm.width as f64 + dot_r * 2.0 + 14.0;
+            let sx = (cw - block_w) / 2.0;
+            let sy = oy + m.height as f64 + 18.0;
+            scene.fill(
+                peniko::Fill::NonZero,
+                Affine::IDENTITY,
+                with_alpha(s.accent, sub_a),
+                None,
+                &Circle::new(Point::new(sx + dot_r, sy + ssz as f64 * 0.42), dot_r as f64),
+            );
+            let sbrush = peniko::Brush::Solid(with_alpha(s.fg_muted, sub_a));
+            draw_layout_brush_xf(
+                scene,
+                &sub,
+                &sbrush,
+                Affine::translate((sx + dot_r * 2.0 + 14.0, sy)),
+            );
+        }
+    }
+
+    // ── punto teal de firma (esquina inf-der) ───────
+    let corner_a = seg(t, 0.04, 0.12) * (1.0 - seg(t, 0.82, 0.88));
+    if corner_a > 0.001 {
+        let cx = cw - 54.0;
+        let cy = ch - 54.0;
+        scene.fill(
+            peniko::Fill::NonZero,
+            Affine::IDENTITY,
+            with_alpha(s.accent, 0.16 * corner_a),
+            None,
+            &Circle::new(Point::new(cx, cy), 18.0),
+        );
+        scene.fill(
+            peniko::Fill::NonZero,
+            Affine::IDENTITY,
+            with_alpha(s.accent, 0.9 * corner_a),
+            None,
+            &Circle::new(Point::new(cx, cy), 6.0),
+        );
+    }
+}
+
+// ───────────────────────── la escena por frame ─────────────────────────
+
+fn build_view(t: f32, cw: f64, ch: f64, s: &Skin) -> View<Msg> {
+    // ── timeline de animación ──
+    // chrome slide-in (11–22%): menubar + header.
+    let chrome = motion::ease_out_cubic(seg(t, 0.11, 0.22));
+    // salud del broker (derivada por umbrales).
+    let health = health_at(t);
+    // sesiones (peers) entran con stagger una vez que el broker está UP (34–58%).
+    let peers_reveal = motion::ease_out_cubic(seg(t, 0.34, 0.58));
+    // matches del timeline aparecen fila a fila (46–80%) — sólo con provider.
+    let matches_reveal = motion::ease_out_cubic(seg(t, 0.46, 0.80));
+    // fade del chrome antes del wordmark (82–88%).
+    let chrome_fade = 1.0 - seg(t, 0.82, 0.88);
+
+    let mut children: Vec<View<Msg>> = Vec::new();
+
+    if chrome_fade > 0.001 {
+        let menubar = menubar(cw, ch, s);
+
+        // header con la línea de probe real.
+        let header_palette = AppHeaderPalette::from_theme(&s.theme);
+        let reload_ms = 4 + ((t * 90.0) as u32 % 9);
+        let header_text = format!(
+            "Probe: {}/init.sock  ·  flow: broker-health/ping  ·  reload {} ms",
+            "$XDG_RUNTIME_DIR", reload_ms
+        );
+        let header = app_header::<Msg>(header_text, vec![], &header_palette);
+
+        // ── cuerpo ──
+        let stat_palette = StatCardPalette::from_theme(&s.theme);
+        let mut body_children: Vec<View<Msg>> = Vec::new();
+
+        // banner permanente con la salud actual.
+        if let Some(b) = health_banner(health) {
+            body_children.push(b);
+        }
+
+        // card de Estado.
+        let (state_accent, state_value, state_descr) = state_card_params(health, s);
+        body_children.push(stat_card_view::<Msg>(
+            "Estado",
+            state_value,
+            &state_descr,
+            state_accent,
+            &[],
+            &stat_palette,
+        ));
+
+        // card de Sesiones activas (peers) — los items entran con stagger.
+        let n_peers_up = matches!(
+            health,
+            Health::UpNoProvider | Health::UpProvider
+        )
+        .then(|| ((PEERS.len() as f32) * peers_reveal).ceil() as usize)
+        .unwrap_or(0)
+        .min(PEERS.len());
+        // Listamos hasta 3 peers (los "recientes") para dejar aire al
+        // timeline de matches debajo — el contador refleja el total real.
+        let shown = n_peers_up.min(3);
+        let peer_items: Vec<String> = PEERS[..shown].iter().map(fmt_peer).collect();
+        let sessions_value = if n_peers_up == 0 {
+            "—".to_string()
+        } else {
+            n_peers_up.to_string()
+        };
+        let sessions_descr = if n_peers_up == 0 {
+            "lista no disponible (broker DOWN o pendiente)".to_string()
+        } else {
+            "labels visibles + flows in/out · (wit) = consciente".to_string()
+        };
+        body_children.push(stat_card_view::<Msg>(
+            "Sesiones activas",
+            sessions_value,
+            &sessions_descr,
+            s.accent_up,
+            &peer_items,
+            &stat_palette,
+        ));
+
+        // card de Timeline de matches (cabecera contador) — sólo con provider.
+        let n_matches = if health == Health::UpProvider {
+            (((MATCHES.len()) as f32) * matches_reveal).ceil() as usize
+        } else {
+            0
+        }
+        .min(MATCHES.len());
+        let timeline_value = n_matches.to_string();
+        let timeline_descr = if n_matches == 0 {
+            "esperando primer match…".to_string()
+        } else {
+            "click selecciona · right-click = menú · cap 50 entries".to_string()
+        };
+        body_children.push(stat_card_view::<Msg>(
+            "Timeline de matches",
+            timeline_value,
+            &timeline_descr,
+            s.accent_partial,
+            &[],
+            &stat_palette,
+        ));
+
+        // filas del timeline: consumidor ← productor, la última resaltada.
+        for i in 0..n_matches {
+            let entry = timeline_entry(&MATCHES[i], i);
+            let txt = format_timeline_entry(&entry);
+            let is_last = i + 1 == n_matches && matches_reveal < 1.0;
+            body_children.push(timeline_row(txt, is_last, s));
+        }
+
+        let body = View::new(Style {
+            flex_direction: FlexDirection::Column,
+            size: Size {
+                width: percent(1.0_f32),
+                height: Dimension::auto(),
+            },
+            flex_grow: 1.0,
+            padding: Rect {
+                left: length(16.0_f32),
+                right: length(16.0_f32),
+                top: length(12.0_f32),
+                bottom: length(16.0_f32),
+            },
+            gap: Size {
+                width: length(0.0_f32),
+                height: length(8.0_f32),
+            },
+            ..Default::default()
+        })
+        .fill(s.theme.bg_app)
+        .children(body_children);
+
+        // slide-in vertical del chrome completo.
+        let dy = lerp(-22.0, 0.0, chrome as f64);
+        let chrome_view = View::new(Style {
+            flex_direction: FlexDirection::Column,
+            size: Size {
+                width: percent(1.0_f32),
+                height: percent(1.0_f32),
+            },
+            ..Default::default()
+        })
+        .fill(s.theme.bg_app)
+        .alpha((chrome * chrome_fade).clamp(0.0, 1.0))
+        .transform(Affine::translate((0.0, dy)))
+        .children(vec![menubar, header, body]);
+        children.push(chrome_view);
+    }
+
+    // overlay full-screen del vector (cold-open + wordmark).
+    let overlay = View::new(Style {
+        position: Position::Absolute,
+        inset: Rect {
+            left: length(0.0),
+            top: length(0.0),
+            right: length(0.0),
+            bottom: length(0.0),
+        },
+        size: Size {
+            width: percent(1.0_f32),
+            height: percent(1.0_f32),
+        },
+        ..Default::default()
+    })
+    .paint_with({
+        let s = s.clone();
+        move |scene, ts, _rect: PaintRect| {
+            draw_overlays(scene, ts, t, cw, ch, &s);
+        }
+    });
+    children.push(overlay);
+
+    View::new(Style {
+        size: Size {
+            width: percent(1.0_f32),
+            height: percent(1.0_f32),
+        },
+        position: Position::Relative,
+        ..Default::default()
+    })
+    .fill(s.bg)
+    .children(children)
+}
+
+fn main() {
+    let mut args = std::env::args().skip(1);
+    let out_dir = args.next().unwrap_or_else(|| "showreel_frames_chasqui".to_string());
+    let n: usize = args.next().and_then(|v| v.parse().ok()).unwrap_or(300);
+    let w: u32 = args.next().and_then(|v| v.parse().ok()).unwrap_or(1600);
+    let h: u32 = args.next().and_then(|v| v.parse().ok()).unwrap_or(900);
+    create_dir_all(&out_dir).expect("mkdir out_dir");
+
+    let theme = Theme::dark();
+    let accent = Color::from_rgba8(0x2B, 0xD9, 0xA6, 0xFF); // teal firma
+    let skin = Skin {
+        accent,
+        bg: theme.bg_app,
+        fg: theme.fg_text,
+        fg_muted: theme.fg_muted,
+        // mismos acentos por estado que la app real.
+        accent_up: Color::from_rgba8(0xa3, 0xbe, 0x8c, 0xff),
+        accent_partial: Color::from_rgba8(0xeb, 0xcb, 0x8b, 0xff),
+        accent_down: Color::from_rgba8(0xbf, 0x61, 0x6a, 0xff),
+        accent_pending: Color::from_rgba8(0x6a, 0x72, 0x80, 0xff),
+        theme,
+    };
+
+    let [br, bg, bb, _] = skin.bg.components;
+    let base = Color::from_rgba8((br * 255.0) as u8, (bg * 255.0) as u8, (bb * 255.0) as u8, 255);
+
+    // GPU una sola vez; reusar device/renderer/target para los N frames.
+    let hal = pollster::block_on(Hal::new(None)).expect("hal");
+    let mut renderer = Renderer::new(&hal).expect("renderer");
+    let target = hal.device.create_texture(&wgpu::TextureDescriptor {
+        label: Some("showreel-chasqui"),
+        size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
+        mip_level_count: 1,
+        sample_count: 1,
+        dimension: wgpu::TextureDimension::D2,
+        format: FMT,
+        usage: wgpu::TextureUsages::STORAGE_BINDING
+            | wgpu::TextureUsages::RENDER_ATTACHMENT
+            | wgpu::TextureUsages::COPY_SRC,
+        view_formats: &[],
+    });
+    let view = target.create_view(&wgpu::TextureViewDescriptor::default());
+
+    let mut ts = Typesetter::new();
+    let cw = w as f64;
+    let ch = h as f64;
+
+    for i in 0..n {
+        let t = if n <= 1 { 0.0 } else { i as f32 / (n as f32 - 1.0) };
+        let root = build_view(t, cw, ch, &skin);
+
+        let mut layout = LayoutTree::new();
+        let mounted: Mounted<Msg> = mount(&mut layout, root);
+        let computed = {
+            let tmap = &mounted.text_measures;
+            layout
+                .compute_with_measure(mounted.root, (w as f32, h as f32), |nid, known, avail| {
+                    match tmap.get(&nid) {
+                        Some(tm) => measure_text_node(&mut ts, tm, known, avail),
+                        None => taffy::Size::ZERO,
+                    }
+                })
+                .expect("layout")
+        };
+        let mut scene = vello::Scene::new();
+        paint(&mut scene, &mounted, &computed, &mut ts, None, None);
+
+        renderer
+            .render_to_view(&hal, &scene, &view, w, h, base)
+            .expect("render_to_view");
+        let path = format!("{out_dir}/frame_{i:04}.png");
+        write_png(&hal, &target, &path, w, h);
+        if i % 30 == 0 || i == n - 1 {
+            eprintln!("showreel-chasqui: frame {}/{} (t={:.3})", i + 1, n, t);
+        }
+    }
+    eprintln!("showreel-chasqui: {n} frames en {out_dir}/ ({w}x{h})");
+}
+
+fn write_png(hal: &Hal, target: &wgpu::Texture, path: &str, w: u32, h: u32) {
+    let unpadded = (w * 4) as usize;
+    let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
+    let padded = unpadded.div_ceil(align) * align;
+    let buf = hal.device.create_buffer(&wgpu::BufferDescriptor {
+        label: Some("readback"),
+        size: (padded * h as usize) as u64,
+        usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
+        mapped_at_creation: false,
+    });
+    let mut enc = hal
+        .device
+        .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
+    enc.copy_texture_to_buffer(
+        wgpu::TexelCopyTextureInfo {
+            texture: target,
+            mip_level: 0,
+            origin: wgpu::Origin3d::ZERO,
+            aspect: wgpu::TextureAspect::All,
+        },
+        wgpu::TexelCopyBufferInfo {
+            buffer: &buf,
+            layout: wgpu::TexelCopyBufferLayout {
+                offset: 0,
+                bytes_per_row: Some(padded as u32),
+                rows_per_image: Some(h),
+            },
+        },
+        wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
+    );
+    hal.queue.submit(std::iter::once(enc.finish()));
+    let slice = buf.slice(..);
+    let (tx, rx) = std::sync::mpsc::channel();
+    slice.map_async(wgpu::MapMode::Read, move |r| {
+        let _ = tx.send(r);
+    });
+    let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
+    rx.recv().unwrap().unwrap();
+    let data = slice.get_mapped_range();
+    let mut pixels = Vec::with_capacity((w * h * 4) as usize);
+    for r in 0..h as usize {
+        let sidx = r * padded;
+        pixels.extend_from_slice(&data[sidx..sidx + unpadded]);
+    }
+    drop(data);
+    buf.unmap();
+    let file = File::create(path).expect("png");
+    let mut enc = png::Encoder::new(BufWriter::new(file), w, h);
+    enc.set_color(png::ColorType::Rgba);
+    enc.set_depth(png::BitDepth::Eight);
+    let mut wr = enc.write_header().unwrap();
+    wr.write_image_data(&pixels).unwrap();
+}
diff --git a/02_ruway/chasqui/chasqui-broker-explorer-llimphi/src/main.rs b/02_ruway/chasqui/chasqui-broker-explorer-llimphi/src/main.rs
index 0229101..38d1a6d 100644
--- a/02_ruway/chasqui/chasqui-broker-explorer-llimphi/src/main.rs
+++ b/02_ruway/chasqui/chasqui-broker-explorer-llimphi/src/main.rs
@@ -20,13 +20,14 @@
 //! - `BRAHMAN_BROKER_PROBE_TYPE` — type name del flow probe
 //!   (default `ping`).
 //!
-//! Usá un type name probable (ej. `monad-list:json`,
+//! Usa un type name probable (ej. `monad-list:json`,
 //! `event-log:tail`) para detectar productores específicos del
 //! ecosistema.
 
 #![forbid(unsafe_code)]
 
 use std::collections::{HashSet, VecDeque};
+use std::hash::{Hash, Hasher};
 use std::path::PathBuf;
 use std::sync::Arc;
 use std::time::{Duration, Instant};
@@ -56,9 +57,15 @@ use llimphi_widget_menubar::{
     DEFAULT_HEIGHT as MENU_H,
 };
 use llimphi_widget_stat_card::{stat_card_view, StatCardPalette};
+use llimphi_widget_skeleton::{skeleton_view, SkeletonPalette};
+use llimphi_widget_empty::{empty_view, EmptyPalette};
+use llimphi_widget_toast::{toast_stack_view, Toast};
+use llimphi_icons::Icon;
+#[cfg(test)]
 use ulid::Ulid;
 
 use app_bus::{AppMenu, Menu, MenuItem};
+use rimay_localize::{t, t_args};
 
 const POLL_INTERVAL: Duration = Duration::from_secs(5);
 const PROBE_TIMEOUT: Duration = Duration::from_secs(1);
@@ -67,6 +74,18 @@ const PROBE_TIMEOUT: Duration = Duration::from_secs(1);
 /// más viejo se descarta. 50 cubre ~4 minutos de actividad densa.
 const TIMELINE_CAP: usize = 50;
 
+/// Cuánto vive un toast efímero antes de auto-descartarse.
+const TOAST_TTL: Duration = Duration::from_secs(4);
+
+/// Hash estable de una cadena → `key` para animaciones implícitas
+/// (la misma entry produce siempre la misma key entre rebuilds, así el
+/// pop-in sólo anima la primera aparición).
+fn key_of(s: &str) -> u64 {
+    let mut h = std::collections::hash_map::DefaultHasher::new();
+    s.hash(&mut h);
+    h.finish()
+}
+
 #[derive(Clone, Debug)]
 enum ProbeState {
     Pending,
@@ -75,19 +94,10 @@ enum ProbeState {
     UpWithProvider { flow: String, producer_socket: PathBuf },
 }
 
-#[derive(Clone, Debug)]
-struct TimelineEntry {
-    at: std::time::SystemTime,
-    kind: card_handshake::messages::MatchEventKind,
-    consumer_label: String,
-    consumer_flow: String,
-    producer_label: String,
-    producer_flow: String,
-    via: chasqui_broker::MatchStrategy,
-    pinned: bool,
-}
-
-type MatchKey = (Ulid, String, Ulid, String);
+// El diff de salud del broker (TimelineEntry/MatchKey/diff_matches) vive en el
+// core agnóstico `card_handshake::health` (Regla 2): observar la diff de matches
+// entre ticks es lógica reusable, no exclusiva de este frontend.
+use card_handshake::health::{diff_matches, MatchKey, TimelineEntry};
 
 struct Model {
     theme: Theme,
@@ -113,6 +123,14 @@ struct Model {
     /// Menú contextual sobre una entry: `(idx, x, y)` ancla en ventana.
     /// `None` cerrado.
     context_menu: Option<(usize, f32, f32)>,
+    /// Toasts efímeros vivos (errores del probe, confirmaciones de
+    /// acciones del usuario).
+    toasts: Vec<Toast>,
+    /// Id incremental para correlacionar toast ↔ `Msg` de expiración.
+    next_toast: u64,
+    /// Hay una cadena de `Msg::Shimmer` en vuelo (evita rearmar dos);
+    /// sólo corre mientras el probe está en estado `Pending`.
+    shimmering: bool,
 }
 
 #[derive(Clone)]
@@ -146,6 +164,12 @@ enum Msg {
     ContextMenuOpen(f32, f32),
     /// Limpia el timeline acumulado.
     ClearTimeline,
+    /// Un toast cumplió su `duration`: se descarta del stack.
+    ToastExpire(u64),
+    /// Tick de animación del shimmer del skeleton — fuerza repaint
+    /// mientras el probe está `Pending`. Se auto-rearma sólo si sigue
+    /// cargando, así no queda un loop de repaint ocioso.
+    Shimmer,
 }
 
 struct Explorer;
@@ -163,6 +187,7 @@ impl App for Explorer {
     }
 
     fn init(handle: &Handle<Msg>) -> Model {
+        rimay_localize::init();
         let flow = std::env::var("BRAHMAN_BROKER_PROBE_FLOW")
             .unwrap_or_else(|_| "broker-health".to_string());
         let type_name = std::env::var("BRAHMAN_BROKER_PROBE_TYPE")
@@ -171,7 +196,7 @@ impl App for Explorer {
         handle.dispatch(Msg::Tick);
         handle.spawn_periodic(POLL_INTERVAL, || Msg::Tick);
 
-        Model {
+        let mut m = Model {
             theme: Theme::dark(),
             socket_path: transport::default_socket_path(),
             flow,
@@ -186,7 +211,14 @@ impl App for Explorer {
             menu_anim: Tween::idle(1.0),
             selected: None,
             context_menu: None,
-        }
+            toasts: Vec::new(),
+            next_toast: 0,
+            shimmering: false,
+        };
+        // Mientras esperamos el primer probe (estado `Pending`) corre el
+        // shimmer del skeleton del timeline.
+        ensure_shimmer(&mut m, handle);
+        m
     }
 
     fn on_key(model: &Model, event: &KeyEvent) -> Option<Msg> {
@@ -217,7 +249,30 @@ impl App for Explorer {
                 handle.spawn(move || run_probe(flow, type_name));
             }
             Msg::ProbeResult { state, sessions, matches, elapsed_ms } => {
+                // Toast sólo en la transición de estado (no en cada tick de
+                // 5 s con el mismo estado), para no spamear.
+                let changed =
+                    std::mem::discriminant(&m.state) != std::mem::discriminant(&state);
                 m.state = state;
+                if changed {
+                    let toast = match &m.state {
+                        ProbeState::Down { reason } => Some(Toast::error(
+                            m.next_toast,
+                            t_args("chasqui-broker-down", &[("reason", reason.clone().into())]),
+                            TOAST_TTL,
+                        )),
+                        ProbeState::UpWithProvider { .. } => Some(Toast::success(
+                            m.next_toast,
+                            t("chasqui-toast-provider-matched"),
+                            TOAST_TTL,
+                        )),
+                        _ => None,
+                    };
+                    if let Some(toast) = toast {
+                        m.next_toast += 1;
+                        push_toast(&mut m, handle, toast);
+                    }
+                }
                 m.sessions = sessions;
                 if let Some(list) = matches {
                     m.diff_matches_into_timeline(&list);
@@ -268,6 +323,14 @@ impl App for Explorer {
             }
             Msg::CycleTheme => {
                 m.theme = Theme::next_after(m.theme.name);
+                let id = m.next_toast;
+                m.next_toast += 1;
+                let toast = Toast::info(
+                    id,
+                    t_args("chasqui-toast-theme", &[("name", m.theme.name.to_string().into())]),
+                    TOAST_TTL,
+                );
+                push_toast(&mut m, handle, toast);
             }
             Msg::SelectEntry(i) => {
                 m.selected = Some(i);
@@ -284,8 +347,24 @@ impl App for Explorer {
                 m.timeline.clear();
                 m.selected = None;
                 m.context_menu = None;
+                let id = m.next_toast;
+                m.next_toast += 1;
+                push_toast(
+                    &mut m,
+                    handle,
+                    Toast::info(id, t("chasqui-toast-timeline-cleared"), TOAST_TTL),
+                );
+            }
+            Msg::ToastExpire(id) => {
+                m.toasts.retain(|t| t.id != id);
+            }
+            Msg::Shimmer => {
+                // El thread durmió 50 ms; `ensure_shimmer` reabre la cadena
+                // sólo si seguimos en `Pending`.
+                m.shimmering = false;
             }
         }
+        ensure_shimmer(&mut m, handle);
         m
     }
 
@@ -319,15 +398,15 @@ impl App for Explorer {
             ProbeState::Pending => {}
             ProbeState::Down { reason } => body_children.push(banner_view::<Msg>(
                 BannerKind::Error,
-                format!("Broker DOWN — {reason}"),
+                t_args("chasqui-broker-down", &[("reason", reason.clone().into())]),
             )),
             ProbeState::UpNoProvider { .. } => body_children.push(banner_view::<Msg>(
                 BannerKind::Warning,
-                "Broker UP, sin provider para el flow".to_string(),
+                t("chasqui-banner-no-provider"),
             )),
             ProbeState::UpWithProvider { .. } => body_children.push(banner_view::<Msg>(
                 BannerKind::Success,
-                "Broker UP, provider matcheado".to_string(),
+                t("chasqui-banner-provider-matched"),
             )),
         }
 
@@ -339,8 +418,9 @@ impl App for Explorer {
             accent_down,
             accent_pending,
         );
+        let label_state = t("chasqui-card-state");
         body_children.push(stat_card_view::<Msg>(
-            "Estado",
+            &label_state,
             state_value,
             &state_descr,
             state_accent,
@@ -375,14 +455,13 @@ impl App for Explorer {
             .map(|l| l.entries.len().to_string())
             .unwrap_or_else(|| "—".into());
         let sessions_descr = match &model.sessions {
-            None => "lista no disponible (broker DOWN o pendiente)".to_string(),
-            Some(l) if l.entries.is_empty() => {
-                "sin sesiones registradas en el broker".into()
-            }
-            Some(_) => "labels visibles + flows in/out · (wit) = consciente".into(),
+            None => t("chasqui-sessions-unavailable"),
+            Some(l) if l.entries.is_empty() => t("chasqui-sessions-empty"),
+            Some(_) => t("chasqui-sessions-some"),
         };
+        let label_sessions = t("chasqui-card-sessions");
         body_children.push(stat_card_view::<Msg>(
-            "Sesiones activas",
+            &label_sessions,
             sessions_count_value,
             &sessions_descr,
             accent_up,
@@ -395,12 +474,13 @@ impl App for Explorer {
         // right-click en la raíz abre el contextual).
         let timeline_value = model.timeline.len().to_string();
         let timeline_descr = if model.timeline.is_empty() {
-            "esperando primer match…".to_string()
+            t("chasqui-timeline-waiting")
         } else {
-            "click selecciona · right-click = menú · cap 50 entries".to_string()
+            t("chasqui-timeline-help")
         };
+        let label_timeline = t("chasqui-card-timeline");
         body_children.push(stat_card_view::<Msg>(
-            "Timeline de matches",
+            &label_timeline,
             timeline_value,
             &timeline_descr,
             accent_partial,
@@ -408,32 +488,78 @@ impl App for Explorer {
             &stat_palette,
         ));
 
-        for (i, e) in model.timeline.iter().take(20).enumerate() {
-            let selected = model.selected == Some(i);
-            let mut row = View::new(Style {
-                size: Size {
-                    width: percent(1.0_f32),
-                    height: length(18.0_f32),
-                },
-                padding: Rect {
-                    left: length(8.0_f32),
-                    right: length(8.0_f32),
-                    top: length(0.0_f32),
-                    bottom: length(0.0_f32),
-                },
-                ..Default::default()
-            })
-            .text_aligned(
-                format_timeline_entry(e),
-                11.0,
-                theme.fg_text,
-                Alignment::Start,
-            )
-            .on_click(Msg::SelectEntry(i));
-            if selected {
-                row = row.fill(theme.bg_selected);
+        // Timeline tri-estado:
+        //  1) Pending y vacío → filas skeleton con shimmer (esperando 1er probe).
+        //  2) vacío pero ya sondeado → empty-state con orientación.
+        //  3) con entries → filas reales, cada una entra con pop-in la
+        //     primera vez que aparece su key.
+        if model.timeline.is_empty() {
+            if matches!(model.state, ProbeState::Pending) {
+                let skel = SkeletonPalette::from_theme(theme);
+                for _ in 0..5 {
+                    body_children.push(
+                        View::new(Style {
+                            size: Size {
+                                width: percent(1.0_f32),
+                                height: length(18.0_f32),
+                            },
+                            flex_shrink: 0.0,
+                            ..Default::default()
+                        })
+                        .radius(4.0)
+                        .clip(true)
+                        .children(vec![skeleton_view::<Msg>(&skel)]),
+                    );
+                }
+            } else {
+                let empty_pal = EmptyPalette::from_theme(theme);
+                let empty_descr = t("chasqui-empty-descr");
+                body_children.push(
+                    View::new(Style {
+                        size: Size {
+                            width: percent(1.0_f32),
+                            height: length(160.0_f32),
+                        },
+                        flex_shrink: 0.0,
+                        ..Default::default()
+                    })
+                    .children(vec![empty_view::<Msg>(
+                        Icon::Bell,
+                        t("chasqui-empty-title"),
+                        Some(empty_descr.as_str()),
+                        &empty_pal,
+                    )]),
+                );
+            }
+        } else {
+            for (i, e) in model.timeline.iter().take(20).enumerate() {
+                let selected = model.selected == Some(i);
+                let mut row = View::new(Style {
+                    size: Size {
+                        width: percent(1.0_f32),
+                        height: length(18.0_f32),
+                    },
+                    padding: Rect {
+                        left: length(8.0_f32),
+                        right: length(8.0_f32),
+                        top: length(0.0_f32),
+                        bottom: length(0.0_f32),
+                    },
+                    ..Default::default()
+                })
+                .text_aligned(
+                    format_timeline_entry(e),
+                    11.0,
+                    theme.fg_text,
+                    Alignment::Start,
+                )
+                .on_click(Msg::SelectEntry(i))
+                .animated_pop_in(key_of(&format_timeline_entry(e)), motion::NORMAL);
+                if selected {
+                    row = row.fill(theme.bg_selected);
+                }
+                body_children.push(row);
             }
-            body_children.push(row);
         }
 
         let body = View::new(Style {
@@ -458,7 +584,7 @@ impl App for Explorer {
         .fill(theme.bg_app)
         .children(body_children);
 
-        View::new(Style {
+        let root = View::new(Style {
             flex_direction: FlexDirection::Column,
             size: Size {
                 width: percent(1.0_f32),
@@ -470,7 +596,26 @@ impl App for Explorer {
         // Right-click en la raíz (origen 0,0 ⇒ local == ventana) abre el
         // menú contextual sobre la entry seleccionada.
         .on_right_click_at(|x, y, _w, _h| Some(Msg::ContextMenuOpen(x, y)))
-        .children(vec![menubar, header, body])
+        .children(vec![menubar, header, body]);
+
+        // Overlay de toasts efímeros (bottom-right). Click en uno = descartarlo.
+        let now = Instant::now();
+        let alive: Vec<Toast> = model.toasts.iter().filter(|t| t.is_alive(now)).cloned().collect();
+        if alive.is_empty() {
+            root
+        } else {
+            View::new(Style {
+                size: Size {
+                    width: percent(1.0_f32),
+                    height: percent(1.0_f32),
+                },
+                ..Default::default()
+            })
+            .children(vec![
+                root,
+                toast_stack_view(&alive, viewport_of(model), Msg::ToastExpire),
+            ])
+        }
     }
 
     fn view_overlay(model: &Model) -> Option<View<Msg>> {
@@ -480,13 +625,13 @@ impl App for Explorer {
                 .timeline
                 .get(idx)
                 .map(format_timeline_entry)
-                .unwrap_or_else(|| "Entry".to_string());
+                .unwrap_or_else(|| t("chasqui-entry-fallback"));
             let viewport = viewport_of(model);
             // Acciones reales del probe (sólo lectura): refrescar el
             // probe y limpiar el timeline. No inventamos edición.
             let items = vec![
-                ContextMenuItem::action("Refrescar probe"),
-                ContextMenuItem::action("Limpiar timeline"),
+                ContextMenuItem::action(t("chasqui-refresh-probe")),
+                ContextMenuItem::action(t("chasqui-clear-timeline")),
             ];
             let on_pick: Arc<dyn Fn(usize) -> Msg + Send + Sync> =
                 Arc::new(move |i: usize| match i {
@@ -545,26 +690,57 @@ fn menubar_spec<'a>(
 fn app_menu() -> AppMenu {
     AppMenu::new()
         .menu(
-            Menu::new("Archivo")
-                .item(MenuItem::new("Refrescar probe", "file.refresh").shortcut("Ctrl+R"))
-                .item(MenuItem::new("Limpiar timeline", "file.clear"))
-                .item(MenuItem::new("Salir", "file.quit").shortcut("Ctrl+Q").separated()),
+            Menu::new(t("chasqui-menu-file"))
+                .item(MenuItem::new(t("chasqui-refresh-probe"), "file.refresh").shortcut("Ctrl+R"))
+                .item(MenuItem::new(t("chasqui-clear-timeline"), "file.clear"))
+                .item(MenuItem::new(t("exit"), "file.quit").shortcut("Ctrl+Q").separated()),
         )
         .menu(
-            Menu::new("Ver")
-                .item(MenuItem::new("Reconectar", "view.reconnect"))
-                .item(MenuItem::new("Cambiar tema", "view.theme").separated()),
+            Menu::new(t("chasqui-menu-view"))
+                .item(MenuItem::new(t("chasqui-menu-reconnect"), "view.reconnect"))
+                .item(MenuItem::new(t("cycle-theme"), "view.theme").separated()),
         )
-        .menu(Menu::new("Ayuda").item(MenuItem::new("Acerca de", "help.about")))
+        .menu(Menu::new(t("help")).item(MenuItem::new(t("about"), "help.about")))
+}
+
+/// Empuja un toast al stack y programa su expiración (acción real del
+/// usuario o evento del probe, no ruido de cada tick).
+fn push_toast(m: &mut Model, handle: &Handle<Msg>, toast: Toast) {
+    let id = toast.id;
+    m.toasts.push(toast);
+    handle.spawn(move || {
+        std::thread::sleep(TOAST_TTL);
+        Msg::ToastExpire(id)
+    });
+}
+
+/// Arranca la cadena de ticks del shimmer si el probe sigue `Pending` y no
+/// hay ya una corriendo. Se auto-detiene cuando el primer probe responde.
+fn ensure_shimmer(m: &mut Model, handle: &Handle<Msg>) {
+    if m.shimmering || !matches!(m.state, ProbeState::Pending) {
+        return;
+    }
+    m.shimmering = true;
+    handle.spawn(move || {
+        std::thread::sleep(Duration::from_millis(50));
+        Msg::Shimmer
+    });
 }
 
 /// Traduce un command id del menú principal al `Msg`/efecto real.
-fn handle_menu_command(model: Model, cmd: &str, handle: &Handle<Msg>) -> Model {
+fn handle_menu_command(mut model: Model, cmd: &str, handle: &Handle<Msg>) -> Model {
     match cmd {
         // Reconectar == re-disparar el probe; el ProbeResult refresca
         // estado/sesiones/matches.
         "file.refresh" | "view.reconnect" => {
             handle.dispatch(Msg::Tick);
+            let id = model.next_toast;
+            model.next_toast += 1;
+            push_toast(
+                &mut model,
+                handle,
+                Toast::info(id, t("chasqui-toast-refreshing"), TOAST_TTL),
+            );
             model
         }
         "file.clear" => {
@@ -612,24 +788,27 @@ fn state_card_params(
         ProbeState::Pending => (
             accent_pending,
             "PENDING".into(),
-            "esperando primer probe…".into(),
+            t("chasqui-state-pending"),
         ),
         ProbeState::Down { reason } => (
             accent_down,
             "DOWN".into(),
-            format!("connect failed: {reason}"),
+            t_args("chasqui-state-down", &[("reason", reason.clone().into())]),
         ),
         ProbeState::UpNoProvider { flow } => (
             accent_partial,
             "UP / NO PROVIDER".into(),
-            format!("broker reachable; sin productor para flow `{flow}`"),
+            t_args("chasqui-state-no-provider", &[("flow", flow.clone().into())]),
         ),
         ProbeState::UpWithProvider { flow, producer_socket } => (
             accent_up,
             "UP / PROVIDER".into(),
-            format!(
-                "flow `{flow}` matcheado en producer socket: {}",
-                producer_socket.display()
+            t_args(
+                "chasqui-state-provider",
+                &[
+                    ("flow", flow.clone().into()),
+                    ("socket", producer_socket.display().to_string().into()),
+                ],
             ),
         ),
     }
@@ -667,66 +846,6 @@ fn run_probe(flow: String, type_name: String) -> Msg {
     }
 }
 
-/// Diff puro entre snapshots de matches. Devuelve la lista de
-/// entries nuevas (Available + Lost) en orden Available-primero, y
-/// el set actualizado de keys.
-fn diff_matches(
-    last_keys: &HashSet<MatchKey>,
-    list: &card_handshake::messages::MatchList,
-) -> (Vec<TimelineEntry>, HashSet<MatchKey>) {
-    use card_handshake::messages::MatchEventKind;
-    let now = std::time::SystemTime::now();
-    let current_keys: HashSet<MatchKey> = list
-        .matches
-        .iter()
-        .map(|m| {
-            (
-                m.consumer.session,
-                m.consumer.flow_name.clone(),
-                m.producer.session,
-                m.producer.flow_name.clone(),
-            )
-        })
-        .collect();
-
-    let mut entries = Vec::new();
-    for m in &list.matches {
-        let key = (
-            m.consumer.session,
-            m.consumer.flow_name.clone(),
-            m.producer.session,
-            m.producer.flow_name.clone(),
-        );
-        if !last_keys.contains(&key) {
-            entries.push(TimelineEntry {
-                at: now,
-                kind: MatchEventKind::Available,
-                consumer_label: m.consumer_label.clone(),
-                consumer_flow: m.consumer.flow_name.clone(),
-                producer_label: m.producer_label.clone(),
-                producer_flow: m.producer.flow_name.clone(),
-                via: m.via,
-                pinned: m.pinned,
-            });
-        }
-    }
-    for key in last_keys.iter() {
-        if !current_keys.contains(key) {
-            entries.push(TimelineEntry {
-                at: now,
-                kind: MatchEventKind::Lost,
-                consumer_label: String::new(),
-                consumer_flow: key.1.clone(),
-                producer_label: String::new(),
-                producer_flow: key.3.clone(),
-                via: chasqui_broker::MatchStrategy::Exact,
-                pinned: false,
-            });
-        }
-    }
-    (entries, current_keys)
-}
-
 fn format_timeline_entry(e: &TimelineEntry) -> String {
     use card_handshake::messages::MatchEventKind;
     let secs_today = e
diff --git a/02_ruway/chasqui/chasqui-broker/Cargo.toml b/02_ruway/chasqui/chasqui-broker/Cargo.toml
index 05eac74..b46a315 100644
--- a/02_ruway/chasqui/chasqui-broker/Cargo.toml
+++ b/02_ruway/chasqui/chasqui-broker/Cargo.toml
@@ -11,5 +11,4 @@ description = "Brahman — broker de tipos: empareja productores↔consumidores
 [dependencies]
 card-core = { workspace = true }
 serde = { workspace = true }
-thiserror = { workspace = true }
 ulid = { workspace = true }
diff --git a/02_ruway/chasqui/chasqui-card/src/intension.rs b/02_ruway/chasqui/chasqui-card/src/intension.rs
new file mode 100644
index 0000000..83cb8d1
--- /dev/null
+++ b/02_ruway/chasqui/chasqui-card/src/intension.rs
@@ -0,0 +1,154 @@
+//! Cuerpo **intensional** de una Mónada: la regla que (re)deriva sus
+//! miembros en cada scan, en vez de curarlos a mano.
+//!
+//! Una Mónada **extensional** lista sus miembros explícitamente (un
+//! álbum: estas 40 fotos). Una Mónada **intensional** no guarda la
+//! lista: guarda un *predicado* ([`MonadQuery`]) que el motor evalúa
+//! contra el corpus para producir los miembros ("Fotos" = todo lo que
+//! sea imagen, en cualquier parte). Es la misma idea que una
+//! smart-playlist o un saved-search: la membresía es una consulta, no
+//! una colección.
+//!
+//! Este crate sólo define el **tipo** de la regla (dato puro,
+//! serializable). La **evaluación** —que necesita leer el corpus y, para
+//! [`MonadQuery::Near`], los embeddings— vive en `chasqui-core::resolve`,
+//! igual que el resto de la lógica de scan/cluster/attraction.
+
+use std::collections::BTreeSet;
+
+use serde::{Deserialize, Serialize};
+
+use crate::Lens;
+
+/// Predicado que define los miembros de una Mónada intensional.
+///
+/// Es un álgebra chica y cerrada bajo composición ([`All`](Self::All) /
+/// [`Any`](Self::Any) / [`Not`](Self::Not)), de modo que casos reales se
+/// expresan combinando hojas simples:
+///
+/// - `"Fotos"` = `Lens { lens: Gallery }` (o `Extension` de los formatos
+///   de imagen, si se prefiere por extensión).
+/// - `"Código Rust grande"` = `All { of: [Extension{rs}, ...] }`.
+/// - `"Cerca de este viaje"` = `Near { min_similarity }`, comparando el
+///   embedding de cada archivo contra el `centroid` de la propia Mónada
+///   (que vive en el manifiesto, no aquí — la query es agnóstica del
+///   centroide concreto).
+///
+/// Las hojas [`Extension`](Self::Extension) y [`Lens`](Self::Lens) son
+/// deterministas y baratas (no requieren embeddings); [`Near`](Self::Near)
+/// es la hoja semántica que apoya en el modelo vectorial de `chasqui`.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(tag = "op", rename_all = "snake_case")]
+pub enum MonadQuery {
+    /// Verdadero si la extensión del archivo (lowercase, sin punto) está
+    /// en el conjunto. `{"png","jpg","jpeg",...}` = "imágenes por formato".
+    Extension {
+        /// Extensiones aceptadas, ya normalizadas a lowercase sin punto.
+        exts: BTreeSet<String>,
+    },
+    /// Verdadero si el lente discernido del archivo coincide. Es el modo
+    /// natural de "todo lo que se ve con tal vista" (Gallery, Code, …).
+    Lens {
+        /// Lente exigido.
+        lens: Lens,
+    },
+    /// Verdadero si el embedding del archivo está a coseno ≥
+    /// `min_similarity` del centroide de la Mónada. La hoja semántica:
+    /// "lo que se parece a esto".
+    Near {
+        /// Umbral de similitud coseno en `[-1, 1]`. Típico ≈ 0.7.
+        min_similarity: f32,
+    },
+    /// Conjunción: el archivo entra si satisface **todas** las
+    /// subconsultas. `of` vacío ⇒ verdadero (neutro del AND).
+    All {
+        /// Subconsultas que deben cumplirse todas.
+        of: Vec<MonadQuery>,
+    },
+    /// Disyunción: el archivo entra si satisface **alguna**. `of` vacío
+    /// ⇒ falso (neutro del OR).
+    Any {
+        /// Subconsultas, basta una.
+        of: Vec<MonadQuery>,
+    },
+    /// Negación: invierte la subconsulta.
+    Not {
+        /// Subconsulta a negar.
+        inner: Box<MonadQuery>,
+    },
+}
+
+impl MonadQuery {
+    /// Atajo: una query de imágenes por las extensiones más comunes.
+    /// Útil para sembrar la Mónada intensional canónica "Fotos".
+    pub fn imagenes() -> Self {
+        MonadQuery::Lens { lens: Lens::Gallery }
+    }
+
+    /// Atajo: una query por un único formato de archivo.
+    pub fn extension(ext: impl Into<String>) -> Self {
+        let mut exts = BTreeSet::new();
+        exts.insert(ext.into().to_lowercase());
+        MonadQuery::Extension { exts }
+    }
+
+    /// `true` si la query es puramente léxica (Extension/composición de
+    /// Extension) y por lo tanto evaluable **sin** leer el contenido ni
+    /// los embeddings — sólo mirando la ruta/extensión. Le sirve al
+    /// motor para decidir si puede resolverla en frío.
+    pub fn es_lexica(&self) -> bool {
+        match self {
+            MonadQuery::Extension { .. } => true,
+            MonadQuery::Lens { .. } | MonadQuery::Near { .. } => false,
+            MonadQuery::All { of } | MonadQuery::Any { of } => of.iter().all(Self::es_lexica),
+            MonadQuery::Not { inner } => inner.es_lexica(),
+        }
+    }
+
+    /// `true` si la query usa la hoja semántica [`Near`](Self::Near) en
+    /// algún lugar — es decir, su evaluación necesita un centroide y los
+    /// embeddings del corpus.
+    pub fn usa_embeddings(&self) -> bool {
+        match self {
+            MonadQuery::Near { .. } => true,
+            MonadQuery::Extension { .. } | MonadQuery::Lens { .. } => false,
+            MonadQuery::All { of } | MonadQuery::Any { of } => of.iter().any(Self::usa_embeddings),
+            MonadQuery::Not { inner } => inner.usa_embeddings(),
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn roundtrip_json_con_tag_op() {
+        let q = MonadQuery::All {
+            of: vec![
+                MonadQuery::extension("rs"),
+                MonadQuery::Not {
+                    inner: Box::new(MonadQuery::Near { min_similarity: 0.8 }),
+                },
+            ],
+        };
+        let s = serde_json::to_string(&q).unwrap();
+        assert!(s.contains(r#""op":"all""#), "{s}");
+        let back: MonadQuery = serde_json::from_str(&s).unwrap();
+        assert_eq!(back, q);
+    }
+
+    #[test]
+    fn lexica_vs_semantica() {
+        assert!(MonadQuery::extension("png").es_lexica());
+        assert!(!MonadQuery::imagenes().es_lexica()); // Lens necesita discernir
+        assert!(!MonadQuery::Near { min_similarity: 0.7 }.es_lexica());
+
+        let mixta = MonadQuery::Any {
+            of: vec![MonadQuery::extension("png"), MonadQuery::Near { min_similarity: 0.7 }],
+        };
+        assert!(!mixta.es_lexica());
+        assert!(mixta.usa_embeddings());
+        assert!(!MonadQuery::extension("png").usa_embeddings());
+    }
+}
diff --git a/02_ruway/chasqui/chasqui-card/src/lib.rs b/02_ruway/chasqui/chasqui-card/src/lib.rs
index 3712b80..0d9deb6 100644
--- a/02_ruway/chasqui/chasqui-card/src/lib.rs
+++ b/02_ruway/chasqui/chasqui-card/src/lib.rs
@@ -33,10 +33,20 @@ use ulid::Ulid;
 // Re-export para consumidores
 pub use ::ulid;
 
+pub mod intension;
 pub mod query;
 
+pub use intension::MonadQuery;
+
 /// Versión del esquema del manifiesto. Bump al cambiar el schema.
-pub const MONAD_SCHEMA_VERSION: u16 = 1;
+///
+/// v2 (grafo de Mónadas): `MonadManifest` gana `submonads` (contención
+/// DAG: una Mónada puede contener otras) y `query` (cuerpo intensional:
+/// los miembros se derivan de un [`MonadQuery`] en vez de curarse a
+/// mano). Ambos campos son aditivos con `serde default`, así que un
+/// manifiesto v1 deserializa sin pérdida; `validate` acepta esquemas
+/// **iguales o anteriores** (sólo rechaza los más nuevos que no entiende).
+pub const MONAD_SCHEMA_VERSION: u16 = 2;
 
 /// Identificador opaco de un archivo registrado en la DB.
 pub type FileId = Ulid;
@@ -176,6 +186,26 @@ pub struct MonadManifest {
     /// IDs de archivos miembros (incluye pins).
     pub members: BTreeSet<FileId>,
 
+    /// **Sub-Mónadas contenidas** (contención DAG). Una Mónada puede
+    /// contener otras además de archivos: "Fotos" contiene el álbum
+    /// "Viaje", que contiene las fotos. Como una sub-Mónada se referencia
+    /// por su `MonadId`, la *misma* sub-Mónada puede aparecer bajo varios
+    /// padres — multi-pertenencia sin duplicar el objeto. Es el eje de
+    /// **contención**, ortogonal a `lineage` (que es *derivación*:
+    /// split/merge histórico, no quién-contiene-a-quién).
+    #[serde(default)]
+    pub submonads: BTreeSet<MonadId>,
+
+    /// Cuerpo **intensional**, si la Mónada es una consulta. `None` =
+    /// Mónada extensional (los `members` se curan a mano o por
+    /// clustering). `Some(q)` = los miembros se (re)derivan evaluando `q`
+    /// contra el corpus en cada scan; `members` queda como la
+    /// materialización cacheada de esa evaluación (más los `pins`, que el
+    /// usuario fija aunque la query no los capture). Ver
+    /// [`intension::MonadQuery`].
+    #[serde(default)]
+    pub query: Option<MonadQuery>,
+
     /// Unix ms de creación de la Mónada.
     pub created_at_ms: u64,
 
@@ -232,6 +262,8 @@ impl MonadManifest {
             dominant_lens: Lens::default(),
             pins: BTreeSet::new(),
             members: BTreeSet::new(),
+            submonads: BTreeSet::new(),
+            query: None,
             created_at_ms: now_ms,
             updated_at_ms: now_ms,
             extensions: BTreeMap::new(),
@@ -240,7 +272,11 @@ impl MonadManifest {
 
     /// Validación semántica.
     pub fn validate(&self) -> Result<(), MonadError> {
-        if self.schema_version != MONAD_SCHEMA_VERSION {
+        // Aceptamos esquemas iguales o anteriores (los campos nuevos son
+        // aditivos con default). Sólo rechazamos uno MÁS nuevo que el que
+        // este binario entiende: ahí sí podrían venir invariantes que no
+        // sabemos verificar.
+        if self.schema_version > MONAD_SCHEMA_VERSION {
             return Err(MonadError::SchemaMismatch {
                 got: self.schema_version,
                 expected: MONAD_SCHEMA_VERSION,
@@ -255,7 +291,15 @@ impl MonadManifest {
         if !(0.0..=1.0).contains(&self.entropy) {
             return Err(MonadError::InvalidEntropy(self.entropy));
         }
-        if self.members.is_empty() && self.pins.is_empty() {
+        // Una Mónada es no-vacía si tiene algo que mostrar o una regla que
+        // la define: archivos, pines, sub-Mónadas, o un cuerpo intensional
+        // (que puede resolver a cero miembros hoy y a muchos tras el
+        // próximo scan — sigue siendo una Mónada legítima).
+        if self.members.is_empty()
+            && self.pins.is_empty()
+            && self.submonads.is_empty()
+            && self.query.is_none()
+        {
             return Err(MonadError::Empty);
         }
         let actual = self.members.len() as u32;
@@ -412,6 +456,58 @@ mod tests {
         assert_eq!(data.presentation_hint, "code");
     }
 
+    #[test]
+    fn submonads_solo_es_no_vacia() {
+        // Una Mónada-contenedor que sólo agrupa otras Mónadas (sin
+        // archivos propios) es válida — es el caso de "Fotos" conteniendo
+        // álbumes.
+        let mut m = MonadManifest::new("Fotos");
+        m.submonads.insert(Ulid::new());
+        m.touch();
+        m.validate().expect("submonads-only debe validar");
+    }
+
+    #[test]
+    fn query_sola_es_no_vacia() {
+        // Una Mónada intensional recién creada puede no tener miembros
+        // materializados todavía y aun así ser válida: su cuerpo es la
+        // query.
+        let mut m = MonadManifest::new("Imágenes");
+        m.query = Some(MonadQuery::imagenes());
+        m.touch();
+        assert_eq!(m.cardinality, 0);
+        m.validate().expect("query-only debe validar");
+    }
+
+    #[test]
+    fn schema_anterior_se_acepta() {
+        // Un manifiesto de un esquema previo (v1) sigue siendo legible:
+        // validate sólo rechaza esquemas MÁS nuevos.
+        let mut m = MonadManifest::new("legacy");
+        m.members.insert(Ulid::new());
+        m.touch();
+        m.schema_version = 1;
+        m.validate().expect("v1 debe validar contra un binario v2");
+
+        m.schema_version = MONAD_SCHEMA_VERSION + 1;
+        assert!(matches!(m.validate(), Err(MonadError::SchemaMismatch { .. })));
+    }
+
+    #[test]
+    fn json_roundtrip_preserva_grafo() {
+        let mut m = MonadManifest::new("Fotos");
+        m.submonads.insert(Ulid::new());
+        m.submonads.insert(Ulid::new());
+        m.query = Some(MonadQuery::Any {
+            of: vec![MonadQuery::extension("png"), MonadQuery::extension("jpg")],
+        });
+        m.touch();
+        let s = m.to_json_pretty().unwrap();
+        let m2 = MonadManifest::from_json(&s).unwrap();
+        assert_eq!(m2.submonads, m.submonads);
+        assert_eq!(m2.query, m.query);
+    }
+
     #[test]
     fn json_roundtrip() {
         let mut m = MonadManifest::new("test-monad");
diff --git a/02_ruway/chasqui/chasqui-core/Cargo.toml b/02_ruway/chasqui/chasqui-core/Cargo.toml
index 1827a00..c09f842 100644
--- a/02_ruway/chasqui/chasqui-core/Cargo.toml
+++ b/02_ruway/chasqui/chasqui-core/Cargo.toml
@@ -13,17 +13,18 @@ chasqui-card = { path = "../chasqui-card" }
 chasqui-nous = { path = "../chasqui-nous" }
 shuma-discern = { workspace = true }
 card-core = { workspace = true }
-card-handshake = { path = "../card-handshake" }
 card-sidecar = { path = "../card-sidecar" }
 blake3 = { workspace = true }
-serde = { workspace = true }
 serde_json = { workspace = true }
 sled = { workspace = true }
 thiserror = { workspace = true }
-tokio = { workspace = true }
 ulid = { workspace = true }
 walkdir = "2"
 notify = { workspace = true }
+# El contrato del DAG navegable compartido (PLAN-GRAFO): el adaptador
+# `grafo::GrafoMonadas` ve el grafo de contención de Mónadas como GrafoNavegable
+# para el cockpit universal (visor de DAGs en nahual).
+grafo-nav = { workspace = true }
 
 [dev-dependencies]
 tempfile = { workspace = true }
diff --git a/02_ruway/chasqui/chasqui-core/src/bin/nouser.rs b/02_ruway/chasqui/chasqui-core/src/bin/nouser.rs
index f47087f..65e19f3 100644
--- a/02_ruway/chasqui/chasqui-core/src/bin/nouser.rs
+++ b/02_ruway/chasqui/chasqui-core/src/bin/nouser.rs
@@ -118,7 +118,7 @@ fn cmd_scan(args: &[String]) -> Cmd {
         min_files()
     );
     if db.monad_count() == 0 {
-        println!("  (ninguna Mónada — bajá NOUSER_MIN_FILES o apuntá a un dir con más archivos)");
+        println!("  (ninguna Mónada — baja NOUSER_MIN_FILES o apunta a un dir con más archivos)");
         return Ok(());
     }
     println!();
diff --git a/02_ruway/chasqui/chasqui-core/src/cluster.rs b/02_ruway/chasqui/chasqui-core/src/cluster.rs
index 85af5b5..5bcb1c9 100644
--- a/02_ruway/chasqui/chasqui-core/src/cluster.rs
+++ b/02_ruway/chasqui/chasqui-core/src/cluster.rs
@@ -155,9 +155,13 @@ fn top_extensions(files: &[&FileEntry], n: usize) -> Vec<String> {
 /// Elige el lente dominante según la extensión más frecuente, con
 /// fallback a `shuma-discern` sobre el head del archivo más
 /// representativo cuando la extensión no da hint claro (Lens::Grid).
-fn pick_lens(files: &[&FileEntry]) -> Lens {
-    let dominant = top_extensions(files, 1).into_iter().next();
-    let by_ext = match dominant.as_deref() {
+/// Mapea una extensión (lowercase, sin punto) a su lente determinista,
+/// o [`Lens::Grid`] si la extensión no da hint claro. Determinista y sin
+/// disco — la fuente de verdad del "lente por formato", compartida entre
+/// el clustering ([`pick_lens`]) y la evaluación de queries intensionales
+/// ([`crate::resolve`]).
+pub fn lens_from_ext(ext: Option<&str>) -> Lens {
+    match ext {
         Some("rs" | "py" | "ts" | "tsx" | "js" | "jsx" | "go" | "java" | "kt" | "c" | "cpp"
         | "cc" | "h" | "hpp" | "rb" | "swift" | "zig") => Lens::Code,
         Some("png" | "jpg" | "jpeg" | "gif" | "webp" | "svg" | "bmp" | "tiff" | "heic") => {
@@ -166,7 +170,12 @@ fn pick_lens(files: &[&FileEntry]) -> Lens {
         Some("md" | "markdown" | "rst" | "txt" | "org" | "tex") => Lens::Markdown,
         Some("db" | "sqlite" | "sqlite3" | "csv" | "tsv" | "parquet") => Lens::Database,
         _ => Lens::Grid,
-    };
+    }
+}
+
+fn pick_lens(files: &[&FileEntry]) -> Lens {
+    let dominant = top_extensions(files, 1).into_iter().next();
+    let by_ext = lens_from_ext(dominant.as_deref());
     if by_ext != Lens::Grid {
         return by_ext;
     }
diff --git a/02_ruway/chasqui/chasqui-core/src/db.rs b/02_ruway/chasqui/chasqui-core/src/db.rs
index 6674015..941965e 100644
--- a/02_ruway/chasqui/chasqui-core/src/db.rs
+++ b/02_ruway/chasqui/chasqui-core/src/db.rs
@@ -146,6 +146,19 @@ impl MonadDb {
         self.monads.get(&id)
     }
 
+    /// Elimina una Mónada del store (cache + persistencia). Devuelve el
+    /// manifiesto removido, o `None` si no existía. No toca a quién la
+    /// referenciaba como sub-Mónada — eso es responsabilidad de la capa
+    /// de edición (`crate::edit`), que mantiene la coherencia del grafo.
+    pub fn remove_monad(&mut self, id: MonadId) -> Option<MonadManifest> {
+        if let Some(db) = &self.persistence {
+            if let Ok(tree) = db.open_tree(TREE_MONADS) {
+                let _ = tree.remove(id.to_string().as_bytes());
+            }
+        }
+        self.monads.remove(&id)
+    }
+
     pub fn monads(&self) -> impl Iterator<Item = &MonadManifest> + '_ {
         self.monads.values()
     }
diff --git a/02_ruway/chasqui/chasqui-core/src/edit.rs b/02_ruway/chasqui/chasqui-core/src/edit.rs
new file mode 100644
index 0000000..f4d8d48
--- /dev/null
+++ b/02_ruway/chasqui/chasqui-core/src/edit.rs
@@ -0,0 +1,442 @@
+//! Edición del **grafo de Mónadas**: las mutaciones que convierten a
+//! nahual en un *file manager* (no un mero visor).
+//!
+//! "Para manejar mis datos los voy a querer modificar, y submonadizar": el
+//! usuario edita la **organización** (el grafo), no necesariamente los
+//! bytes de los archivos. Estas operaciones reorganizan Mónadas y su
+//! pertenencia sin mover un solo archivo en disco — el archivo físico es
+//! agnóstico a en cuántas Mónadas vive.
+//!
+//! Todas las mutaciones siguen el patrón **clonar→mutar→reinsertar** sobre
+//! [`MonadDb`], para que la escritura pase por `insert_monad` y herede el
+//! write-through a sled cuando el store es persistente.
+//!
+//! Invariantes que esta capa mantiene:
+//! - El grafo de contención es un **DAG**: [`add_submonad`] /
+//!   [`submonadize`] rechazan cualquier arista que cierre un ciclo.
+//! - Al cambiar los miembros de una Mónada, su `centroid` se recalcula
+//!   (vía [`crate::embed`]) para que la atracción y las queries `Near`
+//!   sigan siendo significativas.
+//! - [`delete_monad`] y [`merge`] dejan el grafo coherente: ningún padre
+//!   queda apuntando a una sub-Mónada que ya no existe.
+
+use chasqui_card::{FileId, Lens, MonadId, MonadManifest, MonadQuery};
+use thiserror::Error;
+
+use crate::db::MonadDb;
+use crate::embed;
+
+/// Error de una operación de edición del grafo.
+#[derive(Debug, Error, PartialEq, Eq)]
+pub enum EditError {
+    #[error("Mónada inexistente: {0}")]
+    NotFound(MonadId),
+    /// Agregar `child` bajo `parent` cerraría un ciclo, porque `parent` ya
+    /// es alcanzable desde `child` (o son la misma).
+    #[error("ciclo: {child} ya contiene (transitivamente) a {parent}")]
+    Cycle { parent: MonadId, child: MonadId },
+}
+
+/// `true` si `target` es alcanzable desde `from` bajando por sub-Mónadas
+/// (incluye el caso `from == target`). Sirve para detectar ciclos antes de
+/// agregar una arista de contención.
+fn reachable(db: &MonadDb, from: MonadId, target: MonadId) -> bool {
+    if from == target {
+        return true;
+    }
+    let mut stack = vec![from];
+    let mut visto = std::collections::BTreeSet::new();
+    while let Some(id) = stack.pop() {
+        if !visto.insert(id) {
+            continue;
+        }
+        if let Some(m) = db.monad(id) {
+            for sub in &m.submonads {
+                if *sub == target {
+                    return true;
+                }
+                stack.push(*sub);
+            }
+        }
+    }
+    false
+}
+
+/// Recalcula el centroide de una Mónada desde los embeddings de sus
+/// miembros actuales. Sin miembros, el centroide queda vacío (y `Near`
+/// deja de matchear, como corresponde).
+fn recompute_centroid(db: &mut MonadDb, id: MonadId) {
+    let Some(m) = db.monad(id) else { return };
+    let mut m = m.clone();
+    let vecs: Vec<Vec<f32>> = m
+        .members
+        .iter()
+        .filter_map(|fid| db.file(*fid))
+        .map(|f| embed::embed(f).to_vec())
+        .collect();
+    m.centroid = embed::centroid(&vecs);
+    m.centroid_model = if m.centroid.is_empty() {
+        None
+    } else {
+        Some(embed::MODEL_ID.to_string())
+    };
+    m.touch();
+    db.insert_monad(m);
+}
+
+/// Crea una Mónada vacía con un label y un lente, y la inserta. Devuelve
+/// su id. Queda sin miembros hasta que se le agreguen (o se le ponga una
+/// query); recién ahí valida como no-vacía.
+pub fn create_monad(db: &mut MonadDb, label: impl Into<String>, lens: Lens) -> MonadId {
+    let mut m = MonadManifest::new(label);
+    m.dominant_lens = lens;
+    m.touch();
+    let id = m.id;
+    db.insert_monad(m);
+    id
+}
+
+/// Renombra una Mónada.
+pub fn rename(db: &mut MonadDb, id: MonadId, label: impl Into<String>) -> Result<(), EditError> {
+    let mut m = db.monad(id).ok_or(EditError::NotFound(id))?.clone();
+    m.label = label.into();
+    m.touch();
+    db.insert_monad(m);
+    Ok(())
+}
+
+/// Cambia el lente dominante (la "vista" que el shell elige para la
+/// Mónada).
+pub fn set_lens(db: &mut MonadDb, id: MonadId, lens: Lens) -> Result<(), EditError> {
+    let mut m = db.monad(id).ok_or(EditError::NotFound(id))?.clone();
+    m.dominant_lens = lens;
+    m.touch();
+    db.insert_monad(m);
+    Ok(())
+}
+
+/// Fija (o quita, con `None`) el cuerpo intensional. Convertir una Mónada
+/// extensional en intensional no borra sus `members` curados: la query
+/// *suma* (ver `resolve::effective_members`).
+pub fn set_query(
+    db: &mut MonadDb,
+    id: MonadId,
+    query: Option<MonadQuery>,
+) -> Result<(), EditError> {
+    let mut m = db.monad(id).ok_or(EditError::NotFound(id))?.clone();
+    m.query = query;
+    m.touch();
+    db.insert_monad(m);
+    Ok(())
+}
+
+/// Agrega un archivo como miembro curado de la Mónada y recalcula el
+/// centroide.
+pub fn add_member(db: &mut MonadDb, id: MonadId, file: FileId) -> Result<(), EditError> {
+    let mut m = db.monad(id).ok_or(EditError::NotFound(id))?.clone();
+    m.members.insert(file);
+    m.touch();
+    db.insert_monad(m);
+    recompute_centroid(db, id);
+    Ok(())
+}
+
+/// Quita un archivo de los miembros curados (y de los pines). Recalcula el
+/// centroide.
+pub fn remove_member(db: &mut MonadDb, id: MonadId, file: FileId) -> Result<(), EditError> {
+    let mut m = db.monad(id).ok_or(EditError::NotFound(id))?.clone();
+    m.members.remove(&file);
+    m.pins.remove(&file);
+    m.touch();
+    db.insert_monad(m);
+    recompute_centroid(db, id);
+    Ok(())
+}
+
+/// Agrega `child` como sub-Mónada de `parent`. Rechaza la arista si
+/// cerraría un ciclo (si `parent` ya es alcanzable desde `child`).
+pub fn add_submonad(db: &mut MonadDb, parent: MonadId, child: MonadId) -> Result<(), EditError> {
+    if db.monad(parent).is_none() {
+        return Err(EditError::NotFound(parent));
+    }
+    if db.monad(child).is_none() {
+        return Err(EditError::NotFound(child));
+    }
+    if reachable(db, child, parent) {
+        return Err(EditError::Cycle { parent, child });
+    }
+    let mut m = db.monad(parent).unwrap().clone();
+    m.submonads.insert(child);
+    m.touch();
+    db.insert_monad(m);
+    Ok(())
+}
+
+/// Quita la arista de contención `parent → child`. No borra la sub-Mónada,
+/// sólo la desvincula de ese padre (puede seguir colgando de otros).
+pub fn remove_submonad(
+    db: &mut MonadDb,
+    parent: MonadId,
+    child: MonadId,
+) -> Result<(), EditError> {
+    let mut m = db.monad(parent).ok_or(EditError::NotFound(parent))?.clone();
+    m.submonads.remove(&child);
+    m.touch();
+    db.insert_monad(m);
+    Ok(())
+}
+
+/// **Submonadizar**: crea una nueva Mónada hija de `parent` y le *traslada*
+/// la selección de archivos y sub-Mónadas indicada (la saca de `parent` y
+/// la mete en la hija). Es la operación canónica de "tomar un puñado de
+/// cosas dentro de una Mónada y agruparlas en una sub-Mónada propia".
+///
+/// La hija hereda el lente de `parent` por defecto; su centroide se calcula
+/// de los archivos trasladados. Devuelve el id de la hija.
+pub fn submonadize(
+    db: &mut MonadDb,
+    parent: MonadId,
+    label: impl Into<String>,
+    members: &[FileId],
+    submonads: &[MonadId],
+) -> Result<MonadId, EditError> {
+    let padre = db.monad(parent).ok_or(EditError::NotFound(parent))?.clone();
+
+    // La hija nace con la selección y el lente del padre.
+    let mut hija = MonadManifest::new(label);
+    hija.dominant_lens = padre.dominant_lens;
+    hija.lineage = Some(parent);
+    for f in members {
+        hija.members.insert(*f);
+    }
+    for s in submonads {
+        hija.submonads.insert(*s);
+    }
+    hija.touch();
+    let hija_id = hija.id;
+    db.insert_monad(hija);
+    recompute_centroid(db, hija_id);
+
+    // El padre suelta la selección y adopta a la hija.
+    let mut padre = padre;
+    for f in members {
+        padre.members.remove(f);
+        padre.pins.remove(f);
+    }
+    for s in submonads {
+        padre.submonads.remove(s);
+    }
+    padre.submonads.insert(hija_id);
+    padre.touch();
+    db.insert_monad(padre);
+    recompute_centroid(db, parent);
+
+    Ok(hija_id)
+}
+
+/// **Fusionar** `from` en `into`: traslada todos los miembros y
+/// sub-Mónadas de `from` a `into`, repunta a todo padre que apuntaba a
+/// `from` para que ahora apunte a `into`, y borra `from`. El grafo queda
+/// sin referencias colgadas.
+pub fn merge(db: &mut MonadDb, into: MonadId, from: MonadId) -> Result<(), EditError> {
+    if into == from {
+        return Ok(());
+    }
+    let origen = db.monad(from).ok_or(EditError::NotFound(from))?.clone();
+    let mut destino = db.monad(into).ok_or(EditError::NotFound(into))?.clone();
+
+    for f in &origen.members {
+        destino.members.insert(*f);
+    }
+    for p in &origen.pins {
+        destino.pins.insert(*p);
+    }
+    for s in &origen.submonads {
+        if *s != into {
+            destino.submonads.insert(*s);
+        }
+    }
+    destino.submonads.remove(&from);
+    destino.touch();
+    db.insert_monad(destino);
+
+    // Repuntar padres de `from` → `into`.
+    let padres: Vec<MonadId> = db
+        .monads()
+        .filter(|m| m.submonads.contains(&from))
+        .map(|m| m.id)
+        .collect();
+    for pid in padres {
+        if pid == into {
+            continue;
+        }
+        let mut p = db.monad(pid).unwrap().clone();
+        p.submonads.remove(&from);
+        if pid != into {
+            p.submonads.insert(into);
+        }
+        p.touch();
+        db.insert_monad(p);
+    }
+
+    db.remove_monad(from);
+    recompute_centroid(db, into);
+    Ok(())
+}
+
+/// Borra una Mónada y la desvincula de todo padre que la contenía. Los
+/// archivos y sub-Mónadas que contenía NO se borran — sólo se disuelve el
+/// agrupamiento.
+pub fn delete_monad(db: &mut MonadDb, id: MonadId) {
+    let padres: Vec<MonadId> = db
+        .monads()
+        .filter(|m| m.submonads.contains(&id))
+        .map(|m| m.id)
+        .collect();
+    for pid in padres {
+        if let Some(p) = db.monad(pid) {
+            let mut p = p.clone();
+            p.submonads.remove(&id);
+            p.touch();
+            db.insert_monad(p);
+        }
+    }
+    db.remove_monad(id);
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::resolve;
+    use chasqui_card::FileEntry;
+    use std::path::PathBuf;
+    use ulid::Ulid;
+
+    fn mk(path: &str, ext: &str) -> FileEntry {
+        FileEntry {
+            id: FileId::from(Ulid::new()),
+            path: PathBuf::from(path),
+            content_hash: None,
+            size: 100,
+            mtime_ms: 1_700_000_000_000,
+            extension: Some(ext.into()),
+        }
+    }
+
+    fn db_con_archivos(n: usize) -> (MonadDb, Vec<FileId>) {
+        let mut db = MonadDb::new();
+        let mut ids = Vec::new();
+        for i in 0..n {
+            let f = mk(&format!("/proj/src/f{i}.rs"), "rs");
+            ids.push(f.id);
+            db.insert_file(f);
+        }
+        (db, ids)
+    }
+
+    #[test]
+    fn crear_agregar_y_recalcular_centroide() {
+        let (mut db, files) = db_con_archivos(2);
+        let m = create_monad(&mut db, "src", Lens::Code);
+        assert!(db.monad(m).unwrap().centroid.is_empty());
+
+        add_member(&mut db, m, files[0]).unwrap();
+        add_member(&mut db, m, files[1]).unwrap();
+        let mon = db.monad(m).unwrap();
+        assert_eq!(mon.cardinality, 2);
+        assert!(!mon.centroid.is_empty(), "el centroide se recalcula al agregar");
+        assert_eq!(mon.centroid_model.as_deref(), Some(embed::MODEL_ID));
+
+        remove_member(&mut db, m, files[0]).unwrap();
+        assert_eq!(db.monad(m).unwrap().cardinality, 1);
+    }
+
+    #[test]
+    fn add_submonad_rechaza_ciclo() {
+        let mut db = MonadDb::new();
+        let a = create_monad(&mut db, "A", Lens::Grid);
+        let b = create_monad(&mut db, "B", Lens::Grid);
+        add_submonad(&mut db, a, b).unwrap(); // A ⊃ B
+        // B ⊃ A cerraría el ciclo.
+        assert_eq!(add_submonad(&mut db, b, a), Err(EditError::Cycle { parent: b, child: a }));
+        // A ⊃ A también.
+        assert_eq!(add_submonad(&mut db, a, a), Err(EditError::Cycle { parent: a, child: a }));
+    }
+
+    #[test]
+    fn submonadizar_traslada_la_seleccion() {
+        let (mut db, files) = db_con_archivos(3);
+        let padre = create_monad(&mut db, "todo", Lens::Code);
+        for f in &files {
+            add_member(&mut db, padre, *f).unwrap();
+        }
+        assert_eq!(db.monad(padre).unwrap().cardinality, 3);
+
+        // Submonadizar dos de los tres archivos a una hija "sub".
+        let hija = submonadize(&mut db, padre, "sub", &files[0..2], &[]).unwrap();
+
+        // El padre queda con 1 archivo + la hija como sub-Mónada.
+        let p = db.monad(padre).unwrap();
+        assert_eq!(p.cardinality, 1, "el padre soltó 2 archivos");
+        assert!(p.submonads.contains(&hija));
+
+        // La hija tiene los 2 archivos y lineage al padre.
+        let h = db.monad(hija).unwrap();
+        assert_eq!(h.cardinality, 2);
+        assert_eq!(h.lineage, Some(padre));
+
+        // Los 3 archivos siguen alcanzables transitivamente desde el padre.
+        assert_eq!(resolve::transitive_files(&db, padre).len(), 3);
+    }
+
+    #[test]
+    fn fusionar_repunta_padres_y_borra_origen() {
+        let (mut db, files) = db_con_archivos(2);
+        let raiz = create_monad(&mut db, "raíz", Lens::Grid);
+        let a = create_monad(&mut db, "A", Lens::Code);
+        let b = create_monad(&mut db, "B", Lens::Code);
+        add_member(&mut db, a, files[0]).unwrap();
+        add_member(&mut db, b, files[1]).unwrap();
+        add_submonad(&mut db, raiz, a).unwrap();
+        add_submonad(&mut db, raiz, b).unwrap();
+
+        // Fusionar B en A: A se queda con ambos archivos, B desaparece, y
+        // raíz deja de apuntar a B.
+        merge(&mut db, a, b).unwrap();
+        assert!(db.monad(b).is_none(), "B se borró");
+        assert_eq!(db.monad(a).unwrap().cardinality, 2);
+        let raiz = db.monad(raiz).unwrap();
+        assert!(raiz.submonads.contains(&a));
+        assert!(!raiz.submonads.contains(&b), "raíz no apunta a una Mónada borrada");
+    }
+
+    #[test]
+    fn borrar_desvincula_de_padres() {
+        let mut db = MonadDb::new();
+        let raiz = create_monad(&mut db, "raíz", Lens::Grid);
+        let hija = create_monad(&mut db, "hija", Lens::Grid);
+        add_submonad(&mut db, raiz, hija).unwrap();
+
+        delete_monad(&mut db, hija);
+        assert!(db.monad(hija).is_none());
+        assert!(!db.monad(raiz).unwrap().submonads.contains(&hija), "sin referencia colgada");
+    }
+
+    #[test]
+    fn set_query_convierte_a_intensional_sin_perder_curados() {
+        let (mut db, files) = db_con_archivos(1);
+        let png = mk("/a/foto.png", "png");
+        let png_id = png.id;
+        db.insert_file(png);
+
+        let m = create_monad(&mut db, "mix", Lens::Grid);
+        add_member(&mut db, m, files[0]).unwrap(); // un .rs curado
+        set_query(&mut db, m, Some(MonadQuery::imagenes())).unwrap();
+
+        // effective_members = el .rs curado + el .png por la query.
+        let eff = resolve::effective_members(&db, m);
+        assert!(eff.contains(&files[0]));
+        assert!(eff.contains(&png_id));
+        assert_eq!(eff.len(), 2);
+    }
+}
diff --git a/02_ruway/chasqui/chasqui-core/src/engine_socket.rs b/02_ruway/chasqui/chasqui-core/src/engine_socket.rs
index 5aa38fb..8369f68 100644
--- a/02_ruway/chasqui/chasqui-core/src/engine_socket.rs
+++ b/02_ruway/chasqui/chasqui-core/src/engine_socket.rs
@@ -10,7 +10,7 @@
 //! que `chasqui-nous` (mock/real ↔ chasqui-core), reutilizado.
 //!
 //! Threading: un thread dedicado, blocking I/O. No vale la pena traer
-//! tokio acá — la frecuencia esperada es muy baja (UI poll cada 2s)
+//! tokio aquí — la frecuencia esperada es muy baja (UI poll cada 2s)
 //! y el handler es trivial (lock db → snapshot → write).
 
 use std::io::{BufRead, BufReader, Write};
diff --git a/02_ruway/chasqui/chasqui-core/src/grafo.rs b/02_ruway/chasqui/chasqui-core/src/grafo.rs
new file mode 100644
index 0000000..4fc908a
--- /dev/null
+++ b/02_ruway/chasqui/chasqui-core/src/grafo.rs
@@ -0,0 +1,191 @@
+//! Adaptador: el **grafo de contención de Mónadas** como [`GrafoNavegable`]
+//! (PLAN-GRAFO). Es el DAG que nahual muestra en su file-manager: una Mónada
+//! («Fotos») contiene sub-Mónadas («Viaje») y archivos; una misma sub-Mónada
+//! puede colgar de varios padres (multi-pertenencia). El eje es `submonads`
+//! (contención), no `lineage` (derivación histórica split/merge).
+//!
+//! Verbos del dominio (ver [`nahual_source_core::MonadGraphMut`]): **Saltar**
+//! (abrir la Mónada) y, sobre una selección, **Cercenar** (disolver el
+//! agrupamiento — `delete_monad`; no borra los archivos). No hay *Restablecer*
+//! ni *Conciliar* por nodo: el merge de Mónadas (`merge_monads`) es entre dos y
+//! no encaja en el menú de un solo nodo.
+//!
+//! `MonadId` es un `Ulid` (128 bits) y no cabe en `NodoId = u64`, así que el
+//! adaptador mantiene la tabla `ids` para traducir de vuelta.
+
+use grafo_nav::{GrafoNavegable, Nodo, NodoId, Rol, Verbo};
+
+use chasqui_card::MonadId;
+
+use crate::db::MonadDb;
+
+/// Un **snapshot owned** del DAG de contención de Mónadas de una [`MonadDb`]. No
+/// retiene el borrow de la DB (copia label/entropy/aristas), así puede cruzar la
+/// frontera `dyn Source` de nahual —construido dentro de `with_db(...)` y
+/// devuelto— sin pelear con el `RwLock`.
+pub struct GrafoMonadas {
+    /// `(id, label, entropy)` por nodo, en orden de la DB (índice = `NodoId`).
+    nodos: Vec<(MonadId, String, f32)>,
+    /// Aristas `(contenedora, contenida)` por `MonadId`.
+    aristas: Vec<(MonadId, MonadId)>,
+    /// Mónada en foco (se pinta [`Rol::Activo`]).
+    activo: Option<MonadId>,
+}
+
+impl GrafoMonadas {
+    /// Toma el snapshot desde la DB: nodos + aristas de contención (`submonads`
+    /// hacia Mónadas presentes).
+    pub fn from_db(db: &MonadDb) -> Self {
+        let nodos: Vec<(MonadId, String, f32)> = db
+            .monads()
+            .map(|m| (m.id, m.label.clone(), m.entropy))
+            .collect();
+        let presentes = alloc_set(&nodos);
+        let mut aristas = Vec::new();
+        for m in db.monads() {
+            for sub in &m.submonads {
+                if presentes.contains(sub) {
+                    aristas.push((m.id, *sub));
+                }
+            }
+        }
+        Self { nodos, aristas, activo: None }
+    }
+
+    /// Marca la Mónada en foco (se pinta [`Rol::Activo`]).
+    pub fn con_activo(mut self, activo: Option<MonadId>) -> Self {
+        self.activo = activo;
+        self
+    }
+
+    /// Traduce el id compacto del visor al `MonadId` real (para abrir/seleccionar
+    /// la Mónada al clickear un nodo).
+    pub fn monad_id_de(&self, id: NodoId) -> Option<MonadId> {
+        self.nodos.get(id as usize).map(|(m, _, _)| *m)
+    }
+
+    /// Traduce un `MonadId` al id compacto del visor.
+    pub fn id_de(&self, m: &MonadId) -> Option<NodoId> {
+        self.nodos.iter().position(|(x, _, _)| x == m).map(|i| i as NodoId)
+    }
+}
+
+impl GrafoNavegable for GrafoMonadas {
+    fn nodos(&self) -> Vec<Nodo> {
+        self.nodos
+            .iter()
+            .enumerate()
+            .map(|(i, (mid, label, entropy))| {
+                let rol = if self.activo == Some(*mid) {
+                    Rol::Activo
+                } else if *entropy > 0.66 {
+                    // Mónada dispersa: el manifiesto sugiere bifurcarla.
+                    Rol::Stale
+                } else {
+                    Rol::Normal
+                };
+                let etiqueta = if label.is_empty() {
+                    String::from("(sin nombre)")
+                } else {
+                    label.clone()
+                };
+                Nodo::new(i as NodoId, etiqueta, rol)
+            })
+            .collect()
+    }
+
+    fn aristas(&self) -> Vec<(NodoId, NodoId)> {
+        // Contención: `(contenedora → contenida)` = `(ancestro → descendiente)`.
+        self.aristas
+            .iter()
+            .filter_map(|(f, t)| Some((self.id_de(f)?, self.id_de(t)?)))
+            .collect()
+    }
+
+    fn acciones(&self, seleccion: Option<NodoId>) -> Vec<Verbo> {
+        let mut v = vec![Verbo::Saltar];
+        if seleccion.is_some() {
+            // Cercenar = disolver el agrupamiento (delete_monad); no toca archivos.
+            v.push(Verbo::Cercenar);
+        }
+        v
+    }
+}
+
+/// Conjunto de los `MonadId` presentes en el snapshot (para descartar aristas a
+/// sub-Mónadas ausentes).
+fn alloc_set(nodos: &[(MonadId, String, f32)]) -> std::collections::BTreeSet<MonadId> {
+    nodos.iter().map(|(id, _, _)| *id).collect()
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use chasqui_card::MonadManifest;
+
+    /// DB en contención: raíz «Fotos» contiene «Viaje» y «Familia»; «Viaje»
+    /// contiene «Cusco». Devuelve (db, id_fotos, id_viaje, id_cusco).
+    fn db_contencion() -> (MonadDb, MonadId, MonadId, MonadId) {
+        let mut cusco = MonadManifest::new("Cusco");
+        let mut viaje = MonadManifest::new("Viaje");
+        let mut familia = MonadManifest::new("Familia");
+        let mut fotos = MonadManifest::new("Fotos");
+        viaje.submonads.insert(cusco.id);
+        fotos.submonads.insert(viaje.id);
+        fotos.submonads.insert(familia.id);
+        // Marcar una dispersa para el test de rol.
+        familia.entropy = 0.9;
+        let (fid, vid, cid) = (fotos.id, viaje.id, cusco.id);
+        let mut db = MonadDb::new();
+        for m in [cusco, viaje, familia, fotos] {
+            db.insert_monad(m);
+        }
+        (db, fid, vid, cid)
+    }
+
+    #[test]
+    fn estructura_de_contencion() {
+        let (db, fotos, viaje, cusco) = db_contencion();
+        let g = GrafoMonadas::from_db(&db);
+        assert_eq!(g.nodos().len(), 4);
+        // Aristas: fotos→viaje, fotos→familia, viaje→cusco = 3.
+        assert_eq!(g.aristas().len(), 3);
+        // «Fotos» es raíz (nadie la contiene); «Cusco»/«Familia» son cabezas.
+        let raices: Vec<MonadId> =
+            g.raices().iter().filter_map(|id| g.monad_id_de(*id)).collect();
+        assert_eq!(raices, vec![fotos]);
+        // El cono descendente de «Fotos» es todo (4).
+        let cono = g.cono_descendente(g.id_de(&fotos).unwrap());
+        assert_eq!(cono.len(), 4);
+        // «Cusco» es una hoja: su cono descendente es sólo él.
+        assert_eq!(g.cono_descendente(g.id_de(&cusco).unwrap()).len(), 1);
+        assert!(g.cabezas().contains(&g.id_de(&cusco).unwrap()));
+        let _ = viaje;
+    }
+
+    #[test]
+    fn rol_activo_y_dispersa() {
+        let (db, fotos, _viaje, _cusco) = db_contencion();
+        let g = GrafoMonadas::from_db(&db).con_activo(Some(fotos));
+        let nodos = g.nodos();
+        let rol = |mid: &MonadId| {
+            let nid = g.id_de(mid).unwrap();
+            nodos.iter().find(|n| n.id == nid).unwrap().rol
+        };
+        assert_eq!(rol(&fotos), Rol::Activo, "el foco gana");
+        // «Familia» tiene entropy 0.9 → Stale (sugerencia de bifurcar).
+        assert!(nodos.iter().any(|n| n.rol == Rol::Stale));
+    }
+
+    #[test]
+    fn verbos_saltar_y_cercenar() {
+        let (db, _f, _v, _c) = db_contencion();
+        let g = GrafoMonadas::from_db(&db);
+        assert_eq!(g.acciones(None), vec![Verbo::Saltar]);
+        let sel = g.acciones(Some(0));
+        assert!(sel.contains(&Verbo::Saltar));
+        assert!(sel.contains(&Verbo::Cercenar));
+        assert!(!sel.contains(&Verbo::Conciliar));
+        assert!(!sel.contains(&Verbo::Restablecer));
+    }
+}
diff --git a/02_ruway/chasqui/chasqui-core/src/lib.rs b/02_ruway/chasqui/chasqui-core/src/lib.rs
index e0ce4f1..247cb51 100644
--- a/02_ruway/chasqui/chasqui-core/src/lib.rs
+++ b/02_ruway/chasqui/chasqui-core/src/lib.rs
@@ -27,8 +27,12 @@
 
 pub mod cluster;
 pub mod db;
+pub mod edit;
 pub mod embed;
 pub mod engine_socket;
+pub mod grafo;
+pub mod resolve;
 pub mod scanner;
 
 pub use chasqui_card::*;
+pub use grafo::GrafoMonadas;
diff --git a/02_ruway/chasqui/chasqui-core/src/resolve.rs b/02_ruway/chasqui/chasqui-core/src/resolve.rs
new file mode 100644
index 0000000..7101683
--- /dev/null
+++ b/02_ruway/chasqui/chasqui-core/src/resolve.rs
@@ -0,0 +1,303 @@
+//! Resolución del **grafo de Mónadas**: contención (sub-Mónadas) +
+//! cuerpo intensional (queries).
+//!
+//! El modelo (`chasqui-card`) ahora describe un DAG: una Mónada contiene
+//! archivos (`members`) y/o otras Mónadas (`submonads`), y puede definir
+//! su membresía por una regla ([`MonadQuery`]) en vez de a mano. Este
+//! módulo es la lógica que *recorre* ese grafo:
+//!
+//! - [`resolve_query`] evalúa un predicado contra un corpus de archivos.
+//! - [`effective_members`] da los archivos directos de una Mónada
+//!   (curados + intensional + pines).
+//! - [`transitive_files`] baja por las sub-Mónadas (guardando ciclos y
+//!   diamantes) y junta todos los archivos alcanzables.
+//! - [`child_monads`] resuelve las sub-Mónadas a sus manifiestos.
+//!
+//! Determinista y sin red. La hoja semántica [`MonadQuery::Near`] usa los
+//! pseudo-embeddings de [`crate::embed`]; el resto es puramente léxico.
+
+use std::collections::BTreeSet;
+
+use chasqui_card::{FileEntry, FileId, MonadId, MonadManifest, MonadQuery};
+
+use crate::cluster::lens_from_ext;
+use crate::db::MonadDb;
+use crate::embed;
+
+/// Evalúa una [`MonadQuery`] contra un corpus y devuelve los IDs de los
+/// archivos que la satisfacen.
+///
+/// `centroid` es el centroide de la Mónada dueña de la query — lo que la
+/// hoja [`MonadQuery::Near`] usa como referencia de "cerca de esto". Si
+/// la query no usa `Near`, `centroid` se ignora (pasa `&[]`).
+pub fn resolve_query<'a, I>(query: &MonadQuery, files: I, centroid: &[f32]) -> BTreeSet<FileId>
+where
+    I: IntoIterator<Item = &'a FileEntry>,
+{
+    files
+        .into_iter()
+        .filter(|f| matches(query, f, centroid))
+        .map(|f| f.id)
+        .collect()
+}
+
+/// Decide si un archivo satisface la query. Recursivo sobre el álgebra.
+fn matches(query: &MonadQuery, file: &FileEntry, centroid: &[f32]) -> bool {
+    match query {
+        MonadQuery::Extension { exts } => file
+            .extension
+            .as_deref()
+            .is_some_and(|e| exts.contains(e)),
+        // Lente determinista por extensión. Refinamiento futuro: discernir
+        // por contenido (shuma-discern) cuando la extensión miente; hoy nos
+        // quedamos en el mapeo barato y sin disco, coherente con el lente
+        // que el clustering asigna.
+        MonadQuery::Lens { lens } => lens_from_ext(file.extension.as_deref()) == *lens,
+        MonadQuery::Near { min_similarity } => {
+            if centroid.is_empty() {
+                return false;
+            }
+            // Recalcula el embedding por nodo Near; las queries reales
+            // tienen ≤1, así que no vale la pena cachear todavía.
+            let v = embed::embed(file);
+            embed::cosine_similarity(&v, centroid) >= *min_similarity
+        }
+        MonadQuery::All { of } => of.iter().all(|sub| matches(sub, file, centroid)),
+        MonadQuery::Any { of } => of.iter().any(|sub| matches(sub, file, centroid)),
+        MonadQuery::Not { inner } => !matches(inner, file, centroid),
+    }
+}
+
+/// Archivos **directos** de una Mónada: la unión de sus miembros curados,
+/// los miembros derivados de su cuerpo intensional (si tiene `query`), y
+/// sus pines. No baja por las sub-Mónadas (para eso, [`transitive_files`]).
+///
+/// Mónada inexistente ⇒ conjunto vacío.
+pub fn effective_members(db: &MonadDb, id: MonadId) -> BTreeSet<FileId> {
+    let Some(m) = db.monad(id) else {
+        return BTreeSet::new();
+    };
+    let mut out: BTreeSet<FileId> = m.members.iter().copied().collect();
+    out.extend(m.pins.iter().copied());
+    if let Some(query) = &m.query {
+        out.extend(resolve_query(query, db.files(), &m.centroid));
+    }
+    out
+}
+
+/// Sub-Mónadas directas de una Mónada, resueltas a sus manifiestos.
+/// Skipea silenciosamente IDs colgados (sub-Mónada borrada).
+pub fn child_monads(db: &MonadDb, id: MonadId) -> Vec<&MonadManifest> {
+    match db.monad(id) {
+        Some(m) => m.submonads.iter().filter_map(|sid| db.monad(*sid)).collect(),
+        None => Vec::new(),
+    }
+}
+
+/// Todos los archivos alcanzables desde una Mónada bajando por el DAG de
+/// sub-Mónadas. Cuenta cada archivo una sola vez aunque sea alcanzable por
+/// varios caminos (es un `BTreeSet`), y termina aunque el grafo tenga
+/// ciclos o diamantes gracias al set de visitados.
+pub fn transitive_files(db: &MonadDb, root: MonadId) -> BTreeSet<FileId> {
+    let mut visited: BTreeSet<MonadId> = BTreeSet::new();
+    let mut files: BTreeSet<FileId> = BTreeSet::new();
+    collect_files(db, root, &mut visited, &mut files);
+    files
+}
+
+fn collect_files(
+    db: &MonadDb,
+    id: MonadId,
+    visited: &mut BTreeSet<MonadId>,
+    files: &mut BTreeSet<FileId>,
+) {
+    // Guarda de ciclos/diamantes: cada Mónada se visita una vez.
+    if !visited.insert(id) {
+        return;
+    }
+    let Some(m) = db.monad(id) else {
+        return;
+    };
+    files.extend(effective_members(db, id));
+    for sub in &m.submonads {
+        collect_files(db, *sub, visited, files);
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use chasqui_card::{FileId, Lens};
+    use std::collections::BTreeSet;
+    use std::path::PathBuf;
+    use ulid::Ulid;
+
+    fn mk(path: &str, ext: Option<&str>, size: u64) -> FileEntry {
+        FileEntry {
+            id: FileId::from(Ulid::new()),
+            path: PathBuf::from(path),
+            content_hash: None,
+            size,
+            mtime_ms: 1_700_000_000_000,
+            extension: ext.map(String::from),
+        }
+    }
+
+    fn exts(list: &[&str]) -> MonadQuery {
+        MonadQuery::Extension {
+            exts: list.iter().map(|s| s.to_string()).collect(),
+        }
+    }
+
+    #[test]
+    fn query_extension_selecciona_por_formato() {
+        let corpus = vec![
+            mk("/a/1.png", Some("png"), 100),
+            mk("/a/2.jpg", Some("jpg"), 100),
+            mk("/a/3.rs", Some("rs"), 100),
+        ];
+        let hit = resolve_query(&exts(&["png", "jpg"]), &corpus, &[]);
+        assert_eq!(hit.len(), 2);
+        assert!(hit.contains(&corpus[0].id));
+        assert!(hit.contains(&corpus[1].id));
+        assert!(!hit.contains(&corpus[2].id));
+    }
+
+    #[test]
+    fn query_lens_gallery_capta_imagenes() {
+        let corpus = vec![
+            mk("/a/1.png", Some("png"), 100),
+            mk("/a/2.heic", Some("heic"), 100),
+            mk("/a/3.rs", Some("rs"), 100),
+        ];
+        let hit = resolve_query(&MonadQuery::imagenes(), &corpus, &[]);
+        assert_eq!(hit.len(), 2, "png + heic son Gallery, rs no");
+    }
+
+    #[test]
+    fn query_not_y_all_componen() {
+        let corpus = vec![
+            mk("/a/1.png", Some("png"), 100),
+            mk("/a/2.rs", Some("rs"), 100),
+        ];
+        // "imágenes que NO sean png" → vacío (sólo hay png entre imágenes).
+        let q = MonadQuery::All {
+            of: vec![
+                MonadQuery::imagenes(),
+                MonadQuery::Not { inner: Box::new(exts(&["png"])) },
+            ],
+        };
+        assert!(resolve_query(&q, &corpus, &[]).is_empty());
+    }
+
+    #[test]
+    fn query_near_usa_centroide() {
+        // Centroide de un cluster de .rs en /proj/src.
+        let rust = vec![
+            embed::embed(&mk("/proj/src/a.rs", Some("rs"), 1000)).to_vec(),
+            embed::embed(&mk("/proj/src/b.rs", Some("rs"), 1100)).to_vec(),
+        ];
+        let centroid = embed::centroid(&rust);
+
+        let corpus = vec![
+            mk("/proj/src/c.rs", Some("rs"), 1200),       // cerca
+            mk("/photos/x.jpg", Some("jpg"), 5_000_000),  // lejos
+        ];
+        let hit = resolve_query(&MonadQuery::Near { min_similarity: 0.7 }, &corpus, &centroid);
+        assert!(hit.contains(&corpus[0].id), "el .rs debe caer cerca del centroide");
+        assert!(!hit.contains(&corpus[1].id), "la foto debe quedar lejos");
+    }
+
+    #[test]
+    fn near_sin_centroide_no_matchea() {
+        let corpus = vec![mk("/a/x.rs", Some("rs"), 100)];
+        let hit = resolve_query(&MonadQuery::Near { min_similarity: 0.0 }, &corpus, &[]);
+        assert!(hit.is_empty(), "sin centroide, Near no puede decidir");
+    }
+
+    #[test]
+    fn effective_members_une_curados_y_query() {
+        let mut db = MonadDb::new();
+        let png = mk("/a/1.png", Some("png"), 100);
+        let rs = mk("/a/2.rs", Some("rs"), 100);
+        let png_id = png.id;
+        let rs_id = rs.id;
+        db.insert_file(png.clone());
+        db.insert_file(rs.clone());
+
+        // Mónada intensional "imágenes" + un .rs fijado a mano como miembro.
+        let mut m = MonadManifest::new("mixta");
+        m.query = Some(MonadQuery::imagenes());
+        m.members.insert(rs_id);
+        m.touch();
+        let mid = m.id;
+        db.insert_monad(m);
+
+        let eff = effective_members(&db, mid);
+        assert!(eff.contains(&png_id), "el png entra por la query");
+        assert!(eff.contains(&rs_id), "el rs entra por miembro curado");
+        assert_eq!(eff.len(), 2);
+    }
+
+    #[test]
+    fn transitive_baja_por_submonadas_y_dedup() {
+        let mut db = MonadDb::new();
+        let f1 = mk("/album/1.jpg", Some("jpg"), 100);
+        let f2 = mk("/album/2.jpg", Some("jpg"), 100);
+        let f1_id = f1.id;
+        let f2_id = f2.id;
+        db.insert_file(f1);
+        db.insert_file(f2);
+
+        // Álbum extensional con dos fotos.
+        let mut album = MonadManifest::new("Viaje");
+        album.members.insert(f1_id);
+        album.members.insert(f2_id);
+        album.touch();
+        let album_id = album.id;
+        db.insert_monad(album);
+
+        // "Fotos" contiene al álbum (sub-Mónada) y además una de las fotos
+        // directamente — el mismo archivo alcanzable por dos caminos.
+        let mut fotos = MonadManifest::new("Fotos");
+        fotos.submonads.insert(album_id);
+        fotos.members.insert(f1_id);
+        fotos.touch();
+        let fotos_id = fotos.id;
+        db.insert_monad(fotos);
+
+        let all = transitive_files(&db, fotos_id);
+        assert_eq!(all.len(), 2, "f1 contado una sola vez pese a dos caminos");
+        assert!(all.contains(&f1_id) && all.contains(&f2_id));
+
+        let kids = child_monads(&db, fotos_id);
+        assert_eq!(kids.len(), 1);
+        assert_eq!(kids[0].label, "Viaje");
+    }
+
+    #[test]
+    fn transitive_termina_con_ciclo() {
+        let mut db = MonadDb::new();
+        let f = mk("/x/a.rs", Some("rs"), 100);
+        let f_id = f.id;
+        db.insert_file(f);
+
+        let mut a = MonadManifest::new("A");
+        a.members.insert(f_id);
+        let a_id = a.id;
+        let mut b = MonadManifest::new("B");
+        b.members.insert(f_id);
+        let b_id = b.id;
+        // Ciclo: A contiene B, B contiene A.
+        a.submonads.insert(b_id);
+        b.submonads.insert(a_id);
+        a.touch();
+        b.touch();
+        db.insert_monad(a);
+        db.insert_monad(b);
+
+        // No debe colgarse; junta el único archivo.
+        let all = transitive_files(&db, a_id);
+        assert_eq!(all, BTreeSet::from([f_id]));
+    }
+}
diff --git a/02_ruway/chasqui/chasqui-explorer-llimphi/Cargo.toml b/02_ruway/chasqui/chasqui-explorer-llimphi/Cargo.toml
index 2f0ecd1..8bd450c 100644
--- a/02_ruway/chasqui/chasqui-explorer-llimphi/Cargo.toml
+++ b/02_ruway/chasqui/chasqui-explorer-llimphi/Cargo.toml
@@ -8,7 +8,6 @@ publish.workspace = true
 description = "Explorador Llimphi de Mónadas: panel que descubre al daemon chasqui vía broker brahman y consulta sus Mónadas dinámicamente. Reemplazo del `chasqui-explorer` GPUI; el discovery (card-sidecar) y el cliente de query no cambian, sólo el frontend."
 
 [dependencies]
-card-core = { workspace = true }
 card-sidecar = { path = "../card-sidecar" }
 chasqui-card = { path = "../chasqui-card" }
 llimphi-ui = { workspace = true }
@@ -18,6 +17,10 @@ llimphi-widget-banner = { workspace = true }
 llimphi-widget-card = { workspace = true }
 llimphi-widget-menubar = { workspace = true }
 llimphi-widget-context-menu = { workspace = true }
+llimphi-widget-skeleton = { workspace = true }
+llimphi-widget-empty = { workspace = true }
+llimphi-widget-toast = { workspace = true }
+llimphi-icons = { workspace = true }
 llimphi-motion = { workspace = true }
 app-bus = { workspace = true }
 rimay-localize = { workspace = true }
diff --git a/02_ruway/chasqui/chasqui-explorer-llimphi/src/main.rs b/02_ruway/chasqui/chasqui-explorer-llimphi/src/main.rs
index 155a4e8..e46aee0 100644
--- a/02_ruway/chasqui/chasqui-explorer-llimphi/src/main.rs
+++ b/02_ruway/chasqui/chasqui-explorer-llimphi/src/main.rs
@@ -20,8 +20,9 @@
 
 #![forbid(unsafe_code)]
 
+use std::hash::{Hash, Hasher};
 use std::path::PathBuf;
-use std::time::Duration;
+use std::time::{Duration, Instant};
 
 use card_sidecar::{await_provider_blocking, build_consumer_card, ConsumerError};
 use chasqui_card::query::client as query_client;
@@ -32,13 +33,18 @@ use llimphi_ui::llimphi_layout::taffy::{
     prelude::{length, percent, AlignItems, Dimension, FlexDirection, Size, Style},
     Rect,
 };
+use llimphi_ui::llimphi_raster::kurbo::Affine;
 use llimphi_ui::llimphi_raster::peniko::Color;
 use llimphi_ui::llimphi_text::Alignment;
 use llimphi_ui::{App, Handle, Key, KeyEvent, KeyState, NamedKey, View};
 use llimphi_motion::{animate, motion, Tween};
+use llimphi_icons::Icon;
 use llimphi_widget_app_header::{app_header, AppHeaderPalette};
 use llimphi_widget_banner::{banner_view, BannerKind};
 use llimphi_widget_card::{card_view, CardOptions, CardPalette};
+use llimphi_widget_empty::{empty_view, EmptyPalette};
+use llimphi_widget_skeleton::{skeleton_view, SkeletonPalette};
+use llimphi_widget_toast::{toast_stack_view, Toast};
 use llimphi_widget_context_menu::{
     context_menu_view, ContextMenuItem, ContextMenuPalette, ContextMenuSpec,
 };
@@ -53,6 +59,18 @@ use std::sync::Arc;
 const REFRESH_INTERVAL: Duration = Duration::from_secs(2);
 const DISCOVERY_TIMEOUT: Duration = Duration::from_secs(3);
 const QUERY_TIMEOUT: Duration = Duration::from_secs(2);
+/// Cuánto vive un toast antes de auto-descartarse.
+const TOAST_TTL: Duration = Duration::from_secs(4);
+/// Cadencia del repaint que anima el shimmer del skeleton mientras carga.
+const SHIMMER_MS: u64 = 50;
+
+/// Hash estable de una cadena → `key` para animaciones implícitas (la misma
+/// Mónada produce siempre la misma key entre rebuilds).
+fn key_of(s: &str) -> u64 {
+    let mut h = std::collections::hash_map::DefaultHasher::new();
+    s.hash(&mut h);
+    h.finish()
+}
 
 struct Model {
     theme: Theme,
@@ -76,6 +94,15 @@ struct Model {
     /// Menú contextual sobre una Mónada: `(idx, x, y)` ancla en ventana.
     /// `None` cerrado.
     context_menu: Option<(usize, f32, f32)>,
+    /// Esperando la primera respuesta del daemon (muestra skeleton en vez de
+    /// hueco vacío). Baja al primer `Refresh`, sea Ok o fallo.
+    loading: bool,
+    /// Toasts vivos (errores de conexión, acciones del usuario).
+    toasts: Vec<Toast>,
+    /// Id incremental para correlacionar toast ↔ Msg de expiración.
+    next_toast: u64,
+    /// Hay una cadena de `Msg::Anim` en vuelo (evita rearmar dos).
+    ticking: bool,
 }
 
 #[derive(Clone)]
@@ -105,6 +132,11 @@ enum Msg {
     /// `(x, y)` de ventana sobre la Mónada seleccionada. Sin selección
     /// es no-op.
     ContextMenuOpen(f32, f32),
+    /// Tick de animación — fuerza repaint para el shimmer del skeleton mientras
+    /// esperamos la primera respuesta. Se auto-rearma sólo si sigue cargando.
+    Anim,
+    /// Un toast cumplió su `duration`: se descarta del stack.
+    ToastExpire(u64),
 }
 
 #[derive(Clone)]
@@ -143,7 +175,7 @@ impl App for Explorer {
         handle.dispatch(Msg::Tick);
         handle.spawn_periodic(REFRESH_INTERVAL, || Msg::Tick);
 
-        Model {
+        let mut m = Model {
             theme: Theme::dark(),
             socket: None,
             snapshot: None,
@@ -154,7 +186,14 @@ impl App for Explorer {
             menu_anim: Tween::idle(1.0),
             selected: None,
             context_menu: None,
-        }
+            loading: true,
+            toasts: Vec::new(),
+            next_toast: 0,
+            ticking: false,
+        };
+        // Shimmer del skeleton mientras llega la primera respuesta.
+        ensure_anim(&mut m, handle);
+        m
     }
 
     fn on_key(model: &Model, event: &KeyEvent) -> Option<Msg> {
@@ -185,33 +224,47 @@ impl App for Explorer {
                 let prior_socket = m.socket.clone();
                 handle.spawn(move || Msg::Refresh(tick(prior_socket)));
             }
-            Msg::Refresh(outcome) => match outcome {
-                TickOutcome::Ok { socket, source, snapshot } => {
-                    m.socket = Some(socket);
-                    m.socket_source = Some(source);
-                    m.snapshot = Some(*snapshot);
-                    m.error = None;
-                    // Si la selección quedó fuera de rango tras el
-                    // refresh, la descartamos.
-                    let count = m.snapshot.as_ref().map(|s| s.monads.len()).unwrap_or(0);
-                    if m.selected.map(|i| i >= count).unwrap_or(false) {
-                        m.selected = None;
-                        m.context_menu = None;
+            Msg::Refresh(outcome) => {
+                // Primera respuesta recibida: apaga el skeleton.
+                m.loading = false;
+                // Toast de error sólo en la *transición* a fallo (None→Some);
+                // si el daemon sigue caído el error persiste y no spammeamos.
+                let tenia_error = m.error.is_some();
+                match outcome {
+                    TickOutcome::Ok { socket, source, snapshot } => {
+                        m.socket = Some(socket);
+                        m.socket_source = Some(source);
+                        m.snapshot = Some(*snapshot);
+                        m.error = None;
+                        // Si la selección quedó fuera de rango tras el
+                        // refresh, la descartamos.
+                        let count = m.snapshot.as_ref().map(|s| s.monads.len()).unwrap_or(0);
+                        if m.selected.map(|i| i >= count).unwrap_or(false) {
+                            m.selected = None;
+                            m.context_menu = None;
+                        }
+                    }
+                    TickOutcome::DiscoveryFailed(msg) => {
+                        m.socket = None;
+                        m.socket_source = None;
+                        m.error = Some(msg);
+                    }
+                    TickOutcome::QueryFailed(msg) => {
+                        // Invalida el socket cacheado: la próxima iteración
+                        // re-descubre.
+                        m.socket = None;
+                        m.socket_source = None;
+                        m.error = Some(msg);
                     }
                 }
-                TickOutcome::DiscoveryFailed(msg) => {
-                    m.socket = None;
-                    m.socket_source = None;
-                    m.error = Some(msg);
-                }
-                TickOutcome::QueryFailed(msg) => {
-                    // Invalida el socket cacheado: la próxima iteración
-                    // re-descubre.
-                    m.socket = None;
-                    m.socket_source = None;
-                    m.error = Some(msg);
+                if !tenia_error {
+                    if let Some(e) = m.error.clone() {
+                        let id = m.next_toast;
+                        m.next_toast += 1;
+                        push_toast(&mut m, handle, Toast::error(id, e, TOAST_TTL));
+                    }
                 }
-            },
+            }
             Msg::MenuOpen(which) => {
                 m.menu_open = which;
                 // Abrir un menú raíz cierra cualquier contextual.
@@ -256,6 +309,11 @@ impl App for Explorer {
                 m.socket = None;
                 m.socket_source = None;
                 m.error = None;
+                m.loading = true;
+                ensure_anim(&mut m, handle);
+                let id = m.next_toast;
+                m.next_toast += 1;
+                push_toast(&mut m, handle, Toast::info(id, "Reconectando…", TOAST_TTL));
                 handle.dispatch(Msg::Tick);
             }
             Msg::SelectMonad(i) => {
@@ -270,7 +328,16 @@ impl App for Explorer {
                     m.context_menu = Some((i, x, y));
                 }
             }
+            Msg::Anim => {
+                // El thread durmió; sólo rearmamos si seguimos cargando (abajo).
+                m.ticking = false;
+            }
+            Msg::ToastExpire(id) => {
+                m.toasts.retain(|t| t.id != id);
+            }
         }
+        // Si quedó una carga en vuelo, mantén el shimmer animado.
+        ensure_anim(&mut m, handle);
         m
     }
 
@@ -321,8 +388,32 @@ impl App for Explorer {
             body_children.push(banner_view::<Msg>(BannerKind::Error, e.clone()));
         }
 
-        if let Some(snap) = &model.snapshot {
+        if model.loading && model.snapshot.is_none() {
+            // Esperando la primera respuesta: cards-placeholder con shimmer en
+            // vez de un hueco vacío. Requiere los ticks de `Msg::Anim`.
+            body_children.extend(skeleton_cards(theme));
+        } else if let Some(snap) = &model.snapshot {
             body_children.push(engine_card(snap, accent_engine, theme, &card_palette));
+            if snap.monads.is_empty() {
+                // Engine vivo pero sin Mónadas todavía: estado vacío con
+                // orientación, no un hueco.
+                body_children.push(
+                    View::new(Style {
+                        size: Size {
+                            width: percent(1.0_f32),
+                            height: length(220.0_f32),
+                        },
+                        flex_grow: 1.0,
+                        ..Default::default()
+                    })
+                    .children(vec![empty_view(
+                        Icon::Search,
+                        "Sin Mónadas",
+                        Some("El engine está vivo pero todavía no expone Mónadas. Aparecerán aquí en cuanto el daemon las indexe."),
+                        &EmptyPalette::from_theme(theme),
+                    )]),
+                );
+            }
             for (i, m) in snap.monads.iter().enumerate() {
                 let selected = model.selected == Some(i);
                 let card = monad_card(m, accent_data, theme, &card_palette);
@@ -336,6 +427,9 @@ impl App for Explorer {
                 } else {
                     card
                 };
+                // Pop-in: cada Mónada entra con un fade la primera vez que
+                // aparece su key (estable por id).
+                let card = card.animated_enter(key_of(&m.id.to_string()), motion::NORMAL);
                 body_children.push(card);
             }
         }
@@ -360,9 +454,13 @@ impl App for Explorer {
             ..Default::default()
         })
         .fill(theme.bg_app)
-        .children(body_children);
+        .children(body_children)
+        // Transición de escena: al pasar entre cargando / error / conectado la
+        // `scene_key` cambia y el cuerpo entra con fade + slide-up en vez de
+        // saltar.
+        .animated_enter_from(scene_key(model), motion::SLOW, Affine::translate((0.0, 24.0)));
 
-        View::new(Style {
+        let root = View::new(Style {
             flex_direction: FlexDirection::Column,
             size: Size {
                 width: percent(1.0_f32),
@@ -374,7 +472,24 @@ impl App for Explorer {
         // Right-click en la raíz (origen 0,0 ⇒ local == ventana) abre el
         // menú contextual sobre la Mónada seleccionada.
         .on_right_click_at(|x, y, _w, _h| Some(Msg::ContextMenuOpen(x, y)))
-        .children(vec![menubar, header, body])
+        .children(vec![menubar, header, body]);
+
+        // Overlay de toasts (bottom-right). Click en uno = descartarlo.
+        let now = Instant::now();
+        let alive: Vec<Toast> = model.toasts.iter().filter(|t| t.is_alive(now)).cloned().collect();
+        if alive.is_empty() {
+            root
+        } else {
+            let viewport = viewport_of(model);
+            View::new(Style {
+                size: Size {
+                    width: percent(1.0_f32),
+                    height: percent(1.0_f32),
+                },
+                ..Default::default()
+            })
+            .children(vec![root, toast_stack_view(&alive, viewport, Msg::ToastExpire)])
+        }
     }
 
     fn view_overlay(model: &Model) -> Option<View<Msg>> {
@@ -419,6 +534,43 @@ impl App for Explorer {
     }
 }
 
+/// Empuja un toast al stack y programa su expiración.
+fn push_toast(m: &mut Model, handle: &Handle<Msg>, toast: Toast) {
+    let id = toast.id;
+    m.toasts.push(toast);
+    handle.spawn(move || {
+        std::thread::sleep(TOAST_TTL);
+        Msg::ToastExpire(id)
+    });
+}
+
+/// Arranca la cadena de ticks de shimmer si seguimos esperando la primera
+/// respuesta y no hay ya una corriendo. La cadena se auto-detiene cuando
+/// `loading` baja (ver `Msg::Anim`), así no queda un repaint ocioso.
+fn ensure_anim(m: &mut Model, handle: &Handle<Msg>) {
+    if m.ticking || !m.loading {
+        return;
+    }
+    m.ticking = true;
+    handle.spawn(move || {
+        std::thread::sleep(Duration::from_millis(SHIMMER_MS));
+        Msg::Anim
+    });
+}
+
+/// `key` estable de la escena del cuerpo. Cambia al pasar entre cargando /
+/// error / conectado → dispara la transición de entrada del cuerpo; estable
+/// mientras la escena no cambia.
+fn scene_key(m: &Model) -> u64 {
+    if m.loading && m.snapshot.is_none() {
+        key_of("cargando")
+    } else if m.error.is_some() && m.snapshot.is_none() {
+        key_of("error")
+    } else {
+        key_of("conectado")
+    }
+}
+
 /// Viewport para clampear overlays: tamaño de ventana del Model si lo
 /// llevara; como el explorer no lo trackea, usamos `initial_size()`.
 fn viewport_of(_model: &Model) -> (f32, f32) {
@@ -704,6 +856,51 @@ fn text_line(text: &str, color: Color, _theme: &Theme) -> View<Msg> {
     .text_aligned(text.to_string(), 11.0, color, Alignment::Start)
 }
 
+/// Cards-placeholder con shimmer mientras llega la primera respuesta del
+/// daemon — el usuario ve la forma de lo que viene, no un hueco. Requiere los
+/// repaints periódicos de `Msg::Anim` mientras `loading`.
+fn skeleton_cards(theme: &Theme) -> Vec<View<Msg>> {
+    let pal = SkeletonPalette::from_theme(theme);
+    // Un bloque lleno y clipeado para que el `skeleton_view` (absolute) pinte
+    // dentro de su caja.
+    let bloque = move |w: Dimension, h: f32| -> View<Msg> {
+        View::new(Style {
+            size: Size { width: w, height: length(h) },
+            flex_shrink: 0.0,
+            ..Default::default()
+        })
+        .radius(4.0_f64)
+        .clip(true)
+        .children(vec![skeleton_view::<Msg>(&pal)])
+    };
+    (0..4)
+        .map(|_| {
+            let titulo = bloque(percent(0.45_f32), 16.0);
+            let l1 = bloque(percent(0.7_f32), 11.0);
+            let l2 = bloque(percent(0.55_f32), 11.0);
+            View::new(Style {
+                flex_direction: FlexDirection::Column,
+                size: Size {
+                    width: percent(1.0_f32),
+                    height: Dimension::auto(),
+                },
+                padding: Rect {
+                    left: length(12.0_f32),
+                    right: length(12.0_f32),
+                    top: length(12.0_f32),
+                    bottom: length(12.0_f32),
+                },
+                gap: Size { width: length(0.0_f32), height: length(8.0_f32) },
+                flex_shrink: 0.0,
+                ..Default::default()
+            })
+            .fill(theme.bg_panel)
+            .radius(8.0_f64)
+            .children(vec![titulo, l1, l2])
+        })
+        .collect()
+}
+
 fn tick(prior_socket: Option<PathBuf>) -> TickOutcome {
     let (socket, source) = match prior_socket {
         Some(p) => (p, "cache"),
diff --git a/02_ruway/chasqui/chasqui-nous-real/src/cache.rs b/02_ruway/chasqui/chasqui-nous-real/src/cache.rs
index cd1cde6..8662e7e 100644
--- a/02_ruway/chasqui/chasqui-nous-real/src/cache.rs
+++ b/02_ruway/chasqui/chasqui-nous-real/src/cache.rs
@@ -70,7 +70,7 @@ impl EmbedCache {
     }
 
     /// Almacena. Errores se loggean pero no propagan — cache miss es
-    /// recuperable, no querés tirar el embed válido por fallo de I/O
+    /// recuperable, no quieres tirar el embed válido por fallo de I/O
     /// de cache.
     pub fn put(&self, file_sha: &[u8; 32], model_id: &str, embedding: &[f32]) {
         let key = build_key(file_sha, model_id);
@@ -139,7 +139,7 @@ mod tests {
     use super::*;
 
     fn sha(s: &[u8]) -> [u8; 32] {
-        arje_cas::sha256_of(s)
+        arje_cas::blake3_of(s)
     }
 
     #[test]
diff --git a/02_ruway/chasqui/chasqui-nous-real/src/embeddings.rs b/02_ruway/chasqui/chasqui-nous-real/src/embeddings.rs
index 28b0078..775c6fb 100644
--- a/02_ruway/chasqui/chasqui-nous-real/src/embeddings.rs
+++ b/02_ruway/chasqui/chasqui-nous-real/src/embeddings.rs
@@ -135,7 +135,7 @@ fn handle_file(
     // bajo la semántica del proveedor (el modelo nunca vio los bytes
     // adicionales). Si la cabeza cambia, el hash cambia y caemos a
     // re-embed naturalmente.
-    let file_sha = arje_cas::sha256_of(&buf);
+    let file_sha = arje_cas::blake3_of(&buf);
 
     if let Some(cache) = cache {
         if let Some(cached) = cache.get(&file_sha, model_id) {
diff --git a/02_ruway/chasqui/chasqui-nous-real/src/main.rs b/02_ruway/chasqui/chasqui-nous-real/src/main.rs
index 5ba8570..6813a2a 100644
--- a/02_ruway/chasqui/chasqui-nous-real/src/main.rs
+++ b/02_ruway/chasqui/chasqui-nous-real/src/main.rs
@@ -63,7 +63,7 @@ async fn main() -> std::io::Result<()> {
 
     #[cfg(not(feature = "embeddings"))]
     info!(
-        "chasqui-nous-real corriendo en modo STUB (compilá con \
+        "chasqui-nous-real corriendo en modo STUB (compila con \
         --features embeddings para activar el modelo)"
     );
 
diff --git a/02_ruway/chasqui/chasqui-nous/src/lib.rs b/02_ruway/chasqui/chasqui-nous/src/lib.rs
index d85e37d..3c5f725 100644
--- a/02_ruway/chasqui/chasqui-nous/src/lib.rs
+++ b/02_ruway/chasqui/chasqui-nous/src/lib.rs
@@ -24,7 +24,7 @@
 //! El consumidor (chasqui-core) y el proveedor (chasqui-nous-mock,
 //! chasqui-nous-real) deben acordar en types EXACTOS. Tener el contrato
 //! en su crate evita que cada lado declare structs paralelos que se
-//! desincronizan. Si bumpeás el wire, bumpeás aquí.
+//! desincronizan. Si bumpeas el wire, bumpeas aquí.
 //!
 //! ## Swap por priority_contexts
 //!
diff --git a/Cargo.lock b/Cargo.lock
index 4adedba..9e3eb73 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -18,6 +18,114 @@ version = "0.1.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
 
+[[package]]
+name = "accesskit"
+version = "0.24.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3b7f7f85a7e5f68090000ed7622545829afd484d210358702ae4cb97dd0c320"
+dependencies = [
+ "uuid",
+]
+
+[[package]]
+name = "accesskit_atspi_common"
+version = "0.19.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "023da0e5097f46df7092d5280b02efb9bbf8d93298daeced42652463e357d636"
+dependencies = [
+ "accesskit",
+ "accesskit_consumer",
+ "atspi-common",
+ "phf",
+ "serde",
+ "zvariant",
+]
+
+[[package]]
+name = "accesskit_consumer"
+version = "0.38.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d10a236f96f87d70732e44520046785431ef01d5bcd6b041317bfadd2f88245"
+dependencies = [
+ "accesskit",
+ "hashbrown 0.16.1",
+]
+
+[[package]]
+name = "accesskit_ios"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "750c4e9f6ce888dfe8a10c0f1b5ceb646a7854fd46579d74919219d1bb314083"
+dependencies = [
+ "accesskit",
+ "accesskit_consumer",
+ "hashbrown 0.16.1",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
+ "objc2-ui-kit",
+]
+
+[[package]]
+name = "accesskit_macos"
+version = "0.26.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce02dc63b43f0c9296af9ac946312a2dc8814427d7a64d2d600971dac55b6076"
+dependencies = [
+ "accesskit",
+ "accesskit_consumer",
+ "hashbrown 0.16.1",
+ "objc2 0.5.2",
+ "objc2-app-kit 0.2.2",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "accesskit_unix"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03e156ed3802e35eefe894ef2671bc6c889303d8a7e110b5e1b48f504b91362f"
+dependencies = [
+ "accesskit",
+ "accesskit_atspi_common",
+ "async-channel",
+ "async-executor",
+ "async-task",
+ "atspi",
+ "futures-lite",
+ "futures-util",
+ "serde",
+ "zbus",
+]
+
+[[package]]
+name = "accesskit_windows"
+version = "0.34.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "106c2b961215864d1c2e703ee63269c25c4e80a577ffb2c1017b9c17dcdf83a1"
+dependencies = [
+ "accesskit",
+ "accesskit_consumer",
+ "hashbrown 0.16.1",
+ "static_assertions",
+ "windows 0.62.2",
+ "windows-core 0.62.2",
+]
+
+[[package]]
+name = "accesskit_winit"
+version = "0.33.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5b41e63a69f36d9f1f41e70464c7e5f72eee485ef26aab19f0b4f86e6c0a84c"
+dependencies = [
+ "accesskit",
+ "accesskit_ios",
+ "accesskit_macos",
+ "accesskit_unix",
+ "accesskit_windows",
+ "raw-window-handle",
+ "winit",
+]
+
 [[package]]
 name = "adler2"
 version = "2.0.1"
@@ -113,16 +221,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd"
 dependencies = [
  "android-properties",
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
  "cc",
  "jni",
  "libc",
  "log",
  "ndk",
  "ndk-context",
- "ndk-sys 0.6.0+11769913",
+ "ndk-sys",
  "num_enum",
- "thiserror 2.0.18",
+ "thiserror 2.0.19",
 ]
 
 [[package]]
@@ -142,14 +250,14 @@ dependencies = [
 
 [[package]]
 name = "anyhow"
-version = "1.0.102"
+version = "1.0.104"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
 
 [[package]]
 name = "app-bus"
 version = "0.1.0"
-source = "git+https://gitea.tawasuyu.net/sergio/gioser.git?tag=v0.1.0#692b40336c1e9cb12ab561764d6943cb65905ab3"
+source = "git+https://git.tawasuyu.net/tawasuyu/tawasuyu.git?tag=v0.2.0#a446bb3a73a0e022158cc0ec7deb65173859644b"
 dependencies = [
  "directories",
  "serde",
@@ -162,6 +270,27 @@ version = "1.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
 
+[[package]]
+name = "arboard"
+version = "3.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf"
+dependencies = [
+ "clipboard-win",
+ "image",
+ "log",
+ "objc2 0.6.4",
+ "objc2-app-kit 0.3.2",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-foundation 0.3.2",
+ "parking_lot 0.12.5",
+ "percent-encoding",
+ "windows-sys 0.60.2",
+ "wl-clipboard-rs",
+ "x11rb",
+]
+
 [[package]]
 name = "arg_enum_proc_macro"
 version = "0.3.4"
@@ -170,16 +299,16 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
 name = "arje-cas"
 version = "0.0.1"
-source = "git+https://gitea.tawasuyu.net/sergio/gioser.git?tag=v0.1.0#692b40336c1e9cb12ab561764d6943cb65905ab3"
+source = "git+https://git.tawasuyu.net/tawasuyu/tawasuyu.git?tag=v0.2.0#a446bb3a73a0e022158cc0ec7deb65173859644b"
 dependencies = [
  "anyhow",
- "sha2",
+ "blake3",
  "tracing",
 ]
 
@@ -191,9 +320,9 @@ checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
 
 [[package]]
 name = "arrayvec"
-version = "0.7.6"
+version = "0.7.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
+checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
 
 [[package]]
 name = "as-raw-xcb-connection"
@@ -231,7 +360,7 @@ dependencies = [
  "nom 7.1.3",
  "num-traits",
  "rusticata-macros",
- "thiserror 2.0.18",
+ "thiserror 2.0.19",
  "time",
 ]
 
@@ -243,7 +372,7 @@ checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
  "synstructure",
 ]
 
@@ -255,7 +384,45 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "async-broadcast"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-channel"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
+dependencies = [
+ "concurrent-queue",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-executor"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
+dependencies = [
+ "async-task",
+ "concurrent-queue",
+ "fastrand",
+ "futures-lite",
+ "pin-project-lite",
+ "slab",
 ]
 
 [[package]]
@@ -276,15 +443,79 @@ dependencies = [
  "windows-sys 0.61.2",
 ]
 
+[[package]]
+name = "async-lock"
+version = "3.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-process"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
+dependencies = [
+ "async-channel",
+ "async-io",
+ "async-lock",
+ "async-signal",
+ "async-task",
+ "blocking",
+ "cfg-if",
+ "event-listener",
+ "futures-lite",
+ "rustix 1.1.4",
+]
+
+[[package]]
+name = "async-recursion"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "async-signal"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485"
+dependencies = [
+ "async-io",
+ "async-lock",
+ "atomic-waker",
+ "cfg-if",
+ "futures-core",
+ "futures-io",
+ "rustix 1.1.4",
+ "signal-hook-registry",
+ "slab",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "async-task"
+version = "4.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
+
 [[package]]
 name = "async-trait"
-version = "0.1.89"
+version = "0.1.91"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
+checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 3.0.3",
 ]
 
 [[package]]
@@ -315,6 +546,43 @@ version = "1.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
 
+[[package]]
+name = "atspi"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c77886257be21c9cd89a4ae7e64860c6f0eefca799bb79127913052bd0eefb3d"
+dependencies = [
+ "atspi-common",
+ "atspi-proxies",
+]
+
+[[package]]
+name = "atspi-common"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20c5617155740c98003016429ad13fe43ce7a77b007479350a9f8bf95a29f63d"
+dependencies = [
+ "enumflags2",
+ "serde",
+ "static_assertions",
+ "zbus",
+ "zbus-lockstep",
+ "zbus-lockstep-macros",
+ "zbus_names",
+ "zvariant",
+]
+
+[[package]]
+name = "atspi-proxies"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2230e48787ed3eb4088996eab66a32ca20c0b67bbd4fd6cdfe79f04f1f04c9fc"
+dependencies = [
+ "atspi-common",
+ "serde",
+ "zbus",
+]
+
 [[package]]
 name = "attohttpc"
 version = "0.30.1"
@@ -348,7 +616,7 @@ dependencies = [
  "num-traits",
  "pastey",
  "rayon",
- "thiserror 2.0.18",
+ "thiserror 2.0.19",
  "v_frame",
  "y4m",
 ]
@@ -392,6 +660,12 @@ dependencies = [
  "match-lookup",
 ]
 
+[[package]]
+name = "base45"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240e56f4d3c453c36faacb695c535a4d5f8c7d23dac175014f32eb0a71012a03"
+
 [[package]]
 name = "base64"
 version = "0.13.1"
@@ -439,12 +713,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
 
 [[package]]
 name = "bitflags"
-version = "2.12.1"
+version = "2.13.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a"
-dependencies = [
- "serde_core",
-]
+checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
 
 [[package]]
 name = "bitstream-io"
@@ -502,6 +773,19 @@ dependencies = [
  "objc2 0.5.2",
 ]
 
+[[package]]
+name = "blocking"
+version = "1.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
+dependencies = [
+ "async-channel",
+ "async-task",
+ "futures-io",
+ "futures-lite",
+ "piper",
+]
+
 [[package]]
 name = "bs58"
 version = "0.5.1"
@@ -525,22 +809,22 @@ checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
 
 [[package]]
 name = "bytemuck"
-version = "1.25.0"
+version = "1.25.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
+checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
 dependencies = [
  "bytemuck_derive",
 ]
 
 [[package]]
 name = "bytemuck_derive"
-version = "1.10.2"
+version = "1.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff"
+checksum = "f65693059b6b9c588b9f62fed1cedbf0a8b805631457ea162d68f0de186f3de5"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
@@ -557,9 +841,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
 
 [[package]]
 name = "bytes"
-version = "1.11.1"
+version = "1.12.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
+checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
 
 [[package]]
 name = "calloop"
@@ -567,7 +851,7 @@ version = "0.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
  "log",
  "polling",
  "rustix 0.38.44",
@@ -597,7 +881,7 @@ dependencies = [
  "serde",
  "serde_json",
  "tempfile",
- "thiserror 2.0.18",
+ "thiserror 2.0.19",
  "tokio",
  "tracing",
  "ulid",
@@ -606,11 +890,12 @@ dependencies = [
 [[package]]
 name = "card-core"
 version = "0.1.0"
-source = "git+https://gitea.tawasuyu.net/sergio/gioser.git?tag=v0.1.0#692b40336c1e9cb12ab561764d6943cb65905ab3"
+source = "git+https://git.tawasuyu.net/tawasuyu/tawasuyu.git?tag=v0.2.0#a446bb3a73a0e022158cc0ec7deb65173859644b"
 dependencies = [
+ "format",
  "serde",
  "serde_json",
- "thiserror 2.0.18",
+ "thiserror 2.0.19",
  "toml",
  "ulid",
 ]
@@ -629,7 +914,7 @@ dependencies = [
  "postcard",
  "serde",
  "tempfile",
- "thiserror 2.0.18",
+ "thiserror 2.0.19",
  "tokio",
  "tokio-util",
  "tracing",
@@ -639,7 +924,7 @@ dependencies = [
 [[package]]
 name = "card-net"
 version = "0.1.0"
-source = "git+https://gitea.tawasuyu.net/sergio/gioser.git?tag=v0.1.0#692b40336c1e9cb12ab561764d6943cb65905ab3"
+source = "git+https://git.tawasuyu.net/tawasuyu/tawasuyu.git?tag=v0.2.0#a446bb3a73a0e022158cc0ec7deb65173859644b"
 dependencies = [
  "blake3",
  "futures",
@@ -647,7 +932,7 @@ dependencies = [
  "libp2p-allow-block-list",
  "libp2p-stream",
  "serde",
- "thiserror 2.0.18",
+ "thiserror 2.0.19",
  "tokio",
 ]
 
@@ -661,7 +946,7 @@ dependencies = [
  "card-wit",
  "chasqui-broker",
  "tempfile",
- "thiserror 2.0.18",
+ "thiserror 2.0.19",
  "tokio",
  "tracing",
  "tracing-subscriber",
@@ -670,11 +955,11 @@ dependencies = [
 [[package]]
 name = "card-wit"
 version = "0.1.0"
-source = "git+https://gitea.tawasuyu.net/sergio/gioser.git?tag=v0.1.0#692b40336c1e9cb12ab561764d6943cb65905ab3"
+source = "git+https://git.tawasuyu.net/tawasuyu/tawasuyu.git?tag=v0.2.0#a446bb3a73a0e022158cc0ec7deb65173859644b"
 dependencies = [
  "card-core",
- "thiserror 2.0.18",
- "wit-parser 0.230.0",
+ "thiserror 2.0.19",
+ "wit-parser",
 ]
 
 [[package]]
@@ -688,9 +973,9 @@ dependencies = [
 
 [[package]]
 name = "cc"
-version = "1.2.63"
+version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f"
+checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9"
 dependencies = [
  "find-msvc-tools",
  "jobserver",
@@ -706,9 +991,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
 
 [[package]]
 name = "cfg_aliases"
-version = "0.2.1"
+version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
 
 [[package]]
 name = "chacha20"
@@ -721,6 +1006,17 @@ dependencies = [
  "cpufeatures 0.2.17",
 ]
 
+[[package]]
+name = "chacha20"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.3.0",
+ "rand_core 0.10.1",
+]
+
 [[package]]
 name = "chacha20poly1305"
 version = "0.10.1"
@@ -728,7 +1024,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
 dependencies = [
  "aead",
- "chacha20",
+ "chacha20 0.9.1",
  "cipher",
  "poly1305",
  "zeroize",
@@ -740,7 +1036,6 @@ version = "0.1.0"
 dependencies = [
  "card-core",
  "serde",
- "thiserror 2.0.18",
  "ulid",
 ]
 
@@ -753,14 +1048,21 @@ dependencies = [
  "card-handshake",
  "card-sidecar",
  "chasqui-broker",
+ "llimphi-icons",
  "llimphi-motion",
  "llimphi-theme",
  "llimphi-ui",
  "llimphi-widget-app-header",
  "llimphi-widget-banner",
  "llimphi-widget-context-menu",
+ "llimphi-widget-empty",
  "llimphi-widget-menubar",
+ "llimphi-widget-skeleton",
  "llimphi-widget-stat-card",
+ "llimphi-widget-toast",
+ "png 0.18.1",
+ "pollster",
+ "rimay-localize",
  "ulid",
 ]
 
@@ -771,7 +1073,7 @@ dependencies = [
  "card-core",
  "serde",
  "serde_json",
- "thiserror 2.0.18",
+ "thiserror 2.0.19",
  "ulid",
 ]
 
@@ -781,18 +1083,16 @@ version = "0.1.0"
 dependencies = [
  "blake3",
  "card-core",
- "card-handshake",
  "card-sidecar",
  "chasqui-card",
  "chasqui-nous",
+ "grafo-nav",
  "notify",
- "serde",
  "serde_json",
  "shuma-discern",
  "sled",
  "tempfile",
- "thiserror 2.0.18",
- "tokio",
+ "thiserror 2.0.19",
  "ulid",
  "walkdir",
 ]
@@ -802,9 +1102,9 @@ name = "chasqui-explorer-llimphi"
 version = "0.1.0"
 dependencies = [
  "app-bus",
- "card-core",
  "card-sidecar",
  "chasqui-card",
+ "llimphi-icons",
  "llimphi-motion",
  "llimphi-theme",
  "llimphi-ui",
@@ -812,7 +1112,10 @@ dependencies = [
  "llimphi-widget-banner",
  "llimphi-widget-card",
  "llimphi-widget-context-menu",
+ "llimphi-widget-empty",
  "llimphi-widget-menubar",
+ "llimphi-widget-skeleton",
+ "llimphi-widget-toast",
  "rimay-localize",
  "wawa-config",
 ]
@@ -823,7 +1126,7 @@ version = "0.1.0"
 dependencies = [
  "serde",
  "serde_json",
- "thiserror 2.0.18",
+ "thiserror 2.0.19",
 ]
 
 [[package]]
@@ -871,23 +1174,33 @@ dependencies = [
  "zeroize",
 ]
 
+[[package]]
+name = "clipboard-win"
+version = "5.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4"
+dependencies = [
+ "error-code",
+]
+
 [[package]]
 name = "cobs"
 version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1"
 dependencies = [
- "thiserror 2.0.18",
+ "thiserror 2.0.19",
 ]
 
 [[package]]
 name = "codespan-reporting"
-version = "0.11.1"
+version = "0.12.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
+checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81"
 dependencies = [
+ "serde",
  "termcolor",
- "unicode-width 0.1.14",
+ "unicode-width",
 ]
 
 [[package]]
@@ -945,7 +1258,7 @@ dependencies = [
  "encode_unicode",
  "libc",
  "once_cell",
- "unicode-width 0.2.2",
+ "unicode-width",
  "windows-sys 0.59.0",
 ]
 
@@ -1001,7 +1314,7 @@ checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081"
 dependencies = [
  "bitflags 1.3.2",
  "core-foundation 0.9.4",
- "core-graphics-types",
+ "core-graphics-types 0.1.3",
  "foreign-types 0.5.0",
  "libc",
 ]
@@ -1017,6 +1330,26 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "core-graphics-types"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
+dependencies = [
+ "bitflags 2.13.1",
+ "core-foundation 0.10.1",
+ "libc",
+]
+
+[[package]]
+name = "core_maths"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30"
+dependencies = [
+ "libm",
+]
+
 [[package]]
 name = "cpufeatures"
 version = "0.2.17"
@@ -1052,18 +1385,18 @@ checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
 
 [[package]]
 name = "crossbeam-channel"
-version = "0.5.15"
+version = "0.5.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
+checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e"
 dependencies = [
  "crossbeam-utils",
 ]
 
 [[package]]
 name = "crossbeam-deque"
-version = "0.8.6"
+version = "0.8.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb"
 dependencies = [
  "crossbeam-epoch",
  "crossbeam-utils",
@@ -1071,18 +1404,18 @@ dependencies = [
 
 [[package]]
 name = "crossbeam-epoch"
-version = "0.9.18"
+version = "0.9.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
 dependencies = [
  "crossbeam-utils",
 ]
 
 [[package]]
 name = "crossbeam-utils"
-version = "0.8.21"
+version = "0.8.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
 
 [[package]]
 name = "crunchy"
@@ -1140,7 +1473,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
@@ -1164,7 +1497,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "strsim",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
@@ -1175,7 +1508,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
 dependencies = [
  "darling_core",
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
@@ -1210,7 +1543,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090"
 dependencies = [
  "data-encoding",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
@@ -1242,9 +1575,6 @@ name = "deranged"
 version = "0.5.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
-dependencies = [
- "powerfmt",
-]
 
 [[package]]
 name = "derive_builder"
@@ -1264,7 +1594,7 @@ dependencies = [
  "darling",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
@@ -1274,7 +1604,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
 dependencies = [
  "derive_builder_core",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
@@ -1336,6 +1666,16 @@ version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
 
+[[package]]
+name = "dispatch2"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
+dependencies = [
+ "bitflags 2.13.1",
+ "objc2 0.6.4",
+]
+
 [[package]]
 name = "displaydoc"
 version = "0.2.6"
@@ -1344,7 +1684,7 @@ checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
@@ -1409,9 +1749,9 @@ dependencies = [
 
 [[package]]
 name = "either"
-version = "1.16.0"
+version = "1.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
+checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d"
 
 [[package]]
 name = "embedded-io"
@@ -1440,6 +1780,12 @@ dependencies = [
  "cfg-if",
 ]
 
+[[package]]
+name = "endi"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
+
 [[package]]
 name = "enum-as-inner"
 version = "0.6.1"
@@ -1449,7 +1795,28 @@ dependencies = [
  "heck",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "enumflags2"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
+dependencies = [
+ "enumflags2_derive",
+ "serde",
+]
+
+[[package]]
+name = "enumflags2_derive"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
 ]
 
 [[package]]
@@ -1469,7 +1836,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
@@ -1488,6 +1855,12 @@ dependencies = [
  "windows-sys 0.61.2",
 ]
 
+[[package]]
+name = "error-code"
+version = "3.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59"
+
 [[package]]
 name = "esaxx-rs"
 version = "0.1.10"
@@ -1503,21 +1876,56 @@ dependencies = [
  "num-traits",
 ]
 
+[[package]]
+name = "event-listener"
+version = "5.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
+dependencies = [
+ "event-listener",
+ "pin-project-lite",
+]
+
 [[package]]
 name = "exr"
-version = "1.74.0"
+version = "1.74.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be"
+checksum = "711fe42c9964295e01ee3fba3f9fe0e1d24b98886950d68efe81b1c76e21adf3"
 dependencies = [
  "bit_field",
  "half",
  "lebe",
  "miniz_oxide",
+ "num-complex",
+ "pulp",
  "rayon-core",
  "smallvec",
  "zune-inflate",
 ]
 
+[[package]]
+name = "fastbloom"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef975e30683b2d965054bb0a836f8973857c4ebf6acf274fe46617cd285060d8"
+dependencies = [
+ "foldhash 0.2.0",
+ "libm",
+ "portable-atomic",
+ "siphasher",
+]
+
 [[package]]
 name = "fastembed"
 version = "4.9.1"
@@ -1537,9 +1945,9 @@ dependencies = [
 
 [[package]]
 name = "fastrand"
-version = "2.4.1"
+version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
+checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
 
 [[package]]
 name = "fax"
@@ -1578,6 +1986,12 @@ version = "0.1.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
 
+[[package]]
+name = "fixedbitset"
+version = "0.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
+
 [[package]]
 name = "flate2"
 version = "1.1.9"
@@ -1634,11 +2048,17 @@ version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
 
+[[package]]
+name = "foldhash"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
+
 [[package]]
 name = "font-types"
-version = "0.9.0"
+version = "0.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02a596f5713680923a2080d86de50fe472fb290693cf0f701187a1c8b36996b7"
+checksum = "39a654f404bbcbd48ea58c617c2993ee91d1cb63727a37bf2323a4edeed1b8c5"
 dependencies = [
  "bytemuck",
 ]
@@ -1653,36 +2073,35 @@ dependencies = [
 ]
 
 [[package]]
-name = "fontconfig-cache-parser"
-version = "0.2.0"
+name = "font-types"
+version = "0.12.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7f8afb20c8069fd676d27b214559a337cc619a605d25a87baa90b49a06f3b18"
+checksum = "0a7299a780854a6d391be2ae1c8521c9368471b559dbfd6a8dbd9f407eaff100"
 dependencies = [
  "bytemuck",
- "thiserror 1.0.69",
 ]
 
 [[package]]
 name = "fontique"
-version = "0.4.0"
+version = "0.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "64763d1f274c8383333851435b6cdf071c31cfcdb39fd5860d20943205a007a7"
+checksum = "ff3336bc0b87fe42305047263fa60d2eabd650d29cbe62fdeb2a66c7a0a595f9"
 dependencies = [
  "bytemuck",
- "fontconfig-cache-parser",
  "hashbrown 0.15.5",
- "icu_locid",
+ "icu_locale_core",
+ "linebender_resource_handle",
  "memmap2",
  "objc2 0.6.4",
  "objc2-core-foundation",
  "objc2-core-text",
  "objc2-foundation 0.3.2",
- "peniko",
- "read-fonts 0.29.3",
+ "read-fonts 0.35.0",
  "roxmltree",
  "smallvec",
  "windows 0.58.0",
  "windows-core 0.58.0",
+ "yeslogic-fontconfig-sys",
 ]
 
 [[package]]
@@ -1706,13 +2125,13 @@ dependencies = [
 
 [[package]]
 name = "foreign-types-macros"
-version = "0.2.3"
+version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
+checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 3.0.3",
 ]
 
 [[package]]
@@ -1736,6 +2155,17 @@ dependencies = [
  "percent-encoding",
 ]
 
+[[package]]
+name = "format"
+version = "0.1.0"
+source = "git+https://git.tawasuyu.net/tawasuyu/tawasuyu.git?tag=v0.2.0#a446bb3a73a0e022158cc0ec7deb65173859644b"
+dependencies = [
+ "blake3",
+ "postcard",
+ "serde",
+ "serde-big-array",
+]
+
 [[package]]
 name = "fs2"
 version = "0.4.3"
@@ -1757,9 +2187,9 @@ dependencies = [
 
 [[package]]
 name = "futures"
-version = "0.3.32"
+version = "0.3.33"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
+checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218"
 dependencies = [
  "futures-channel",
  "futures-core",
@@ -1782,9 +2212,9 @@ dependencies = [
 
 [[package]]
 name = "futures-channel"
-version = "0.3.32"
+version = "0.3.33"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
+checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae"
 dependencies = [
  "futures-core",
  "futures-sink",
@@ -1792,15 +2222,15 @@ dependencies = [
 
 [[package]]
 name = "futures-core"
-version = "0.3.32"
+version = "0.3.33"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7"
 
 [[package]]
 name = "futures-executor"
-version = "0.3.32"
+version = "0.3.33"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
+checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458"
 dependencies = [
  "futures-core",
  "futures-task",
@@ -1820,9 +2250,9 @@ dependencies = [
 
 [[package]]
 name = "futures-io"
-version = "0.3.32"
+version = "0.3.33"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
+checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a"
 
 [[package]]
 name = "futures-lite"
@@ -1830,19 +2260,22 @@ version = "2.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
 dependencies = [
+ "fastrand",
  "futures-core",
+ "futures-io",
+ "parking",
  "pin-project-lite",
 ]
 
 [[package]]
 name = "futures-macro"
-version = "0.3.32"
+version = "0.3.33"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
+checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
@@ -1858,15 +2291,15 @@ dependencies = [
 
 [[package]]
 name = "futures-sink"
-version = "0.3.32"
+version = "0.3.33"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
+checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307"
 
 [[package]]
 name = "futures-task"
-version = "0.3.32"
+version = "0.3.33"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
+checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109"
 
 [[package]]
 name = "futures-timer"
@@ -1876,9 +2309,9 @@ checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968"
 
 [[package]]
 name = "futures-util"
-version = "0.3.32"
+version = "0.3.33"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
+checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa"
 dependencies = [
  "futures-channel",
  "futures-core",
@@ -1940,24 +2373,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
 dependencies = [
  "cfg-if",
- "js-sys",
  "libc",
  "r-efi 5.3.0",
  "wasip2",
- "wasm-bindgen",
 ]
 
 [[package]]
 name = "getrandom"
-version = "0.4.2"
+version = "0.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
+checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
 dependencies = [
  "cfg-if",
+ "js-sys",
  "libc",
  "r-efi 6.0.0",
- "wasip2",
- "wasip3",
+ "rand_core 0.10.1",
+ "wasm-bindgen",
 ]
 
 [[package]]
@@ -2014,21 +2446,21 @@ dependencies = [
 
 [[package]]
 name = "gpu-alloc"
-version = "0.6.0"
+version = "0.6.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171"
+checksum = "45cf04b2726f02df5508c6de726acdc90cdf97ac771a9a0ffd8ba10a6e696bf9"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
  "gpu-alloc-types",
 ]
 
 [[package]]
 name = "gpu-alloc-types"
-version = "0.3.0"
+version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4"
+checksum = "b2bbed164dd10ed526c2e4fe3e721ca4a71c61730e5aafac6844b417b3227058"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
 ]
 
 [[package]]
@@ -2049,7 +2481,7 @@ version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
  "gpu-descriptor-types",
  "hashbrown 0.15.5",
 ]
@@ -2060,9 +2492,14 @@ version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
 ]
 
+[[package]]
+name = "grafo-nav"
+version = "0.1.0"
+source = "git+https://git.tawasuyu.net/tawasuyu/tawasuyu.git?tag=v0.2.0#a446bb3a73a0e022158cc0ec7deb65173859644b"
+
 [[package]]
 name = "grid"
 version = "1.0.1"
@@ -2081,9 +2518,9 @@ dependencies = [
 
 [[package]]
 name = "h2"
-version = "0.4.14"
+version = "0.4.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733"
+checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155"
 dependencies = [
  "atomic-waker",
  "bytes",
@@ -2106,9 +2543,23 @@ checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
 dependencies = [
  "cfg-if",
  "crunchy",
+ "num-traits",
  "zerocopy",
 ]
 
+[[package]]
+name = "harfrust"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92c020db12c71d8a12a3fe7607873cade3a01a6287e29d540c8723276221b9d8"
+dependencies = [
+ "bitflags 2.13.1",
+ "bytemuck",
+ "core_maths",
+ "read-fonts 0.35.0",
+ "smallvec",
+]
+
 [[package]]
 name = "hash32"
 version = "0.2.1"
@@ -2126,7 +2577,16 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
 dependencies = [
  "allocator-api2",
  "equivalent",
- "foldhash",
+ "foldhash 0.1.5",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+dependencies = [
+ "foldhash 0.2.0",
 ]
 
 [[package]]
@@ -2194,11 +2654,11 @@ dependencies = [
  "libc",
  "log",
  "native-tls",
- "rand 0.9.4",
+ "rand 0.9.5",
  "reqwest",
  "serde",
  "serde_json",
- "thiserror 2.0.18",
+ "thiserror 2.0.19",
  "ureq",
  "windows-sys 0.60.2",
 ]
@@ -2219,10 +2679,10 @@ dependencies = [
  "idna",
  "ipnet",
  "once_cell",
- "rand 0.9.4",
+ "rand 0.9.5",
  "ring",
  "socket2 0.5.10",
- "thiserror 2.0.18",
+ "thiserror 2.0.19",
  "tinyvec",
  "tokio",
  "tracing",
@@ -2242,10 +2702,10 @@ dependencies = [
  "moka",
  "once_cell",
  "parking_lot 0.12.5",
- "rand 0.9.4",
+ "rand 0.9.5",
  "resolv-conf",
  "smallvec",
- "thiserror 2.0.18",
+ "thiserror 2.0.19",
  "tokio",
  "tracing",
 ]
@@ -2270,9 +2730,9 @@ dependencies = [
 
 [[package]]
 name = "http"
-version = "1.4.1"
+version = "1.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0"
+checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425"
 dependencies = [
  "bytes",
  "itoa",
@@ -2280,9 +2740,9 @@ dependencies = [
 
 [[package]]
 name = "http-body"
-version = "1.0.1"
+version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"
 dependencies = [
  "bytes",
  "http",
@@ -2290,9 +2750,9 @@ dependencies = [
 
 [[package]]
 name = "http-body-util"
-version = "0.1.3"
+version = "0.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
+checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2"
 dependencies = [
  "bytes",
  "futures-core",
@@ -2309,9 +2769,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
 
 [[package]]
 name = "hyper"
-version = "1.10.1"
+version = "1.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498"
+checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72"
 dependencies = [
  "atomic-waker",
  "bytes",
@@ -2376,7 +2836,7 @@ dependencies = [
  "libc",
  "percent-encoding",
  "pin-project-lite",
- "socket2 0.6.4",
+ "socket2 0.6.5",
  "system-configuration",
  "tokio",
  "tower-service",
@@ -2405,24 +2865,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
 dependencies = [
  "displaydoc",
- "litemap 0.8.2",
- "tinystr 0.8.3",
- "writeable 0.6.3",
+ "litemap",
+ "serde",
+ "tinystr",
+ "writeable",
  "zerovec",
 ]
 
-[[package]]
-name = "icu_locid"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
-dependencies = [
- "displaydoc",
- "litemap 0.7.5",
- "tinystr 0.7.6",
- "writeable 0.5.5",
-]
-
 [[package]]
 name = "icu_normalizer"
 version = "2.2.0"
@@ -2471,7 +2920,7 @@ checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
 dependencies = [
  "displaydoc",
  "icu_locale_core",
- "writeable 0.6.3",
+ "writeable",
  "yoke",
  "zerofrom",
  "zerotrie",
@@ -2559,7 +3008,7 @@ dependencies = [
  "hyper",
  "hyper-util",
  "log",
- "rand 0.9.4",
+ "rand 0.9.5",
  "tokio",
  "url",
  "xmltree",
@@ -2601,9 +3050,9 @@ dependencies = [
 
 [[package]]
 name = "imgref"
-version = "1.12.1"
+version = "1.12.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "40fac9d56ed6437b198fddba683305e8e2d651aa42647f00f5ae542e7f5c94a2"
+checksum = "89194689a993ab15268672e99e7b0e19da2da3268ac682e8f02d29d4d1434cd7"
 
 [[package]]
 name = "indexmap"
@@ -2626,7 +3075,7 @@ dependencies = [
  "console",
  "number_prefix",
  "portable-atomic",
- "unicode-width 0.2.2",
+ "unicode-width",
  "web-time",
 ]
 
@@ -2643,9 +3092,9 @@ dependencies = [
 
 [[package]]
 name = "inotify-sys"
-version = "0.1.5"
+version = "0.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
+checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d"
 dependencies = [
  "libc",
 ]
@@ -2676,7 +3125,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
@@ -2704,7 +3153,7 @@ version = "0.3.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222"
 dependencies = [
- "socket2 0.6.4",
+ "socket2 0.6.5",
  "widestring",
  "windows-registry",
  "windows-result 0.4.1",
@@ -2744,7 +3193,7 @@ dependencies = [
  "jni-sys 0.4.1",
  "log",
  "simd_cesu8",
- "thiserror 2.0.18",
+ "thiserror 2.0.19",
  "walkdir",
  "windows-link",
 ]
@@ -2759,7 +3208,7 @@ dependencies = [
  "quote",
  "rustc_version",
  "simd_cesu8",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
@@ -2787,28 +3236,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
 dependencies = [
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
 name = "jobserver"
-version = "0.1.34"
+version = "0.1.35"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
+checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3"
 dependencies = [
- "getrandom 0.3.4",
+ "getrandom 0.4.3",
  "libc",
 ]
 
 [[package]]
 name = "js-sys"
-version = "0.3.99"
+version = "0.3.103"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11"
+checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
 dependencies = [
  "cfg-if",
  "futures-util",
- "once_cell",
  "wasm-bindgen",
 ]
 
@@ -2845,18 +3293,19 @@ version = "1.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
  "libc",
 ]
 
 [[package]]
 name = "kurbo"
-version = "0.11.3"
+version = "0.13.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62"
+checksum = "4b60dfc32f652b926df6192e55525b16d186c69d47876c3ead4da5cc9f8450e2"
 dependencies = [
  "arrayvec",
  "euclid",
+ "polycool",
  "smallvec",
 ]
 
@@ -2866,12 +3315,6 @@ version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
 
-[[package]]
-name = "leb128fmt"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
-
 [[package]]
 name = "lebe"
 version = "0.5.3"
@@ -2880,15 +3323,15 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
 
 [[package]]
 name = "libc"
-version = "0.2.186"
+version = "0.2.189"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
 
 [[package]]
 name = "libfuzzer-sys"
-version = "0.4.12"
+version = "0.4.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d"
+checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2"
 dependencies = [
  "arbitrary",
  "cc",
@@ -2904,6 +3347,12 @@ dependencies = [
  "windows-link",
 ]
 
+[[package]]
+name = "libm"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
+
 [[package]]
 name = "libp2p"
 version = "0.56.0"
@@ -2936,7 +3385,7 @@ dependencies = [
  "multiaddr",
  "pin-project",
  "rw-stream-sink",
- "thiserror 2.0.18",
+ "thiserror 2.0.19",
 ]
 
 [[package]]
@@ -2968,9 +3417,9 @@ dependencies = [
  "libp2p-swarm",
  "quick-protobuf",
  "quick-protobuf-codec",
- "rand 0.8.6",
+ "rand 0.8.7",
  "rand_core 0.6.4",
- "thiserror 2.0.18",
+ "thiserror 2.0.19",
  "tracing",
  "web-time",
 ]
@@ -3003,9 +3452,9 @@ dependencies = [
  "parking_lot 0.12.5",
  "pin-project",
  "quick-protobuf",
- "rand 0.8.6",
+ "rand 0.8.7",
  "rw-stream-sink",
- "thiserror 2.0.18",
+ "thiserror 2.0.19",
  "tracing",
  "unsigned-varint 0.8.0",
  "web-time",
@@ -3028,7 +3477,7 @@ dependencies = [
  "libp2p-swarm",
  "quick-protobuf",
  "quick-protobuf-codec",
- "thiserror 2.0.18",
+ "thiserror 2.0.19",
  "tracing",
  "web-time",
 ]
@@ -3066,7 +3515,7 @@ dependencies = [
  "quick-protobuf",
  "quick-protobuf-codec",
  "smallvec",
- "thiserror 2.0.18",
+ "thiserror 2.0.19",
  "tracing",
 ]
 
@@ -3081,9 +3530,9 @@ dependencies = [
  "hkdf",
  "multihash",
  "prost",
- "rand 0.8.6",
+ "rand 0.8.7",
  "sha2",
- "thiserror 2.0.18",
+ "thiserror 2.0.19",
  "tracing",
  "zeroize",
 ]
@@ -3106,10 +3555,10 @@ dependencies = [
  "libp2p-swarm",
  "quick-protobuf",
  "quick-protobuf-codec",
- "rand 0.8.6",
+ "rand 0.8.7",
  "sha2",
  "smallvec",
- "thiserror 2.0.18",
+ "thiserror 2.0.19",
  "tracing",
  "uint",
  "web-time",
@@ -3127,7 +3576,7 @@ dependencies = [
  "libp2p-core",
  "libp2p-identity",
  "libp2p-swarm",
- "rand 0.8.6",
+ "rand 0.8.7",
  "smallvec",
  "socket2 0.5.10",
  "tokio",
@@ -3167,10 +3616,10 @@ dependencies = [
  "multiaddr",
  "multihash",
  "quick-protobuf",
- "rand 0.8.6",
+ "rand 0.8.7",
  "snow",
  "static_assertions",
- "thiserror 2.0.18",
+ "thiserror 2.0.19",
  "tracing",
  "x25519-dalek",
  "zeroize",
@@ -3178,9 +3627,9 @@ dependencies = [
 
 [[package]]
 name = "libp2p-quic"
-version = "0.13.0"
+version = "0.13.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8dc448b2de9f4745784e3751fe8bc6c473d01b8317edd5ababcb0dec803d843f"
+checksum = "9dcc597d70bf7f6f30cbe07081802c836184e48416e89e9ce73a0ba2c56a319e"
 dependencies = [
  "futures",
  "futures-timer",
@@ -3189,11 +3638,12 @@ dependencies = [
  "libp2p-identity",
  "libp2p-tls",
  "quinn",
- "rand 0.8.6",
+ "quinn-proto",
+ "rand 0.8.7",
  "ring",
  "rustls",
  "socket2 0.5.10",
- "thiserror 2.0.18",
+ "thiserror 2.0.19",
  "tokio",
  "tracing",
 ]
@@ -3215,9 +3665,9 @@ dependencies = [
  "libp2p-swarm",
  "quick-protobuf",
  "quick-protobuf-codec",
- "rand 0.8.6",
+ "rand 0.8.7",
  "static_assertions",
- "thiserror 2.0.18",
+ "thiserror 2.0.19",
  "tracing",
  "web-time",
 ]
@@ -3234,7 +3684,7 @@ dependencies = [
  "libp2p-core",
  "libp2p-identity",
  "libp2p-swarm",
- "rand 0.8.6",
+ "rand 0.8.7",
  "smallvec",
  "tracing",
 ]
@@ -3249,7 +3699,7 @@ dependencies = [
  "libp2p-core",
  "libp2p-identity",
  "libp2p-swarm",
- "rand 0.8.6",
+ "rand 0.8.7",
  "tracing",
 ]
 
@@ -3268,7 +3718,7 @@ dependencies = [
  "libp2p-identity",
  "libp2p-swarm-derive",
  "multistream-select",
- "rand 0.8.6",
+ "rand 0.8.7",
  "smallvec",
  "tokio",
  "tracing",
@@ -3283,7 +3733,7 @@ checksum = "dd297cf53f0cb3dee4d2620bb319ae47ef27c702684309f682bdb7e55a18ae9c"
 dependencies = [
  "heck",
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
@@ -3297,7 +3747,7 @@ dependencies = [
  "if-watch",
  "libc",
  "libp2p-core",
- "socket2 0.6.4",
+ "socket2 0.6.5",
  "tokio",
  "tracing",
 ]
@@ -3316,7 +3766,7 @@ dependencies = [
  "ring",
  "rustls",
  "rustls-webpki",
- "thiserror 2.0.18",
+ "thiserror 2.0.19",
  "x509-parser",
  "yasna",
 ]
@@ -3345,7 +3795,7 @@ dependencies = [
  "either",
  "futures",
  "libp2p-core",
- "thiserror 2.0.18",
+ "thiserror 2.0.19",
  "tracing",
  "yamux 0.12.1",
  "yamux 0.13.10",
@@ -3353,14 +3803,14 @@ dependencies = [
 
 [[package]]
 name = "libredox"
-version = "0.1.17"
+version = "0.1.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3"
+checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
  "libc",
  "plain",
- "redox_syscall 0.8.1",
+ "redox_syscall 0.9.0",
 ]
 
 [[package]]
@@ -3381,12 +3831,6 @@ version = "0.12.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
 
-[[package]]
-name = "litemap"
-version = "0.7.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856"
-
 [[package]]
 name = "litemap"
 version = "0.8.2"
@@ -3402,7 +3846,7 @@ checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
 [[package]]
 name = "llimphi-compositor"
 version = "0.1.0"
-source = "git+https://gitea.tawasuyu.net/sergio/gioser.git?tag=v0.1.0#692b40336c1e9cb12ab561764d6943cb65905ab3"
+source = "git+https://git.tawasuyu.net/tawasuyu/tawasuyu.git?tag=v0.2.0#a446bb3a73a0e022158cc0ec7deb65173859644b"
 dependencies = [
  "llimphi-layout",
  "llimphi-text",
@@ -3413,7 +3857,7 @@ dependencies = [
 [[package]]
 name = "llimphi-hal"
 version = "0.1.0"
-source = "git+https://gitea.tawasuyu.net/sergio/gioser.git?tag=v0.1.0#692b40336c1e9cb12ab561764d6943cb65905ab3"
+source = "git+https://git.tawasuyu.net/tawasuyu/tawasuyu.git?tag=v0.2.0#a446bb3a73a0e022158cc0ec7deb65173859644b"
 dependencies = [
  "pollster",
  "raw-window-handle",
@@ -3421,10 +3865,18 @@ dependencies = [
  "winit",
 ]
 
+[[package]]
+name = "llimphi-icons"
+version = "0.1.0"
+source = "git+https://git.tawasuyu.net/tawasuyu/tawasuyu.git?tag=v0.2.0#a446bb3a73a0e022158cc0ec7deb65173859644b"
+dependencies = [
+ "llimphi-ui",
+]
+
 [[package]]
 name = "llimphi-layout"
 version = "0.1.0"
-source = "git+https://gitea.tawasuyu.net/sergio/gioser.git?tag=v0.1.0#692b40336c1e9cb12ab561764d6943cb65905ab3"
+source = "git+https://git.tawasuyu.net/tawasuyu/tawasuyu.git?tag=v0.2.0#a446bb3a73a0e022158cc0ec7deb65173859644b"
 dependencies = [
  "taffy",
 ]
@@ -3432,7 +3884,7 @@ dependencies = [
 [[package]]
 name = "llimphi-motion"
 version = "0.1.0"
-source = "git+https://gitea.tawasuyu.net/sergio/gioser.git?tag=v0.1.0#692b40336c1e9cb12ab561764d6943cb65905ab3"
+source = "git+https://git.tawasuyu.net/tawasuyu/tawasuyu.git?tag=v0.2.0#a446bb3a73a0e022158cc0ec7deb65173859644b"
 dependencies = [
  "llimphi-theme",
  "llimphi-ui",
@@ -3441,7 +3893,7 @@ dependencies = [
 [[package]]
 name = "llimphi-raster"
 version = "0.1.0"
-source = "git+https://gitea.tawasuyu.net/sergio/gioser.git?tag=v0.1.0#692b40336c1e9cb12ab561764d6943cb65905ab3"
+source = "git+https://git.tawasuyu.net/tawasuyu/tawasuyu.git?tag=v0.2.0#a446bb3a73a0e022158cc0ec7deb65173859644b"
 dependencies = [
  "llimphi-hal",
  "pollster",
@@ -3451,7 +3903,7 @@ dependencies = [
 [[package]]
 name = "llimphi-text"
 version = "0.1.0"
-source = "git+https://gitea.tawasuyu.net/sergio/gioser.git?tag=v0.1.0#692b40336c1e9cb12ab561764d6943cb65905ab3"
+source = "git+https://git.tawasuyu.net/tawasuyu/tawasuyu.git?tag=v0.2.0#a446bb3a73a0e022158cc0ec7deb65173859644b"
 dependencies = [
  "parley",
  "vello",
@@ -3460,7 +3912,7 @@ dependencies = [
 [[package]]
 name = "llimphi-theme"
 version = "0.1.0"
-source = "git+https://gitea.tawasuyu.net/sergio/gioser.git?tag=v0.1.0#692b40336c1e9cb12ab561764d6943cb65905ab3"
+source = "git+https://git.tawasuyu.net/tawasuyu/tawasuyu.git?tag=v0.2.0#a446bb3a73a0e022158cc0ec7deb65173859644b"
 dependencies = [
  "llimphi-raster",
 ]
@@ -3468,21 +3920,27 @@ dependencies = [
 [[package]]
 name = "llimphi-ui"
 version = "0.1.0"
-source = "git+https://gitea.tawasuyu.net/sergio/gioser.git?tag=v0.1.0#692b40336c1e9cb12ab561764d6943cb65905ab3"
+source = "git+https://git.tawasuyu.net/tawasuyu/tawasuyu.git?tag=v0.2.0#a446bb3a73a0e022158cc0ec7deb65173859644b"
 dependencies = [
+ "accesskit",
+ "accesskit_winit",
+ "arboard",
  "llimphi-compositor",
  "llimphi-hal",
  "llimphi-layout",
  "llimphi-raster",
  "llimphi-text",
+ "log",
  "pollster",
+ "uuid",
 ]
 
 [[package]]
 name = "llimphi-widget-app-header"
 version = "0.1.0"
-source = "git+https://gitea.tawasuyu.net/sergio/gioser.git?tag=v0.1.0#692b40336c1e9cb12ab561764d6943cb65905ab3"
+source = "git+https://git.tawasuyu.net/tawasuyu/tawasuyu.git?tag=v0.2.0#a446bb3a73a0e022158cc0ec7deb65173859644b"
 dependencies = [
+ "llimphi-icons",
  "llimphi-theme",
  "llimphi-ui",
  "llimphi-widget-panel",
@@ -3491,7 +3949,7 @@ dependencies = [
 [[package]]
 name = "llimphi-widget-banner"
 version = "0.1.0"
-source = "git+https://gitea.tawasuyu.net/sergio/gioser.git?tag=v0.1.0#692b40336c1e9cb12ab561764d6943cb65905ab3"
+source = "git+https://git.tawasuyu.net/tawasuyu/tawasuyu.git?tag=v0.2.0#a446bb3a73a0e022158cc0ec7deb65173859644b"
 dependencies = [
  "llimphi-ui",
 ]
@@ -3499,7 +3957,7 @@ dependencies = [
 [[package]]
 name = "llimphi-widget-button"
 version = "0.1.0"
-source = "git+https://gitea.tawasuyu.net/sergio/gioser.git?tag=v0.1.0#692b40336c1e9cb12ab561764d6943cb65905ab3"
+source = "git+https://git.tawasuyu.net/tawasuyu/tawasuyu.git?tag=v0.2.0#a446bb3a73a0e022158cc0ec7deb65173859644b"
 dependencies = [
  "llimphi-theme",
  "llimphi-ui",
@@ -3508,7 +3966,7 @@ dependencies = [
 [[package]]
 name = "llimphi-widget-card"
 version = "0.1.0"
-source = "git+https://gitea.tawasuyu.net/sergio/gioser.git?tag=v0.1.0#692b40336c1e9cb12ab561764d6943cb65905ab3"
+source = "git+https://git.tawasuyu.net/tawasuyu/tawasuyu.git?tag=v0.2.0#a446bb3a73a0e022158cc0ec7deb65173859644b"
 dependencies = [
  "llimphi-theme",
  "llimphi-ui",
@@ -3518,17 +3976,28 @@ dependencies = [
 [[package]]
 name = "llimphi-widget-context-menu"
 version = "0.1.0"
-source = "git+https://gitea.tawasuyu.net/sergio/gioser.git?tag=v0.1.0#692b40336c1e9cb12ab561764d6943cb65905ab3"
+source = "git+https://git.tawasuyu.net/tawasuyu/tawasuyu.git?tag=v0.2.0#a446bb3a73a0e022158cc0ec7deb65173859644b"
 dependencies = [
+ "llimphi-icons",
  "llimphi-theme",
  "llimphi-ui",
  "llimphi-widget-panel",
 ]
 
+[[package]]
+name = "llimphi-widget-empty"
+version = "0.1.0"
+source = "git+https://git.tawasuyu.net/tawasuyu/tawasuyu.git?tag=v0.2.0#a446bb3a73a0e022158cc0ec7deb65173859644b"
+dependencies = [
+ "llimphi-icons",
+ "llimphi-theme",
+ "llimphi-ui",
+]
+
 [[package]]
 name = "llimphi-widget-menubar"
 version = "0.1.0"
-source = "git+https://gitea.tawasuyu.net/sergio/gioser.git?tag=v0.1.0#692b40336c1e9cb12ab561764d6943cb65905ab3"
+source = "git+https://git.tawasuyu.net/tawasuyu/tawasuyu.git?tag=v0.2.0#a446bb3a73a0e022158cc0ec7deb65173859644b"
 dependencies = [
  "app-bus",
  "llimphi-theme",
@@ -3540,7 +4009,16 @@ dependencies = [
 [[package]]
 name = "llimphi-widget-panel"
 version = "0.1.0"
-source = "git+https://gitea.tawasuyu.net/sergio/gioser.git?tag=v0.1.0#692b40336c1e9cb12ab561764d6943cb65905ab3"
+source = "git+https://git.tawasuyu.net/tawasuyu/tawasuyu.git?tag=v0.2.0#a446bb3a73a0e022158cc0ec7deb65173859644b"
+dependencies = [
+ "llimphi-theme",
+ "llimphi-ui",
+]
+
+[[package]]
+name = "llimphi-widget-skeleton"
+version = "0.1.0"
+source = "git+https://git.tawasuyu.net/tawasuyu/tawasuyu.git?tag=v0.2.0#a446bb3a73a0e022158cc0ec7deb65173859644b"
 dependencies = [
  "llimphi-theme",
  "llimphi-ui",
@@ -3549,13 +4027,23 @@ dependencies = [
 [[package]]
 name = "llimphi-widget-stat-card"
 version = "0.1.0"
-source = "git+https://gitea.tawasuyu.net/sergio/gioser.git?tag=v0.1.0#692b40336c1e9cb12ab561764d6943cb65905ab3"
+source = "git+https://git.tawasuyu.net/tawasuyu/tawasuyu.git?tag=v0.2.0#a446bb3a73a0e022158cc0ec7deb65173859644b"
 dependencies = [
  "llimphi-theme",
  "llimphi-ui",
  "llimphi-widget-card",
 ]
 
+[[package]]
+name = "llimphi-widget-toast"
+version = "0.1.0"
+source = "git+https://git.tawasuyu.net/tawasuyu/tawasuyu.git?tag=v0.2.0#a446bb3a73a0e022158cc0ec7deb65173859644b"
+dependencies = [
+ "llimphi-icons",
+ "llimphi-theme",
+ "llimphi-ui",
+]
+
 [[package]]
 name = "lock_api"
 version = "0.4.14"
@@ -3567,9 +4055,9 @@ dependencies = [
 
 [[package]]
 name = "log"
-version = "0.4.32"
+version = "0.4.33"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
+checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
 
 [[package]]
 name = "loop9"
@@ -3619,7 +4107,7 @@ checksum = "757aee279b8bdbb9f9e676796fd459e4207a1f986e87886700abf589f5abf771"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
@@ -3633,9 +4121,9 @@ dependencies = [
 
 [[package]]
 name = "matrixmultiply"
-version = "0.3.10"
+version = "0.3.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08"
+checksum = "3f607c237553f086e7043417a51df26b2eb899d3caff94e6a67592ff992fedc7"
 dependencies = [
  "autocfg",
  "rawpointer",
@@ -3653,28 +4141,37 @@ dependencies = [
 
 [[package]]
 name = "memchr"
-version = "2.8.1"
+version = "2.8.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
 
 [[package]]
 name = "memmap2"
-version = "0.9.10"
+version = "0.9.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3"
+checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0"
 dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "memoffset"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
+dependencies = [
+ "autocfg",
+]
+
 [[package]]
 name = "metal"
-version = "0.31.0"
+version = "0.32.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f569fb946490b5743ad69813cb19629130ce9374034abe31614a36402d18f99e"
+checksum = "00c15a6f673ff72ddcc22394663290f870fb224c1bfce55734a75c414150e605"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
  "block",
- "core-graphics-types",
+ "core-graphics-types 0.2.0",
  "foreign-types 0.5.0",
  "log",
  "objc",
@@ -3717,9 +4214,9 @@ dependencies = [
 
 [[package]]
 name = "mio"
-version = "1.2.1"
+version = "1.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
+checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
 dependencies = [
  "libc",
  "wasi",
@@ -3762,7 +4259,7 @@ checksum = "e4db6d5580af57bf992f59068d4ea26fd518574ff48d7639b255a36f9de6e7e9"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
@@ -3796,12 +4293,13 @@ dependencies = [
 
 [[package]]
 name = "multibase"
-version = "0.9.2"
+version = "0.9.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8694bb4835f452b0e3bb06dbebb1d6fc5385b6ca1caf2e55fd165c042390ec77"
+checksum = "7e0e4a371cbf1dfd666b658ba137763edb23c45beb43cfe369b5593cd6b437b6"
 dependencies = [
  "base-x",
  "base256emoji",
+ "base45",
  "data-encoding",
  "data-encoding-macro",
 ]
@@ -3831,24 +4329,28 @@ dependencies = [
 
 [[package]]
 name = "naga"
-version = "24.0.0"
+version = "27.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e380993072e52eef724eddfcde0ed013b0c023c3f0417336ed041aa9f076994e"
+checksum = "066cf25f0e8b11ee0df221219010f213ad429855f57c494f995590c861a9a7d8"
 dependencies = [
  "arrayvec",
  "bit-set",
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
+ "cfg-if",
  "cfg_aliases",
  "codespan-reporting",
+ "half",
+ "hashbrown 0.16.1",
  "hexf-parse",
  "indexmap",
+ "libm",
  "log",
+ "num-traits",
+ "once_cell",
  "rustc-hash 1.1.0",
  "spirv",
- "strum",
- "termcolor",
- "thiserror 2.0.18",
- "unicode-xid",
+ "thiserror 2.0.19",
+ "unicode-ident",
 ]
 
 [[package]]
@@ -3889,10 +4391,10 @@ version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
  "jni-sys 0.3.1",
  "log",
- "ndk-sys 0.6.0+11769913",
+ "ndk-sys",
  "num_enum",
  "raw-window-handle",
  "thiserror 1.0.69",
@@ -3904,15 +4406,6 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
 
-[[package]]
-name = "ndk-sys"
-version = "0.5.0+25.2.9519653"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691"
-dependencies = [
- "jni-sys 0.3.1",
-]
-
 [[package]]
 name = "ndk-sys"
 version = "0.6.0+11769913"
@@ -3937,7 +4430,7 @@ version = "0.28.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4ce3636fa715e988114552619582b530481fd5ef176a1e5c1bf024077c2c9445"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
  "libc",
  "log",
  "netlink-packet-core",
@@ -3945,16 +4438,17 @@ dependencies = [
 
 [[package]]
 name = "netlink-proto"
-version = "0.12.0"
+version = "0.12.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b65d130ee111430e47eed7896ea43ca693c387f097dd97376bffafbf25812128"
+checksum = "e6f7398dddf5f152d2a91a2921a134c6097056e292c0d4b9906007855e7cece6"
 dependencies = [
  "bytes",
- "futures",
+ "futures-channel",
+ "futures-util",
  "log",
  "netlink-packet-core",
  "netlink-sys",
- "thiserror 2.0.18",
+ "thiserror 2.0.19",
 ]
 
 [[package]]
@@ -3982,7 +4476,7 @@ version = "0.30.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
  "cfg-if",
  "cfg_aliases",
  "libc",
@@ -4034,7 +4528,7 @@ version = "6.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
  "crossbeam-channel",
  "filetime",
  "fsevent-sys",
@@ -4058,9 +4552,9 @@ dependencies = [
 
 [[package]]
 name = "num-bigint"
-version = "0.4.6"
+version = "0.4.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
+checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
 dependencies = [
  "num-integer",
  "num-traits",
@@ -4072,6 +4566,7 @@ version = "0.4.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
 dependencies = [
+ "bytemuck",
  "num-traits",
 ]
 
@@ -4089,7 +4584,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
@@ -4119,6 +4614,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
 dependencies = [
  "autocfg",
+ "libm",
 ]
 
 [[package]]
@@ -4140,7 +4636,7 @@ dependencies = [
  "proc-macro-crate",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
@@ -4189,7 +4685,7 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
  "block2",
  "libc",
  "objc2 0.5.2",
@@ -4199,13 +4695,25 @@ dependencies = [
  "objc2-quartz-core",
 ]
 
+[[package]]
+name = "objc2-app-kit"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
+dependencies = [
+ "bitflags 2.13.1",
+ "objc2 0.6.4",
+ "objc2-core-graphics",
+ "objc2-foundation 0.3.2",
+]
+
 [[package]]
 name = "objc2-cloud-kit"
 version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
  "block2",
  "objc2 0.5.2",
  "objc2-core-location",
@@ -4229,7 +4737,7 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
  "block2",
  "objc2 0.5.2",
  "objc2-foundation 0.2.2",
@@ -4241,7 +4749,22 @@ version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
+ "dispatch2",
+ "objc2 0.6.4",
+]
+
+[[package]]
+name = "objc2-core-graphics"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
+dependencies = [
+ "bitflags 2.13.1",
+ "dispatch2",
+ "objc2 0.6.4",
+ "objc2-core-foundation",
+ "objc2-io-surface",
 ]
 
 [[package]]
@@ -4274,7 +4797,7 @@ version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
  "objc2-core-foundation",
 ]
 
@@ -4290,7 +4813,7 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
  "block2",
  "dispatch",
  "libc",
@@ -4303,8 +4826,20 @@ version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
+ "objc2 0.6.4",
+ "objc2-core-foundation",
+]
+
+[[package]]
+name = "objc2-io-surface"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
+dependencies = [
+ "bitflags 2.13.1",
  "objc2 0.6.4",
+ "objc2-core-foundation",
 ]
 
 [[package]]
@@ -4315,7 +4850,7 @@ checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398"
 dependencies = [
  "block2",
  "objc2 0.5.2",
- "objc2-app-kit",
+ "objc2-app-kit 0.2.2",
  "objc2-foundation 0.2.2",
 ]
 
@@ -4325,7 +4860,7 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
  "block2",
  "objc2 0.5.2",
  "objc2-foundation 0.2.2",
@@ -4337,7 +4872,7 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
  "block2",
  "objc2 0.5.2",
  "objc2-foundation 0.2.2",
@@ -4360,7 +4895,7 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
  "block2",
  "objc2 0.5.2",
  "objc2-cloud-kit",
@@ -4392,7 +4927,7 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
  "block2",
  "objc2 0.5.2",
  "objc2-core-location",
@@ -4424,7 +4959,7 @@ version = "6.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0cc3cbf698f9438986c11a880c90a6d04b9de27575afd28bbf45b154b6c709e2"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
  "libc",
  "once_cell",
  "onig_sys",
@@ -4448,11 +4983,11 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
 
 [[package]]
 name = "openssl"
-version = "0.10.80"
+version = "0.10.81"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967"
+checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
  "cfg-if",
  "foreign-types 0.3.2",
  "libc",
@@ -4468,7 +5003,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
@@ -4479,9 +5014,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
 
 [[package]]
 name = "openssl-sys"
-version = "0.9.116"
+version = "0.9.117"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4"
+checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695"
 dependencies = [
  "cc",
  "libc",
@@ -4507,13 +5042,23 @@ dependencies = [
 
 [[package]]
 name = "ordered-float"
-version = "4.6.0"
+version = "5.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951"
+checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e"
 dependencies = [
  "num-traits",
 ]
 
+[[package]]
+name = "ordered-stream"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+]
+
 [[package]]
 name = "ort"
 version = "2.0.0-rc.9"
@@ -4538,6 +5083,16 @@ dependencies = [
  "ureq",
 ]
 
+[[package]]
+name = "os_pipe"
+version = "1.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
 [[package]]
 name = "owned_ttf_parser"
 version = "0.25.1"
@@ -4603,14 +5158,15 @@ dependencies = [
 
 [[package]]
 name = "parley"
-version = "0.4.0"
+version = "0.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e28dadbe655332fd7d996794ec8d0c376695f6ca47bc75aa01e0967c7f28e42a"
+checksum = "26746861bb76dbc9bcd5ed1b0b55d2fedf291100961251702a031ab2abd2ce52"
 dependencies = [
  "fontique",
+ "harfrust",
  "hashbrown 0.15.5",
- "peniko",
- "skrifa 0.31.3",
+ "linebender_resource_handle",
+ "skrifa 0.37.0",
  "swash",
 ]
 
@@ -4638,9 +5194,9 @@ dependencies = [
 
 [[package]]
 name = "peniko"
-version = "0.4.1"
+version = "0.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b44f9ddd2f480176b34278eb653ec1c8062f3b143a4e16eeff5ffac3334e288"
+checksum = "839c8299360d2e998bdb106dc0a6cd71dcc5f4df51df1b620361bf50e283cca6"
 dependencies = [
  "color",
  "kurbo",
@@ -4654,6 +5210,60 @@ version = "2.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
 
+[[package]]
+name = "petgraph"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455"
+dependencies = [
+ "fixedbitset",
+ "hashbrown 0.15.5",
+ "indexmap",
+]
+
+[[package]]
+name = "phf"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
+dependencies = [
+ "phf_macros",
+ "phf_shared",
+ "serde",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
+dependencies = [
+ "fastrand",
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"
+dependencies = [
+ "siphasher",
+]
+
 [[package]]
 name = "pin-project"
 version = "1.1.13"
@@ -4671,7 +5281,7 @@ checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
@@ -4680,6 +5290,17 @@ version = "0.2.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
 
+[[package]]
+name = "piper"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
+dependencies = [
+ "atomic-waker",
+ "fastrand",
+ "futures-io",
+]
+
 [[package]]
 name = "pkcs8"
 version = "0.10.2"
@@ -4721,7 +5342,7 @@ version = "0.18.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
  "crc32fast",
  "fdeflate",
  "flate2",
@@ -4759,6 +5380,15 @@ dependencies = [
  "universal-hash",
 ]
 
+[[package]]
+name = "polycool"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6"
+dependencies = [
+ "arrayvec",
+]
+
 [[package]]
 name = "polyval"
 version = "0.6.2"
@@ -4773,9 +5403,9 @@ dependencies = [
 
 [[package]]
 name = "portable-atomic"
-version = "1.13.1"
+version = "1.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
+checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3"
 
 [[package]]
 name = "portable-atomic-util"
@@ -4829,23 +5459,13 @@ version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa"
 
-[[package]]
-name = "prettyplease"
-version = "0.2.37"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
-dependencies = [
- "proc-macro2",
- "syn",
-]
-
 [[package]]
 name = "proc-macro-crate"
 version = "3.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
 dependencies = [
- "toml_edit 0.25.12+spec-1.1.0",
+ "toml_edit 0.25.13+spec-1.1.0",
 ]
 
 [[package]]
@@ -4856,9 +5476,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.106"
+version = "1.0.107"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
 dependencies = [
  "unicode-ident",
 ]
@@ -4879,7 +5499,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb"
 dependencies = [
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
@@ -4902,14 +5522,14 @@ checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
 name = "prost"
-version = "0.14.3"
+version = "0.14.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568"
+checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1"
 dependencies = [
  "bytes",
  "prost-derive",
@@ -4917,22 +5537,45 @@ dependencies = [
 
 [[package]]
 name = "prost-derive"
-version = "0.14.3"
+version = "0.14.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b"
+checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf"
 dependencies = [
  "anyhow",
  "itertools",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "pulp"
+version = "0.22.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "046aa45b989642ec2e4717c8e72d677b13edd831a4d3b6cf37d9a3e54912496a"
+dependencies = [
+ "bytemuck",
+ "cfg-if",
+ "libm",
+ "num-complex",
+ "paste",
+ "pulp-wasm-simd-flag",
+ "raw-cpuid",
+ "reborrow",
+ "version_check",
 ]
 
+[[package]]
+name = "pulp-wasm-simd-flag"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d8f70e07b9c3962945a74e59ca1c511bba65b6419468acc217c457d93f3c740"
+
 [[package]]
 name = "pxfm"
-version = "0.1.29"
+version = "0.1.30"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f"
+checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea"
 
 [[package]]
 name = "qoi"
@@ -4973,18 +5616,18 @@ dependencies = [
 
 [[package]]
 name = "quick-xml"
-version = "0.39.4"
+version = "0.41.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e"
+checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1"
 dependencies = [
  "memchr",
 ]
 
 [[package]]
 name = "quinn"
-version = "0.11.9"
+version = "0.11.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
+checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
 dependencies = [
  "bytes",
  "cfg_aliases",
@@ -4992,10 +5635,10 @@ dependencies = [
  "pin-project-lite",
  "quinn-proto",
  "quinn-udp",
- "rustc-hash 2.1.2",
+ "rustc-hash 2.1.3",
  "rustls",
- "socket2 0.6.4",
- "thiserror 2.0.18",
+ "socket2 0.6.5",
+ "thiserror 2.0.19",
  "tokio",
  "tracing",
  "web-time",
@@ -5003,20 +5646,22 @@ dependencies = [
 
 [[package]]
 name = "quinn-proto"
-version = "0.11.14"
+version = "0.11.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
+checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560"
 dependencies = [
  "bytes",
- "getrandom 0.3.4",
+ "fastbloom",
+ "getrandom 0.4.3",
  "lru-slab",
- "rand 0.9.4",
+ "rand 0.10.2",
+ "rand_pcg",
  "ring",
- "rustc-hash 2.1.2",
+ "rustc-hash 2.1.3",
  "rustls",
  "rustls-pki-types",
  "slab",
- "thiserror 2.0.18",
+ "thiserror 2.0.19",
  "tinyvec",
  "tracing",
  "web-time",
@@ -5024,23 +5669,23 @@ dependencies = [
 
 [[package]]
 name = "quinn-udp"
-version = "0.5.14"
+version = "0.5.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
+checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
 dependencies = [
  "cfg_aliases",
  "libc",
  "once_cell",
- "socket2 0.6.4",
+ "socket2 0.6.5",
  "tracing",
- "windows-sys 0.60.2",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
 name = "quote"
-version = "1.0.45"
+version = "1.0.47"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
 dependencies = [
  "proc-macro2",
 ]
@@ -5059,9 +5704,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
 
 [[package]]
 name = "rand"
-version = "0.8.6"
+version = "0.8.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
+checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a"
 dependencies = [
  "libc",
  "rand_chacha 0.3.1",
@@ -5070,14 +5715,25 @@ dependencies = [
 
 [[package]]
 name = "rand"
-version = "0.9.4"
+version = "0.9.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
+checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
 dependencies = [
  "rand_chacha 0.9.0",
  "rand_core 0.9.5",
 ]
 
+[[package]]
+name = "rand"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
+dependencies = [
+ "chacha20 0.10.1",
+ "getrandom 0.4.3",
+ "rand_core 0.10.1",
+]
+
 [[package]]
 name = "rand_chacha"
 version = "0.3.1"
@@ -5116,6 +5772,21 @@ dependencies = [
  "getrandom 0.3.4",
 ]
 
+[[package]]
+name = "rand_core"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
+
+[[package]]
+name = "rand_pcg"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
+dependencies = [
+ "rand_core 0.10.1",
+]
+
 [[package]]
 name = "range-alloc"
 version = "0.1.5"
@@ -5149,10 +5820,10 @@ dependencies = [
  "num-traits",
  "paste",
  "profiling",
- "rand 0.9.4",
+ "rand 0.9.5",
  "rand_chacha 0.9.0",
  "simd_helpers",
- "thiserror 2.0.18",
+ "thiserror 2.0.19",
  "v_frame",
  "wasm-bindgen",
 ]
@@ -5172,6 +5843,15 @@ dependencies = [
  "rgb",
 ]
 
+[[package]]
+name = "raw-cpuid"
+version = "11.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186"
+dependencies = [
+ "bitflags 2.13.1",
+]
+
 [[package]]
 name = "raw-window-handle"
 version = "0.6.2"
@@ -5230,34 +5910,42 @@ dependencies = [
 
 [[package]]
 name = "read-fonts"
-version = "0.29.3"
+version = "0.35.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "04ca636dac446b5664bd16c069c00a9621806895b8bb02c2dc68542b23b8f25d"
+checksum = "6717cf23b488adf64b9d711329542ba34de147df262370221940dfabc2c91358"
 dependencies = [
  "bytemuck",
- "font-types 0.9.0",
+ "core_maths",
+ "font-types 0.10.1",
 ]
 
 [[package]]
 name = "read-fonts"
-version = "0.33.1"
+version = "0.37.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50ea612a55c08586a1d15134be8a776186c440c312ebda3b9e8efbfe4255b7f4"
+checksum = "7b634fabf032fab15307ffd272149b622260f55974d9fad689292a5d33df02e5"
 dependencies = [
  "bytemuck",
- "font-types 0.9.0",
+ "font-types 0.11.3",
 ]
 
 [[package]]
 name = "read-fonts"
-version = "0.37.0"
+version = "0.41.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b634fabf032fab15307ffd272149b622260f55974d9fad689292a5d33df02e5"
+checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709"
 dependencies = [
  "bytemuck",
- "font-types 0.11.3",
+ "font-types 0.12.2",
+ "once_cell",
 ]
 
+[[package]]
+name = "reborrow"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430"
+
 [[package]]
 name = "redox_syscall"
 version = "0.2.16"
@@ -5282,16 +5970,16 @@ version = "0.5.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
 ]
 
 [[package]]
 name = "redox_syscall"
-version = "0.8.1"
+version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b44b894f2a6e36457d665d1e08c3866add6ed5e70050c1b4ba8a8ddedb02ce7"
+checksum = "c5102a6aaa05aa011a238e178e6bca86d2cb56fc9f586d37cb80f5bca6e07759"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
 ]
 
 [[package]]
@@ -5313,14 +6001,14 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
 dependencies = [
  "getrandom 0.2.17",
  "libredox",
- "thiserror 2.0.18",
+ "thiserror 2.0.19",
 ]
 
 [[package]]
 name = "regex"
-version = "1.12.3"
+version = "1.13.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
+checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
 dependencies = [
  "aho-corasick",
  "memchr",
@@ -5330,9 +6018,9 @@ dependencies = [
 
 [[package]]
 name = "regex-automata"
-version = "0.4.14"
+version = "0.4.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
+checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad"
 dependencies = [
  "aho-corasick",
  "memchr",
@@ -5341,9 +6029,9 @@ dependencies = [
 
 [[package]]
 name = "regex-syntax"
-version = "0.8.10"
+version = "0.8.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
 
 [[package]]
 name = "renderdoc-sys"
@@ -5409,13 +6097,14 @@ checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"
 [[package]]
 name = "rimay-localize"
 version = "0.1.0"
-source = "git+https://gitea.tawasuyu.net/sergio/gioser.git?tag=v0.1.0#692b40336c1e9cb12ab561764d6943cb65905ab3"
+source = "git+https://git.tawasuyu.net/tawasuyu/tawasuyu.git?tag=v0.2.0#a446bb3a73a0e022158cc0ec7deb65173859644b"
 dependencies = [
+ "directories",
  "fluent-bundle",
  "once_cell",
  "parking_lot 0.12.5",
  "sys-locale",
- "thiserror 2.0.18",
+ "thiserror 2.0.19",
  "tracing",
  "unic-langid",
 ]
@@ -5466,9 +6155,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
 
 [[package]]
 name = "rustc-hash"
-version = "2.1.2"
+version = "2.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
+checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
 
 [[package]]
 name = "rustc_version"
@@ -5494,7 +6183,7 @@ version = "0.38.44"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
  "errno",
  "libc",
  "linux-raw-sys 0.4.15",
@@ -5507,7 +6196,7 @@ version = "1.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
  "errno",
  "libc",
  "linux-raw-sys 0.12.1",
@@ -5516,9 +6205,9 @@ dependencies = [
 
 [[package]]
 name = "rustls"
-version = "0.23.40"
+version = "0.23.42"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
+checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138"
 dependencies = [
  "log",
  "once_cell",
@@ -5531,9 +6220,9 @@ dependencies = [
 
 [[package]]
 name = "rustls-pki-types"
-version = "1.14.1"
+version = "1.15.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
+checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
 dependencies = [
  "web-time",
  "zeroize",
@@ -5552,9 +6241,9 @@ dependencies = [
 
 [[package]]
 name = "rustversion"
-version = "1.0.22"
+version = "1.0.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
 
 [[package]]
 name = "rw-stream-sink"
@@ -5622,7 +6311,7 @@ version = "3.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
  "core-foundation 0.10.1",
  "core-foundation-sys",
  "libc",
@@ -5645,14 +6334,14 @@ version = "0.10.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e14e4d63b804dc0c7ec4a1e52bcb63f02c7ac94476755aa579edac21e01f915d"
 dependencies = [
- "self_cell 1.2.2",
+ "self_cell 1.3.0",
 ]
 
 [[package]]
 name = "self_cell"
-version = "1.2.2"
+version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89"
+checksum = "2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813"
 
 [[package]]
 name = "semver"
@@ -5662,39 +6351,48 @@ checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
 
 [[package]]
 name = "serde"
-version = "1.0.228"
+version = "1.0.229"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
 dependencies = [
  "serde_core",
  "serde_derive",
 ]
 
+[[package]]
+name = "serde-big-array"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11fc7cc2c76d73e0f27ee52abbd64eec84d46f370c88371120433196934e4b7f"
+dependencies = [
+ "serde",
+]
+
 [[package]]
 name = "serde_core"
-version = "1.0.228"
+version = "1.0.229"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.228"
+version = "1.0.229"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 3.0.3",
 ]
 
 [[package]]
 name = "serde_json"
-version = "1.0.150"
+version = "1.0.151"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
+checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
 dependencies = [
  "itoa",
  "memchr",
@@ -5703,6 +6401,17 @@ dependencies = [
  "zmij",
 ]
 
+[[package]]
+name = "serde_repr"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.3",
+]
+
 [[package]]
 name = "serde_spanned"
 version = "0.6.9"
@@ -5753,7 +6462,7 @@ checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
 [[package]]
 name = "shuma-discern"
 version = "0.1.0"
-source = "git+https://gitea.tawasuyu.net/sergio/gioser.git?tag=v0.1.0#692b40336c1e9cb12ab561764d6943cb65905ab3"
+source = "git+https://git.tawasuyu.net/tawasuyu/tawasuyu.git?tag=v0.2.0#a446bb3a73a0e022158cc0ec7deb65173859644b"
 dependencies = [
  "card-core",
  "serde_json",
@@ -5781,15 +6490,15 @@ dependencies = [
 
 [[package]]
 name = "simd-adler32"
-version = "0.3.9"
+version = "0.3.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
+checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
 
 [[package]]
 name = "simd_cesu8"
-version = "1.1.1"
+version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33"
+checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520"
 dependencies = [
  "rustc_version",
  "simdutf8",
@@ -5810,34 +6519,40 @@ version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
 
+[[package]]
+name = "siphasher"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
+
 [[package]]
 name = "skrifa"
-version = "0.31.3"
+version = "0.37.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dbeb4ca4399663735553a09dd17ce7e49a0a0203f03b706b39628c4d913a8607"
+checksum = "8c31071dedf532758ecf3fed987cdb4bd9509f900e026ab684b4ecb81ea49841"
 dependencies = [
  "bytemuck",
- "read-fonts 0.29.3",
+ "read-fonts 0.35.0",
 ]
 
 [[package]]
 name = "skrifa"
-version = "0.35.0"
+version = "0.40.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "576e60c7de4bb6a803a0312f9bef17e78cf1e8d25a80e1ade76770d7a0237955"
+checksum = "7fbdfe3d2475fbd7ddd1f3e5cf8288a30eb3e5f95832829570cd88115a7434ac"
 dependencies = [
  "bytemuck",
- "read-fonts 0.33.1",
+ "read-fonts 0.37.0",
 ]
 
 [[package]]
 name = "skrifa"
-version = "0.40.0"
+version = "0.44.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7fbdfe3d2475fbd7ddd1f3e5cf8288a30eb3e5f95832829570cd88115a7434ac"
+checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68"
 dependencies = [
  "bytemuck",
- "read-fonts 0.37.0",
+ "read-fonts 0.41.0",
 ]
 
 [[package]]
@@ -5873,9 +6588,9 @@ dependencies = [
 
 [[package]]
 name = "smallvec"
-version = "1.15.1"
+version = "1.15.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
 
 [[package]]
 name = "smithay-client-toolkit"
@@ -5883,7 +6598,7 @@ version = "0.19.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
  "calloop",
  "calloop-wayland-source",
  "cursor-icon",
@@ -5940,9 +6655,9 @@ dependencies = [
 
 [[package]]
 name = "socket2"
-version = "0.6.4"
+version = "0.6.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
+checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
 dependencies = [
  "libc",
  "windows-sys 0.61.2",
@@ -5961,9 +6676,9 @@ dependencies = [
 
 [[package]]
 name = "spin"
-version = "0.9.8"
+version = "0.9.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e"
 dependencies = [
  "lock_api",
 ]
@@ -5974,7 +6689,7 @@ version = "0.3.0+sdk-1.3.268.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
 ]
 
 [[package]]
@@ -6023,28 +6738,6 @@ version = "0.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
 
-[[package]]
-name = "strum"
-version = "0.26.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
-dependencies = [
- "strum_macros",
-]
-
-[[package]]
-name = "strum_macros"
-version = "0.26.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
-dependencies = [
- "heck",
- "proc-macro2",
- "quote",
- "rustversion",
- "syn",
-]
-
 [[package]]
 name = "subtle"
 version = "2.6.1"
@@ -6059,20 +6752,31 @@ checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb"
 
 [[package]]
 name = "swash"
-version = "0.2.7"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c2499c2d826531388872b2268718aed907a39bd785ab0dcfe57fab26283f92e"
+dependencies = [
+ "skrifa 0.44.0",
+ "yazi",
+ "zeno",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.119"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "842f3cd369c2ba38966204f983eaa5e54a8e84a7d7159ed36ade2b6c335aae64"
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
 dependencies = [
- "skrifa 0.40.0",
- "yazi",
- "zeno",
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
 ]
 
 [[package]]
 name = "syn"
-version = "2.0.117"
+version = "3.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -6096,7 +6800,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
@@ -6114,7 +6818,7 @@ version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
  "core-foundation 0.9.4",
  "system-configuration-sys",
 ]
@@ -6165,7 +6869,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
 dependencies = [
  "fastrand",
- "getrandom 0.4.2",
+ "getrandom 0.4.3",
  "once_cell",
  "rustix 1.1.4",
  "windows-sys 0.61.2",
@@ -6191,11 +6895,11 @@ dependencies = [
 
 [[package]]
 name = "thiserror"
-version = "2.0.18"
+version = "2.0.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9"
 dependencies = [
- "thiserror-impl 2.0.18",
+ "thiserror-impl 2.0.19",
 ]
 
 [[package]]
@@ -6206,25 +6910,25 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "2.0.18"
+version = "2.0.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 3.0.3",
 ]
 
 [[package]]
 name = "thread_local"
-version = "1.1.9"
+version = "1.1.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
+checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070"
 dependencies = [
  "cfg-if",
 ]
@@ -6245,12 +6949,11 @@ dependencies = [
 
 [[package]]
 name = "time"
-version = "0.3.47"
+version = "0.3.54"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
+checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244"
 dependencies = [
  "deranged",
- "itoa",
  "num-conv",
  "powerfmt",
  "serde_core",
@@ -6260,15 +6963,15 @@ dependencies = [
 
 [[package]]
 name = "time-core"
-version = "0.1.8"
+version = "0.1.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
+checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
 
 [[package]]
 name = "time-macros"
-version = "0.2.27"
+version = "0.2.32"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
+checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
 dependencies = [
  "num-conv",
  "time-core",
@@ -6299,15 +7002,6 @@ dependencies = [
  "strict-num",
 ]
 
-[[package]]
-name = "tinystr"
-version = "0.7.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
-dependencies = [
- "displaydoc",
-]
-
 [[package]]
 name = "tinystr"
 version = "0.8.3"
@@ -6321,9 +7015,9 @@ dependencies = [
 
 [[package]]
 name = "tinyvec"
-version = "1.11.0"
+version = "1.12.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
+checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f"
 dependencies = [
  "tinyvec_macros",
 ]
@@ -6353,7 +7047,7 @@ dependencies = [
  "monostate",
  "onig",
  "paste",
- "rand 0.9.4",
+ "rand 0.9.5",
  "rayon",
  "rayon-cond",
  "regex",
@@ -6361,7 +7055,7 @@ dependencies = [
  "serde",
  "serde_json",
  "spm_precompiled",
- "thiserror 2.0.18",
+ "thiserror 2.0.19",
  "unicode-normalization-alignments",
  "unicode-segmentation",
  "unicode_categories",
@@ -6369,30 +7063,30 @@ dependencies = [
 
 [[package]]
 name = "tokio"
-version = "1.52.3"
+version = "1.53.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
+checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
 dependencies = [
  "bytes",
  "libc",
- "mio 1.2.1",
+ "mio 1.2.2",
  "parking_lot 0.12.5",
  "pin-project-lite",
  "signal-hook-registry",
- "socket2 0.6.4",
+ "socket2 0.6.5",
  "tokio-macros",
  "windows-sys 0.61.2",
 ]
 
 [[package]]
 name = "tokio-macros"
-version = "2.7.0"
+version = "2.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
+checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
@@ -6417,14 +7111,15 @@ dependencies = [
 
 [[package]]
 name = "tokio-util"
-version = "0.7.18"
+version = "0.7.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
+checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
 dependencies = [
  "bytes",
  "futures-core",
  "futures-io",
  "futures-sink",
+ "libc",
  "pin-project-lite",
  "tokio",
 ]
@@ -6475,14 +7170,14 @@ dependencies = [
 
 [[package]]
 name = "toml_edit"
-version = "0.25.12+spec-1.1.0"
+version = "0.25.13+spec-1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7"
+checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b"
 dependencies = [
  "indexmap",
  "toml_datetime 1.1.1+spec-1.1.0",
  "toml_parser",
- "winnow 1.0.3",
+ "winnow 1.0.4",
 ]
 
 [[package]]
@@ -6491,7 +7186,7 @@ version = "1.1.2+spec-1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
 dependencies = [
- "winnow 1.0.3",
+ "winnow 1.0.4",
 ]
 
 [[package]]
@@ -6521,7 +7216,7 @@ version = "0.6.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
  "bytes",
  "futures-util",
  "http",
@@ -6551,6 +7246,7 @@ version = "0.1.44"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
 dependencies = [
+ "log",
  "pin-project-lite",
  "tracing-attributes",
  "tracing-core",
@@ -6564,7 +7260,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
@@ -6606,6 +7302,17 @@ dependencies = [
  "tracing-log",
 ]
 
+[[package]]
+name = "tree_magic_mini"
+version = "3.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6"
+dependencies = [
+ "memchr",
+ "nom 8.0.0",
+ "petgraph",
+]
+
 [[package]]
 name = "try-lock"
 version = "0.2.5"
@@ -6624,7 +7331,7 @@ version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90"
 dependencies = [
- "rustc-hash 2.1.2",
+ "rustc-hash 2.1.3",
 ]
 
 [[package]]
@@ -6633,6 +7340,17 @@ version = "1.20.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
 
+[[package]]
+name = "uds_windows"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
+dependencies = [
+ "memoffset",
+ "tempfile",
+ "windows-sys 0.61.2",
+]
+
 [[package]]
 name = "uint"
 version = "0.10.0"
@@ -6651,7 +7369,7 @@ version = "1.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe"
 dependencies = [
- "rand 0.9.4",
+ "rand 0.9.5",
  "serde",
  "web-time",
 ]
@@ -6672,7 +7390,7 @@ version = "0.9.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658"
 dependencies = [
- "tinystr 0.8.3",
+ "tinystr",
 ]
 
 [[package]]
@@ -6682,7 +7400,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d5957eb82e346d7add14182a3315a7e298f04e1ba4baac36f7f0dbfedba5fc25"
 dependencies = [
  "proc-macro-hack",
- "tinystr 0.8.3",
+ "tinystr",
  "unic-langid-impl",
  "unic-langid-macros-impl",
 ]
@@ -6695,7 +7413,7 @@ checksum = "a1249a628de3ad34b821ecb1001355bca3940bcb2f88558f1a8bd82e977f75b5"
 dependencies = [
  "proc-macro-hack",
  "quote",
- "syn",
+ "syn 2.0.119",
  "unic-langid-impl",
 ]
 
@@ -6720,12 +7438,6 @@ version = "1.13.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
 
-[[package]]
-name = "unicode-width"
-version = "0.1.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
-
 [[package]]
 name = "unicode-width"
 version = "0.2.2"
@@ -6812,12 +7524,13 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
 
 [[package]]
 name = "uuid"
-version = "1.23.2"
+version = "1.24.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7"
+checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239"
 dependencies = [
- "getrandom 0.4.2",
+ "getrandom 0.4.3",
  "js-sys",
+ "serde_core",
  "wasm-bindgen",
 ]
 
@@ -6846,18 +7559,18 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
 
 [[package]]
 name = "vello"
-version = "0.5.1"
+version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fa3f8a53870a2ee699ce05b738a3f9974c92c35ed4874de86052ac68d214811c"
+checksum = "72fef40773530322d5c2ffe3c1107e9874bd8239ac137d1c2b6c1edad695146e"
 dependencies = [
  "bytemuck",
  "futures-intrusive",
  "log",
  "peniko",
  "png 0.17.16",
- "skrifa 0.35.0",
+ "skrifa 0.40.0",
  "static_assertions",
- "thiserror 2.0.18",
+ "thiserror 2.0.19",
  "vello_encoding",
  "vello_shaders",
  "wgpu",
@@ -6865,26 +7578,27 @@ dependencies = [
 
 [[package]]
 name = "vello_encoding"
-version = "0.5.1"
+version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c69b0fe94b0ac7e47619c504ee2c377355174f5c46353c46d03fa5f7e435922b"
+checksum = "24c91203ec4b483440614a9a5c7c2d991932af72c5349659a63ec49476f0b79c"
 dependencies = [
  "bytemuck",
  "guillotiere",
  "peniko",
- "skrifa 0.35.0",
+ "skrifa 0.40.0",
  "smallvec",
 ]
 
 [[package]]
 name = "vello_shaders"
-version = "0.5.1"
+version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b2ebea426bb2f95b7610bca09178b03d809ede1d3c500a9acf6eca43e8f200be"
+checksum = "7a765d44d4bd354146e44f9a860f4e92effd91a97302549be9e47f0a18d8128c"
 dependencies = [
  "bytemuck",
+ "log",
  "naga",
- "thiserror 2.0.18",
+ "thiserror 2.0.19",
  "vello_encoding",
 ]
 
@@ -6921,27 +7635,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
 
 [[package]]
 name = "wasip2"
-version = "1.0.3+wasi-0.2.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
-dependencies = [
- "wit-bindgen 0.57.1",
-]
-
-[[package]]
-name = "wasip3"
-version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
+version = "1.0.4+wasi-0.2.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
+checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
 dependencies = [
- "wit-bindgen 0.51.0",
+ "wit-bindgen",
 ]
 
 [[package]]
 name = "wasm-bindgen"
-version = "0.2.122"
+version = "0.2.126"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409"
+checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
 dependencies = [
  "cfg-if",
  "once_cell",
@@ -6952,9 +7657,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-futures"
-version = "0.4.72"
+version = "0.4.76"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f"
+checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d"
 dependencies = [
  "js-sys",
  "wasm-bindgen",
@@ -6962,9 +7667,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro"
-version = "0.2.122"
+version = "0.2.126"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6"
+checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
 dependencies = [
  "quote",
  "wasm-bindgen-macro-support",
@@ -6972,48 +7677,26 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro-support"
-version = "0.2.122"
+version = "0.2.126"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e"
+checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
 dependencies = [
  "bumpalo",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
  "wasm-bindgen-shared",
 ]
 
 [[package]]
 name = "wasm-bindgen-shared"
-version = "0.2.122"
+version = "0.2.126"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437"
+checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
 dependencies = [
  "unicode-ident",
 ]
 
-[[package]]
-name = "wasm-encoder"
-version = "0.244.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
-dependencies = [
- "leb128fmt",
- "wasmparser 0.244.0",
-]
-
-[[package]]
-name = "wasm-metadata"
-version = "0.244.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
-dependencies = [
- "anyhow",
- "indexmap",
- "wasm-encoder",
- "wasmparser 0.244.0",
-]
-
 [[package]]
 name = "wasm-streams"
 version = "0.4.2"
@@ -7033,19 +7716,7 @@ version = "0.230.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "808198a69b5a0535583370a51d459baa14261dfab04800c4864ee9e1a14346ed"
 dependencies = [
- "bitflags 2.12.1",
- "indexmap",
- "semver",
-]
-
-[[package]]
-name = "wasmparser"
-version = "0.244.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
-dependencies = [
- "bitflags 2.12.1",
- "hashbrown 0.15.5",
+ "bitflags 2.13.1",
  "indexmap",
  "semver",
 ]
@@ -7053,21 +7724,21 @@ dependencies = [
 [[package]]
 name = "wawa-config"
 version = "0.1.0"
-source = "git+https://gitea.tawasuyu.net/sergio/gioser.git?tag=v0.1.0#692b40336c1e9cb12ab561764d6943cb65905ab3"
+source = "git+https://git.tawasuyu.net/tawasuyu/tawasuyu.git?tag=v0.2.0#a446bb3a73a0e022158cc0ec7deb65173859644b"
 dependencies = [
  "directories",
  "notify",
  "serde",
  "serde_json",
- "thiserror 2.0.18",
+ "thiserror 2.0.19",
  "tracing",
 ]
 
 [[package]]
 name = "wayland-backend"
-version = "0.3.15"
+version = "0.3.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d"
+checksum = "016ccf01d1c58b6f8999612813e17c9b2390f7d70671428869913310f83f54b8"
 dependencies = [
  "cc",
  "downcast-rs",
@@ -7079,11 +7750,11 @@ dependencies = [
 
 [[package]]
 name = "wayland-client"
-version = "0.31.14"
+version = "0.31.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144"
+checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
  "rustix 1.1.4",
  "wayland-backend",
  "wayland-scanner",
@@ -7095,7 +7766,7 @@ version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
  "cursor-icon",
  "wayland-backend",
 ]
@@ -7113,11 +7784,11 @@ dependencies = [
 
 [[package]]
 name = "wayland-protocols"
-version = "0.32.12"
+version = "0.32.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f"
+checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
  "wayland-backend",
  "wayland-client",
  "wayland-scanner",
@@ -7129,7 +7800,7 @@ version = "0.3.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
  "wayland-backend",
  "wayland-client",
  "wayland-protocols",
@@ -7142,7 +7813,7 @@ version = "0.3.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
  "wayland-backend",
  "wayland-client",
  "wayland-protocols",
@@ -7151,9 +7822,9 @@ dependencies = [
 
 [[package]]
 name = "wayland-scanner"
-version = "0.31.10"
+version = "0.31.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a"
+checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0"
 dependencies = [
  "proc-macro2",
  "quick-xml",
@@ -7174,9 +7845,9 @@ dependencies = [
 
 [[package]]
 name = "web-sys"
-version = "0.3.99"
+version = "0.3.103"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436"
+checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141"
 dependencies = [
  "js-sys",
  "wasm-bindgen",
@@ -7198,14 +7869,14 @@ version = "0.26.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
 dependencies = [
- "webpki-roots 1.0.7",
+ "webpki-roots 1.0.9",
 ]
 
 [[package]]
 name = "webpki-roots"
-version = "1.0.7"
+version = "1.0.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d"
+checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
 dependencies = [
  "rustls-pki-types",
 ]
@@ -7218,18 +7889,21 @@ checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
 
 [[package]]
 name = "wgpu"
-version = "24.0.5"
+version = "27.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6b0b3436f0729f6cdf2e6e9201f3d39dc95813fad61d826c1ed07918b4539353"
+checksum = "bfe68bac7cde125de7a731c3400723cadaaf1703795ad3f4805f187459cd7a77"
 dependencies = [
  "arrayvec",
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
+ "cfg-if",
  "cfg_aliases",
  "document-features",
+ "hashbrown 0.16.1",
  "js-sys",
  "log",
  "naga",
  "parking_lot 0.12.5",
+ "portable-atomic",
  "profiling",
  "raw-window-handle",
  "smallvec",
@@ -7244,49 +7918,85 @@ dependencies = [
 
 [[package]]
 name = "wgpu-core"
-version = "24.0.5"
+version = "27.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f0aa306497a238d169b9dc70659105b4a096859a34894544ca81719242e1499"
+checksum = "27a75de515543b1897b26119f93731b385a19aea165a1ec5f0e3acecc229cae7"
 dependencies = [
  "arrayvec",
+ "bit-set",
  "bit-vec",
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
+ "bytemuck",
  "cfg_aliases",
  "document-features",
+ "hashbrown 0.16.1",
  "indexmap",
  "log",
  "naga",
  "once_cell",
  "parking_lot 0.12.5",
+ "portable-atomic",
  "profiling",
  "raw-window-handle",
  "rustc-hash 1.1.0",
  "smallvec",
- "thiserror 2.0.18",
+ "thiserror 2.0.19",
+ "wgpu-core-deps-apple",
+ "wgpu-core-deps-emscripten",
+ "wgpu-core-deps-windows-linux-android",
  "wgpu-hal",
  "wgpu-types",
 ]
 
+[[package]]
+name = "wgpu-core-deps-apple"
+version = "27.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0772ae958e9be0c729561d5e3fd9a19679bcdfb945b8b1a1969d9bfe8056d233"
+dependencies = [
+ "wgpu-hal",
+]
+
+[[package]]
+name = "wgpu-core-deps-emscripten"
+version = "27.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b06ac3444a95b0813ecfd81ddb2774b66220b264b3e2031152a4a29fda4da6b5"
+dependencies = [
+ "wgpu-hal",
+]
+
+[[package]]
+name = "wgpu-core-deps-windows-linux-android"
+version = "27.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71197027d61a71748e4120f05a9242b2ad142e3c01f8c1b47707945a879a03c3"
+dependencies = [
+ "wgpu-hal",
+]
+
 [[package]]
 name = "wgpu-hal"
-version = "24.0.4"
+version = "27.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f112f464674ca69f3533248508ee30cb84c67cf06c25ff6800685f5e0294e259"
+checksum = "5b21cb61c57ee198bc4aff71aeadff4cbb80b927beb912506af9c780d64313ce"
 dependencies = [
  "android_system_properties",
  "arrayvec",
  "ash",
  "bit-set",
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
  "block",
  "bytemuck",
+ "cfg-if",
  "cfg_aliases",
- "core-graphics-types",
+ "core-graphics-types 0.2.0",
  "glow",
  "glutin_wgl_sys",
  "gpu-alloc",
  "gpu-allocator",
  "gpu-descriptor",
+ "hashbrown 0.16.1",
  "js-sys",
  "khronos-egl",
  "libc",
@@ -7294,18 +8004,19 @@ dependencies = [
  "log",
  "metal",
  "naga",
- "ndk-sys 0.5.0+25.2.9519653",
+ "ndk-sys",
  "objc",
  "once_cell",
  "ordered-float",
  "parking_lot 0.12.5",
+ "portable-atomic",
+ "portable-atomic-util",
  "profiling",
  "range-alloc",
  "raw-window-handle",
  "renderdoc-sys",
- "rustc-hash 1.1.0",
  "smallvec",
- "thiserror 2.0.18",
+ "thiserror 2.0.19",
  "wasm-bindgen",
  "web-sys",
  "wgpu-types",
@@ -7315,13 +8026,15 @@ dependencies = [
 
 [[package]]
 name = "wgpu-types"
-version = "24.0.0"
+version = "27.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50ac044c0e76c03a0378e7786ac505d010a873665e2d51383dcff8dd227dc69c"
+checksum = "afdcf84c395990db737f2dd91628706cb31e86d72e53482320d368e52b5da5eb"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
+ "bytemuck",
  "js-sys",
  "log",
+ "thiserror 2.0.19",
  "web-sys",
 ]
 
@@ -7438,7 +8151,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
@@ -7449,7 +8162,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
@@ -7460,7 +8173,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
@@ -7471,7 +8184,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
@@ -7787,7 +8500,7 @@ dependencies = [
  "ahash",
  "android-activity",
  "atomic-waker",
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
  "block2",
  "bytemuck",
  "calloop",
@@ -7802,7 +8515,7 @@ dependencies = [
  "memmap2",
  "ndk",
  "objc2 0.5.2",
- "objc2-app-kit",
+ "objc2-app-kit 0.2.2",
  "objc2-foundation 0.2.2",
  "objc2-ui-kit",
  "orbclient",
@@ -7841,89 +8554,19 @@ dependencies = [
 
 [[package]]
 name = "winnow"
-version = "1.0.3"
+version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1"
+checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
 dependencies = [
  "memchr",
 ]
 
-[[package]]
-name = "wit-bindgen"
-version = "0.51.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
-dependencies = [
- "wit-bindgen-rust-macro",
-]
-
 [[package]]
 name = "wit-bindgen"
 version = "0.57.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
 
-[[package]]
-name = "wit-bindgen-core"
-version = "0.51.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
-dependencies = [
- "anyhow",
- "heck",
- "wit-parser 0.244.0",
-]
-
-[[package]]
-name = "wit-bindgen-rust"
-version = "0.51.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
-dependencies = [
- "anyhow",
- "heck",
- "indexmap",
- "prettyplease",
- "syn",
- "wasm-metadata",
- "wit-bindgen-core",
- "wit-component",
-]
-
-[[package]]
-name = "wit-bindgen-rust-macro"
-version = "0.51.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
-dependencies = [
- "anyhow",
- "prettyplease",
- "proc-macro2",
- "quote",
- "syn",
- "wit-bindgen-core",
- "wit-bindgen-rust",
-]
-
-[[package]]
-name = "wit-component"
-version = "0.244.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
-dependencies = [
- "anyhow",
- "bitflags 2.12.1",
- "indexmap",
- "log",
- "serde",
- "serde_derive",
- "serde_json",
- "wasm-encoder",
- "wasm-metadata",
- "wasmparser 0.244.0",
- "wit-parser 0.244.0",
-]
-
 [[package]]
 name = "wit-parser"
 version = "0.230.0"
@@ -7939,33 +8582,27 @@ dependencies = [
  "serde_derive",
  "serde_json",
  "unicode-xid",
- "wasmparser 0.230.0",
+ "wasmparser",
 ]
 
 [[package]]
-name = "wit-parser"
-version = "0.244.0"
+name = "wl-clipboard-rs"
+version = "0.9.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
+checksum = "e9651471a32e87d96ef3a127715382b2d11cc7c8bb9822ded8a7cc94072eb0a3"
 dependencies = [
- "anyhow",
- "id-arena",
- "indexmap",
+ "libc",
  "log",
- "semver",
- "serde",
- "serde_derive",
- "serde_json",
- "unicode-xid",
- "wasmparser 0.244.0",
+ "os_pipe",
+ "rustix 1.1.4",
+ "thiserror 2.0.19",
+ "tree_magic_mini",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-protocols",
+ "wayland-protocols-wlr",
 ]
 
-[[package]]
-name = "writeable"
-version = "0.5.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
-
 [[package]]
 name = "writeable"
 version = "0.6.3"
@@ -8029,7 +8666,7 @@ dependencies = [
  "nom 7.1.3",
  "oid-registry",
  "rusticata-macros",
- "thiserror 2.0.18",
+ "thiserror 2.0.19",
  "time",
 ]
 
@@ -8055,7 +8692,7 @@ version = "0.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5"
 dependencies = [
- "bitflags 2.12.1",
+ "bitflags 2.13.1",
  "dlib",
  "log",
  "once_cell",
@@ -8100,7 +8737,7 @@ dependencies = [
  "nohash-hasher",
  "parking_lot 0.12.5",
  "pin-project",
- "rand 0.8.6",
+ "rand 0.8.7",
  "static_assertions",
 ]
 
@@ -8115,7 +8752,7 @@ dependencies = [
  "nohash-hasher",
  "parking_lot 0.12.5",
  "pin-project",
- "rand 0.9.4",
+ "rand 0.9.5",
  "static_assertions",
  "web-time",
 ]
@@ -8135,6 +8772,17 @@ version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5"
 
+[[package]]
+name = "yeslogic-fontconfig-sys"
+version = "6.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d8b8abf912b9a29ff112e1671c97c33636903d13a69712037190e6805af4f76"
+dependencies = [
+ "dlib",
+ "once_cell",
+ "pkg-config",
+]
+
 [[package]]
 name = "yoke"
 version = "0.8.3"
@@ -8154,10 +8802,107 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
  "synstructure",
 ]
 
+[[package]]
+name = "zbus"
+version = "5.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe18fb60dc696039e738717b76eaea21e7a4489bbb1885020b43c94236d7e98a"
+dependencies = [
+ "async-broadcast",
+ "async-executor",
+ "async-io",
+ "async-lock",
+ "async-process",
+ "async-recursion",
+ "async-task",
+ "async-trait",
+ "blocking",
+ "enumflags2",
+ "event-listener",
+ "futures-core",
+ "futures-lite",
+ "hex",
+ "libc",
+ "ordered-stream",
+ "rustix 1.1.4",
+ "serde",
+ "serde_repr",
+ "tracing",
+ "uds_windows",
+ "uuid",
+ "windows-sys 0.61.2",
+ "winnow 1.0.4",
+ "zbus_macros",
+ "zbus_names",
+ "zvariant",
+]
+
+[[package]]
+name = "zbus-lockstep"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6998de05217a084b7578728a9443d04ea4cd80f2a0839b8d78770b76ccd45863"
+dependencies = [
+ "zbus_xml",
+ "zvariant",
+]
+
+[[package]]
+name = "zbus-lockstep-macros"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10da05367f3a7b7553c8cdf8fa91aee6b64afebe32b51c95177957efc47ca3a0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "zbus-lockstep",
+ "zbus_xml",
+ "zvariant",
+]
+
+[[package]]
+name = "zbus_macros"
+version = "5.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe96480bed92df2b442a1a30df364e12d08eed03aeb061f2b8dc6afb2be91119"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "zbus_names",
+ "zvariant",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zbus_names"
+version = "4.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e"
+dependencies = [
+ "serde",
+ "winnow 1.0.4",
+ "zvariant",
+]
+
+[[package]]
+name = "zbus_xml"
+version = "5.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1586c021a01ca0a9216dcd874e546382e156a5cbab5fab6cb5f10087e22682a"
+dependencies = [
+ "serde",
+ "winnow 1.0.4",
+ "zbus_names",
+ "zvariant",
+]
+
 [[package]]
 name = "zeno"
 version = "0.3.3"
@@ -8166,22 +8911,22 @@ checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524"
 
 [[package]]
 name = "zerocopy"
-version = "0.8.50"
+version = "0.8.55"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1"
+checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb"
 dependencies = [
  "zerocopy-derive",
 ]
 
 [[package]]
 name = "zerocopy-derive"
-version = "0.8.50"
+version = "0.8.55"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639"
+checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
@@ -8201,28 +8946,28 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
  "synstructure",
 ]
 
 [[package]]
 name = "zeroize"
-version = "1.8.2"
+version = "1.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
+checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
 dependencies = [
  "zeroize_derive",
 ]
 
 [[package]]
 name = "zeroize_derive"
-version = "1.4.3"
+version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e"
+checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
@@ -8256,14 +9001,14 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.119",
 ]
 
 [[package]]
 name = "zmij"
-version = "1.0.21"
+version = "1.0.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
 
 [[package]]
 name = "zune-core"
@@ -8288,3 +9033,43 @@ checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296"
 dependencies = [
  "zune-core",
 ]
+
+[[package]]
+name = "zvariant"
+version = "5.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bee2a0bcd2a907786a456fff45aaaaf54c9ba5f50b71ae9ec1a4edd200c94911"
+dependencies = [
+ "endi",
+ "enumflags2",
+ "serde",
+ "winnow 1.0.4",
+ "zvariant_derive",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zvariant_derive"
+version = "5.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38a708216a18780796770bfe3f4739c7c83a3e8f789b755534bbbc06e4e23e12"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zvariant_utils"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90cb9383f9b45290407a1258b202d3f8f01db719eb60b4e4055c6375af4fc7c7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde",
+ "syn 2.0.119",
+ "winnow 1.0.4",
+]
diff --git a/Cargo.toml b/Cargo.toml
index fbef7f2..60acebf 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,7 +1,13 @@
-# Cargo.toml raíz STANDALONE de chasqui — front-door sobre Llimphi.
-# Solo el código de chasqui; Llimphi y lo fundacional por git-dep del monorepo gioser.git.
+# Cargo.toml raíz STANDALONE de chasqui — front-door público.
+# GENERADO por scripts/actualizar-standalone.py desde el monorepo tawasuyu.git
+# (v0.2.0). No editar a mano: se regenera. Lo fundacional se consume por
+# git-dep al tag; sólo el código del dominio vive en este repo.
 [workspace]
 resolver = "2"
+
+# ============================================================
+# Cuadrantes — agregar a medida que se migran
+# ============================================================
 members = [
     "02_ruway/chasqui/card-admin",
     "02_ruway/chasqui/card-handshake",
@@ -16,6 +22,11 @@ members = [
     "02_ruway/chasqui/chasqui-nous-real",
 ]
 
+# `wawa/` se excluye del workspace global porque corre en target
+# `x86_64-unknown-none` y `panic = "abort"`, incompatibles con los
+# perfiles globales. Los crates compartidos se referencian por `path`
+# cruzando la frontera.
+
 [workspace.package]
 version = "0.1.0"
 edition = "2021"
@@ -26,9 +37,32 @@ publish = false
 repository = "https://git.tawasuyu.net/tawasuyu/chasqui"
 
 [workspace.dependencies]
-
+# === Configuración declarativa (vocabulario de esquemas) ===
+allichay = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+marca = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+# Telemetría local de desarrollo (stderr+panics+tracing → $XDG_STATE_HOME/tawasuyu/<dominio>/<bin>.log)
+bitacora = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+# default = [] (núcleo liviano sin vello); `bake` es opt-in. Declaramos
+# default-features = false aquí para que el `default-features = false` de los
+# consumidores (splash/greeter/compositor) se honre y no quede el warning.
+mirada-fondo = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+mirada-teclado-core = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+mirada-teclado-widget = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
 # === Registro de apps / menú global ===
-app-bus = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
+app-bus = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+app-iconset = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+# === Centro de eventos (notificaciones + capturas + clipboard + …) ===
+willay-core = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+willay-checkpoint = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+willay-hilo = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+willay-store = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+willay-emit = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+rag-motor = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+churay-core = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+churay-welcome = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+churay-welcome-runner = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+# === Puente de drag-and-drop compositor → app (suple winit en Wayland) ===
+drop-bridge = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
 # === Serialización ===
 serde = { version = "1", features = ["derive"] }
 serde_json = "1"
@@ -55,8 +89,12 @@ tracing = "0.1"
 tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
 
 # === Linux primitives (arje) ===
-nix = { version = "0.29", features = ["signal", "process", "sched", "mount", "fs", "socket", "net", "user"] }
+nix = { version = "0.29", features = ["signal", "process", "sched", "mount", "fs", "socket", "net", "user", "reboot"] }
 libc = "0.2"
+# DRM/KMS legacy (dumb buffer) para el splash nativo del arranque sin parpadeo.
+# Misma versión que reexporta smithay (mirada-compositor) — el lock ya la fija.
+drm = "0.14"
+font8x8 = { version = "0.3", default-features = false }
 
 # === IDs / Hash / Crypto ===
 ulid = { version = "1", features = ["serde"] }
@@ -70,10 +108,15 @@ argon2 = "0.5"
 rand = "0.8"
 
 # === WASM (arje) ===
-# wasmi 1.0: unifica la versión con renaser (su kernel ya corre 1.0), para
+# wasmi 1.0: unifica la versión con wawa (su kernel ya corre 1.0), para
 # que el ABI WASM del host sea idéntico en Linux y en bare-metal.
 wasmi = "1.0"
 wat = "1"
+# wasmtime 46 (Fase 1 del pipeline JS): tier 0 de puriy-js pasa de wasmi
+# (intérprete) a Wasmtime AOT (Cranelift → nativo) en Linux — quita la doble
+# interpretación de QuickJS.wasm. En wawa bare-metal el runtime es Pulley (spike
+# Q1 cerrado); mismo crate, distinto backend.
+wasmtime = "46"
 
 # === Storage / DB ===
 sled = "0.34"
@@ -88,6 +131,10 @@ bzip2 = "0.4"
 
 # === Compresión (minga multi-bundle) ===
 zstd = "0.13"
+xz2 = "0.1"
+# Contenedores adicionales del visor Archive de nahual (sólo listado).
+sevenz-rust = "0.6"
+unrar = "0.5"
 
 # === HTTP server (iniy-server) ===
 axum = "0.7"
@@ -97,7 +144,7 @@ tower = "0.5"
 instant-distance = "0.6"
 
 # === P2P (minga) ===
-libp2p = { version = "0.56", features = ["tokio", "tcp", "noise", "yamux", "macros", "kad", "identify", "relay", "dcutr", "autonat", "mdns"] }
+libp2p = { version = "0.56", features = ["tokio", "tcp", "dns", "noise", "yamux", "macros", "kad", "identify", "relay", "dcutr", "autonat", "mdns"] }
 libp2p-stream = "=0.4.0-alpha"
 libp2p-allow-block-list = "0.6"
 
@@ -112,7 +159,11 @@ libm = "0.2"
 midly = "0.5"
 
 # === Code parsing (minga) ===
-arboard = "3"
+# `wayland-data-control` trae el backend wl-clipboard-rs: bajo mirada (Wayland
+# propio, sin Xwayland para apps nativas) arboard-x11 fallaba a no-op silencioso
+# y el clipboard de los text-editor quedaba muerto. Con la feature habla
+# zwlr_data_control_manager_v1 (que mirada expone) y cae a X11 si no hay Wayland.
+arboard = { version = "3", features = ["wayland-data-control"] }
 ropey = "1.6"
 tree-sitter = "0.24"
 tree-sitter-rust = "0.23"
@@ -131,7 +182,19 @@ petgraph = "0.6"
 # default-features = false: nos quedamos con PNG + JPEG + WebP (lossless).
 # tullpu-render exporta a las tres; AVIF/TIFF/… los habilitamos si una app
 # los pide específicamente.
-image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp"] }
+# Decoders puro-Rust baratos; avif queda fuera (arrastra dav1d/rav1e).
+image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif", "bmp", "ico", "tiff", "tga", "qoi"] }
+# ONNX Runtime para los proveedores de píxel reales (pixel-verbo-onnx: segmentación
+# u2net/isnet, inpaint, restyle…). `download-binaries` (default) baja el runtime nativo
+# solo; ya lo arrastraba fastembed (rimay-verbo) transitivo — aquí lo declaramos directo.
+ort = "2.0.0-rc.9"
+
+# Rasterizador vectorial CPU (relleno/trazo anti-aliased) para capas vectoriales de tullpu.
+tiny-skia = "0.11"
+# Booleanos de polígonos robustos (union/intersection/difference/xor) para el
+# clipper vectorial de tullpu — restar/intersecar paths que se solapan
+# parcialmente, que la regla de relleno no expresa. Ya estaba en el lock.
+geo = "0.28"
 
 # === FUSE (minga-vfs) ===
 # default-features = false: prescinde de pkg-config/libfuse-dev en build.
@@ -153,91 +216,164 @@ tempfile = "3"
 
 # === Llimphi (motor gráfico soberano) ===
 # wgpu sobre Vulkan/Metal/DX12, winit para ventana en dev Linux.
-# raw-window-handle 0.6 alinea winit 0.30 con wgpu 24.
-# vello 0.5 = rasterizador vectorial sobre wgpu 24.
+# raw-window-handle 0.6 alinea winit 0.30 con wgpu 27.
+# vello 0.7 = rasterizador vectorial sobre wgpu 27 (renderer GPU "wgpu" + opt-in "hybrid" CPU+GPU).
 # taffy 0.9 = motor Flexbox/Grid puro Rust (ya pulled por transitivos, lo alineamos).
-# parley 0.2 = shaping/layout de texto compatible con peniko 0.4 (que vello 0.5 expone).
-wgpu = "24"
+# parley 0.6 = shaping/layout de texto compatible con peniko 0.6 (que vello 0.7 expone).
+wgpu = "27"
 winit = "0.30"
 raw-window-handle = "0.6"
 pollster = "0.4"
-vello = "0.5"
+vello = "0.7"
+# Renderer hybrid CPU+GPU sin compute shaders (mejor compat WebGL2/Adreno viejas).
+# Opt-in; el renderer "wgpu" sigue siendo el default.
+vello_hybrid = "0.0.9"
+# foreign-lottie = fork vendorizado de velato 0.9 (Lottie → vello::Scene),
+# completado para no paniquear ante features no soportadas. Lo consume
+# llimphi-lottie. Ver shared/foreign-lottie.
+foreign-lottie = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+# Árbol de accesibilidad para lectores de pantalla (NVDA/VoiceOver/Orca/TalkBack).
+# Lo consume el runtime de Llimphi (iter 2/3 del plan AccessKit); el modelo
+# `SemanticsSpec` del compositor es independiente de estas crates.
+accesskit = "0.24"
+accesskit_winit = "0.33"
 taffy = "0.9"
 # parley = shaping completo (bidi, ligatures, fallback CJK/emoji vía fontique, line break).
-parley = "0.4"
+parley = "0.6"
 # Bucle Elm (input→update→view→layout→raster→present). Lo consumen las apps.
-llimphi-ui = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
+llimphi-ui = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+# Tier 3 — apps WASM con UI Llimphi real. El guest pinta un WireNode (IR
+# serializable), el host (runner) lo materializa en View<Msg> y rebota los
+# eventos al update del guest. SDK = lado guest, runner = lado host.
+llimphi-wire-view = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-wasm-app-sdk = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-wasm-runner = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+# Núcleo puro de distribución (CAS + verificación + resolve), SIN runner/GPU.
+llimphi-wasm-core = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+# Cara con baterías: re-exporta core + puente al runner para correr la app.
+llimphi-wasm-dist = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-wasm-open = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-wasm-registry = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-wasm-wasi = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+# Transporte P2P del bytecode por hash sobre BrahmanNet (card-net).
+llimphi-wasm-net = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-3d = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+# Runner wlr-layer-shell: corre un `App` de Llimphi como barra anclada a un
+# borde (no como ventana), reusando la plumbing sctk+wgpu que pata probó.
+llimphi-layer = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
 # Paleta semántica compartida por las apps y los widgets.
-llimphi-theme = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
+llimphi-theme = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
 # Tweens y helpers de animación sobre el bucle Elm.
-llimphi-motion = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
+llimphi-motion = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+# Máquina de estados de animación (estilo Rive) clip-agnóstica. Núcleo puro;
+# el render lo cablea el consumidor (llimphi-lottie). Escalón sobre el playback.
+llimphi-anim = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+# Studio de animación «rive»: además del editor (bin), expone como lib los
+# documentos serializables (Doc + RigDoc + Project) que carga mirada-fondo.
+llimphi-anim-studio = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+# Render de mallas deformables (skel::Mesh) a vello Scene: vectorial y texturizada.
+llimphi-mesh = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
 # Iconos vectoriales (BezPath en grid 24×24) compartidos por todas las apps.
-llimphi-icons = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
+llimphi-icons = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+# Puente fino vello_svg → Llimphi para SVG arbitrario (íconos .desktop, logos).
+llimphi-svg = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+# Puente fino velato (Lottie) → Llimphi para animación vectorial autorada (.json).
+llimphi-lottie = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-image = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+# Decodificador de protocolos de gráficos de terminal (kitty/sixel) → RGBA.
+llimphi-term-graphics = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
 # Widgets reusables sobre llimphi-ui — uno por crate.
-llimphi-widget-app-header = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-widget-banner = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-widget-button = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-widget-card = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-clipboard = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-widget-context-menu = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-widget-edit-menu = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-widget-menubar = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-widget-list = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-widget-grid = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-widget-slider = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-widget-scroll = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-widget-splitter = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-widget-stat-card = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-widget-tabs = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-module-command-palette = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-module-diff-viewer = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-module-fif = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-module-file-picker = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-module-bookmarks = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-module-mini-map = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-module-shuma-term = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-module-symbol-outline = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-plugin-host = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-widget-theme-switcher = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-widget-text-area = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-widget-text-editor-core = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-widget-text-editor = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-widget-text-editor-lsp = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-widget-text-input = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-widget-tiled = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-widget-nodegraph = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-widget-tree = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-widget-navigator = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
+llimphi-widget-app-header = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-banner = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-button = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-rive-button = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-card = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-clipboard = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-context-menu = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-edit-menu = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-menubar = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-list = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-grid = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-table = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-color-picker = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-slider = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-scroll = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-lazy-list = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-splitter = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-stat-card = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-tabs = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-module-allichay = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-module-command-palette = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-module-diff-viewer = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-module-fif = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-module-file-picker = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-module-bookmarks = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-module-mini-map = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-module-shuma-term = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-module-symbol-outline = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-plugin-host = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-theme-switcher = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-text-area = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-text-editor-core = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-text-editor = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-text-editor-lsp = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-text-input = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-tiled = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-nodegraph = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-tree = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+grafo-nav = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-navigator = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-detail-table = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-select = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-terminal = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
 # Sello vectorial wawa (rombo + W implícita + Merkle Core).
-llimphi-widget-wawa-mark = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
+llimphi-widget-wawa-mark = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
 # Widgets de elegancia transversal (tooltip, spinner, progress, toast,
 # modal, empty, status-bar, shortcuts-help, splash).
-llimphi-widget-tooltip = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-widget-spinner = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-widget-progress = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-widget-toast = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-widget-modal = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-widget-empty = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-widget-status-bar = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-widget-shortcuts-help = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-widget-timeline = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-widget-splash = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
+llimphi-widget-tooltip = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-spinner = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-progress = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-toast = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-modal = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-empty = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-status-bar = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-shortcuts-help = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-timeline = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-transport = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-waveform = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-splash = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
 # Controles de formulario y signaling (switch, segmented, breadcrumb,
 # badge, avatar, skeleton, field).
-llimphi-widget-switch = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-widget-segmented = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-widget-dock-rail = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-widget-breadcrumb = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-widget-badge = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-widget-avatar = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-widget-skeleton = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-widget-field = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
+llimphi-widget-switch = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-segmented = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-rag-sidebar = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-dock-rail = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-toolbar = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-breadcrumb = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-badge = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-avatar = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-skeleton = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-field = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
 # Firma visual transversal (gradient sutil + hairline accent).
-llimphi-widget-panel = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-widget-panes = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-llimphi-workspace = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
+llimphi-widget-panel = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-panes = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+# Widgets Flutter-like — composición sobre primitivas Tier 1/3 ya
+# expuestas (sombra, gradient, animated, ripple).
+llimphi-widget-chip = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-fab = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-wrap = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-range-slider = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-calendar = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-fitted-box = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-carousel = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-rating = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-gauge = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-scaffold = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-hero = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-widget-router = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+llimphi-workspace = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
 # Abstracción Selector — host (paths) + wawa (khipus).
-llimphi-module-selector = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
+llimphi-module-selector = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
 
 # === Filesystem helpers ===
 directories = "5"
@@ -271,7 +407,7 @@ hex = "0.4"
 portable-pty = "0.9"
 vt100 = "0.16"
 
-# === WASM web (gioser) ===
+# === WASM web (tawasuyu) ===
 wasm-bindgen = "0.2"
 wasm-bindgen-futures = "0.4"
 js-sys = "0.3"
@@ -296,61 +432,92 @@ ttf-parser = "0.25"
 # ============================================================
 # Intra-workspace deps de nahual (referenciadas por workspace = true)
 # ============================================================
-nahual-text-viewer-llimphi = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-nahual-image-viewer-llimphi = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-nahual-thumb-core = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-nahual-gallery-llimphi = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-nahual-video-viewer-llimphi = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-nahual-card-viewer-llimphi = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-nahual-audio-viewer-llimphi = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-nahual-tree-viewer-llimphi = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-nahual-hex-viewer-llimphi = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-nahual-table-viewer-llimphi = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-nahual-markdown-viewer-llimphi = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-nahual-archive-viewer-llimphi = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-nahual-font-viewer-llimphi = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-nahual-map-viewer-llimphi = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-nahual-geo-core = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-nahual-viewer-core = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-nahual-file-explorer-llimphi = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
+nahual-text-viewer-llimphi = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+nahual-image-viewer-llimphi = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+nahual-pdf-viewer-llimphi = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+nahual-cbz-viewer-llimphi = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+nahual-thumb-core = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+nahual-gallery-llimphi = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+nahual-video-viewer-llimphi = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+nahual-card-viewer-llimphi = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+nahual-audio-viewer-llimphi = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+media-module = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+tullpu-module = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+tullpu-core = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+tullpu-icon-core = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+tullpu-icon-cli = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+tullpu-icon-llimphi = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+tullpu-icon-llm = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+foreign-svg = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+foreign-pdf = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+tullpu-render = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+tullpu-render-gpu = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+tullpu-ops = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+tullpu-paint = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+nahual-tree-viewer-llimphi = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+nahual-hex-viewer-llimphi = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+nahual-svg-viewer-llimphi = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+nahual-sheet-viewer-llimphi = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+nahual-dbf-viewer-llimphi = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+nahual-table-viewer-llimphi = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+nahual-markdown-viewer-llimphi = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+nahual-pluma-viewer-llimphi = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+nahual-cotejo-llimphi = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+nahual-deck-viewer-llimphi = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+nahual-archive-viewer-llimphi = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+nahual-font-viewer-llimphi = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+nahual-map-viewer-llimphi = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+nahual-geo-core = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+nahual-geo-voxel = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+nahual-iconos = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+nahual-viewer-core = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+nahual-file-explorer-llimphi = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
 
 # ============================================================
 # Intra-workspace deps de pineal (módulo de gráficos)
 # ============================================================
-pineal-core = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-pineal-render = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-pineal-cartesian = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-pineal-stream = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-pineal-mesh = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-pineal-financial = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-pineal-polar = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-pineal-heatmap = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-pineal-treemap = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-pineal-flow = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-pineal-phosphor = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-pineal-export = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-pineal-hexbin = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-pineal-contour = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-pineal-bars = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-pineal = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
+pineal-core = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+pineal-render = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+pineal-cartesian = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+pineal-stream = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+pineal-mesh = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+pineal-financial = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+pineal-polar = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+pineal-heatmap = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+pineal-treemap = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+pineal-flow = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+pineal-phosphor = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+pineal-export = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+pineal-hexbin = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+pineal-contour = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+pineal-bars = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+pineal = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
 
 # ============================================================
 # Intra-workspace deps de iniy (laboratorio semántico de creencias)
 # ============================================================
-iniy-core = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-iniy-ingest = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-iniy-extract = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-iniy-nli = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-iniy-nli-llm = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-iniy-graph = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-iniy-store = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
+iniy-core = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+iniy-evidencia = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+iniy-derive = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+iniy-emisores = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+iniy-cache = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+iniy-daemon = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+iniy-ingest = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+iniy-extract = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+iniy-nli = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+iniy-nli-llm = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+iniy-graph = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+iniy-store = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+# la física de decaimiento de khipu la importa iniy-derive (SDD iniy §3)
+khipu-gravity = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
 
 # === auto: declarados por crates internos faltantes ===
-cosmos-coords = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-cosmos-core = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-cosmos-ephemeris = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-cosmos-time = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-cosmos-wcs = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
+cosmos-coords = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+cosmos-cities = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+cosmos-core = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+cosmos-ephemeris = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+cosmos-time = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+cosmos-wcs = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
 
 # === auto: externas de eternal ===
 celestial-eop-data = { version = "0.1"}
@@ -358,6 +525,7 @@ approx = "0.5"
 byteorder = "1.5"
 cc = "1.0"
 chrono = "0.4"
+chrono-tz = "0.10"
 crc32fast = "1.4"
 criterion = "0.5"
 csv = "1.4"
@@ -366,6 +534,11 @@ glob = "0.3"
 indicatif = "0.18"
 lz4_flex = "0.11"
 memmap2 = "0.9"
+# Cliente Wayland (lado cliente) — lo usa hapiy para hablar zwlr_screencopy
+# contra mirada (u otro compositor wlroots) y capturar la pantalla.
+wayland-client = "0.31"
+wayland-protocols-wlr = { version = "0.3", features = ["client"] }
+wayland-protocols-misc = { version = "0.3", features = ["client"] }
 mockito = "1.0"
 ndarray = "0.15"
 num-traits = "0.2"
@@ -399,6 +572,9 @@ markup5ever_rcdom = "0.39"
 cssparser = "0.35"
 url = "2"
 ureq = { version = "2", default-features = false, features = ["tls"] }
+# Charset de la web real: no todo es UTF-8 (google.com sirve ISO-8859-1).
+# Ya venía transitivo por el stack de Servo — aquí se hace explícito.
+encoding_rs = "0.8"
 
 # === takiy-synth (SoundFont MIDI) ===
 # rustysynth = sintetizador SF2 puro Rust, MIT. Reemplaza el oscilador
@@ -427,7 +603,7 @@ hound = "3.5"
 symphonia = { version = "0.5", default-features = false, features = ["mp3", "flac", "vorbis", "ogg"] }
 
 # === media-source-opus (decoder Opus NATIVO puro-Rust) ===
-# Opus es el formato de audio nativo de gioser (par del video AV1). ogg
+# Opus es el formato de audio nativo de tawasuyu (par del video AV1). ogg
 # demuxea las páginas Ogg; opus-wave es un port puro-Rust de libopus
 # (SILK+CELT, sin C ni FFI) — par del rav1d del lado video.
 ogg = "0.9"
@@ -438,11 +614,30 @@ opus-wave = "3"
 # paquetes de los tracks V_AV1 y A_OPUS para alimentar a media-source-av1
 # y media-source-opus — un .webm AV1+Opus se reproduce 100% nativo.
 matroska-demuxer = "0.7"
-# === git-deps al monorepo (agregados por la extracción) ===
-arje-cas = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-card-core = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-card-net = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-card-wit = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-rimay-localize = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-shuma-discern = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
-wawa-config = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.1.0" }
+
+# === Perfil `dist`: binarios de reparto livianos (equipos chicos / poca RAM) ===
+# Hereda de `release` pero prioriza HUELLA sobre tiempo de compilación: LTO thin
+# (poda código muerto entre crates), un solo codegen-unit (mejor inlining/DCE) y
+# `strip` de símbolos. Reduce el tamaño en disco Y el segmento de código mapeado
+# en RAM de cada binario (mirada, pata, apps Llimphi). No toca `cargo build
+# --release` de iteración diaria; se usa sólo al empaquetar:
+#   cargo build --profile dist -p mirada-compositor -p pata-host …
+# `panic = "unwind"` se mantiene a propósito: mirada-plugin-host aísla plugins
+# con `catch_unwind`, así que `abort` rompería el sandbox.
+# === Crates del monorepo consumidos por git-dep (pin del tag) ===
+arje-cas = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+card-core = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+card-net = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+card-wit = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+rimay-localize = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+shuma-discern = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+wawa-config = { git = "https://git.tawasuyu.net/tawasuyu/tawasuyu.git", tag = "v0.2.0" }
+
+[profile.dist]
+inherits = "release"
+lto = "thin"
+codegen-units = 1
+strip = "symbols"
+panic = "unwind"
+
+