diff --git a/Cargo.lock b/Cargo.lock
index 2b6d924..9383dcb 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3950,7 +3950,9 @@ version = "0.1.0"
dependencies = [
"gioser-canvas-web",
"js-sys",
+ "pluma-reader-web",
"wasm-bindgen",
+ "wasm-bindgen-futures",
"web-sys",
]
@@ -7995,6 +7997,24 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
+[[package]]
+name = "pluma-md"
+version = "0.1.0"
+dependencies = [
+ "pulldown-cmark",
+]
+
+[[package]]
+name = "pluma-reader-web"
+version = "0.1.0"
+dependencies = [
+ "js-sys",
+ "pluma-md",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
[[package]]
name = "png"
version = "0.17.16"
@@ -8291,6 +8311,24 @@ dependencies = [
"syn 1.0.109",
]
+[[package]]
+name = "pulldown-cmark"
+version = "0.12.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f86ba2052aebccc42cbbb3ed234b8b13ce76f75c3551a303cb2bcffcff12bb14"
+dependencies = [
+ "bitflags 2.11.1",
+ "memchr",
+ "pulldown-cmark-escape",
+ "unicase",
+]
+
+[[package]]
+name = "pulldown-cmark-escape"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
+
[[package]]
name = "pxfm"
version = "0.1.29"
diff --git a/Cargo.toml b/Cargo.toml
index b5165d8..eda49e7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -117,6 +117,12 @@ members = [
"crates/modules/gioser/gioser-shaders",
"crates/modules/gioser/gioser-canvas-web",
+ # ============================================================
+ # modules/pluma/ — markdown agnóstico + visor web elegante
+ # ============================================================
+ "crates/modules/pluma/pluma-md",
+ "crates/modules/pluma/pluma-reader-web",
+
# ============================================================
# apps/ — apps que consumen el protocolo (yahweh modules+shell)
# ============================================================
@@ -231,10 +237,14 @@ directories = "5"
# === WASM web (gioser) ===
wasm-bindgen = "0.2"
+wasm-bindgen-futures = "0.4"
js-sys = "0.3"
web-sys = "0.3"
glam = "0.30"
+# === Markdown (pluma) ===
+pulldown-cmark = { version = "0.12", default-features = false, features = ["html"] }
+
# ============================================================
# Intra-workspace deps de yahweh (referenciadas por workspace = true)
# ============================================================
diff --git a/crates/apps/gioser-web/Cargo.toml b/crates/apps/gioser-web/Cargo.toml
index 0b2f3d9..e666b52 100644
--- a/crates/apps/gioser-web/Cargo.toml
+++ b/crates/apps/gioser-web/Cargo.toml
@@ -11,7 +11,9 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
gioser-canvas-web = { path = "../../modules/gioser/gioser-canvas-web" }
+pluma-reader-web = { path = "../../modules/pluma/pluma-reader-web" }
wasm-bindgen.workspace = true
+wasm-bindgen-futures.workspace = true
js-sys.workspace = true
[dependencies.web-sys]
@@ -23,9 +25,13 @@ features = [
"HtmlElement",
"HtmlCanvasElement",
"CssStyleDeclaration",
- "MouseEvent",
+ "DomTokenList",
+ "DomRect",
+ "Event",
"EventTarget",
+ "MouseEvent",
+ "KeyboardEvent",
+ "NodeList",
"Performance",
"console",
]
-
diff --git a/crates/apps/gioser-web/index.html b/crates/apps/gioser-web/index.html
index abb54b9..7d5173a 100644
--- a/crates/apps/gioser-web/index.html
+++ b/crates/apps/gioser-web/index.html
@@ -7,67 +7,110 @@
-
+
-
+
Gio·Ser
- EN EL CENTRO · EL SER
-
-
-
+
+
+
+
+
+
+
+
diff --git a/crates/apps/gioser-web/md/agua.md b/crates/apps/gioser-web/md/agua.md
new file mode 100644
index 0000000..43d52bb
--- /dev/null
+++ b/crates/apps/gioser-web/md/agua.md
@@ -0,0 +1,25 @@
+# Agua
+
+> *Lo que fluye. Lo que une dentro y afuera.*
+
+El **Agua** es el dominio de la **espiritualidad aplicada**: las
+prácticas, lecturas y tradiciones que sostienen la atención y dan
+sentido al hacer. No es decoración mística: es la práctica concreta
+de mantenerse permeable, vivo, conectado.
+
+## Espiritualidad aplicada
+
+Aplicada significa que no se queda en libros: pasa por la práctica
+diaria — la lectura, la meditación, la ceremonia, la conversación
+honda. El agua moja todos los otros ejes.
+
+## Lo que vive acá
+
+- Notas de lectura sobre filosofía, mística, sabiduría andina.
+- Diario de prácticas (meditación, ceremonias, retiros).
+- Conversaciones con maestros y comunidades.
+
+## Próximamente
+
+*Acá se va a ir armando una bitácora de lecturas y prácticas. Por
+ahora el placeholder verifica el render bajo el tema **agua**.*
diff --git a/crates/apps/gioser-web/md/aire.md b/crates/apps/gioser-web/md/aire.md
new file mode 100644
index 0000000..8147ce8
--- /dev/null
+++ b/crates/apps/gioser-web/md/aire.md
@@ -0,0 +1,26 @@
+# Aire
+
+> *Lo que respira el sistema. Lo que sube.*
+
+El **Aire** es el dominio del **software público y la IA**. Es la capa
+intangible que transporta pensamiento — los bits que vuelan entre
+máquinas, las inferencias que destilan sentido del ruido, las APIs
+que conversan sin verse.
+
+## Aspiración
+
+El Aire **aspira**: empuja hacia arriba. Es el movimiento de subir el
+nivel de abstracción, de hacer que una cosa difícil parezca obvia, de
+regalarle al usuario una herramienta que no le pesa.
+
+## Lo que vive acá
+
+- Herramientas open source que **GioSer** publica y mantiene.
+- Modelos de IA que asisten al ciclo de creación.
+- Documentación, ensayos, manifiestos.
+
+## Próximamente
+
+*Esta sección se va a llenar con los proyectos concretos del eje aire.*
+Por ahora, este placeholder vive en `md/aire.md` y se renderiza vía
+`pluma-md` con tema *aire*.
diff --git a/crates/apps/gioser-web/md/fuego.md b/crates/apps/gioser-web/md/fuego.md
new file mode 100644
index 0000000..f78e4fb
--- /dev/null
+++ b/crates/apps/gioser-web/md/fuego.md
@@ -0,0 +1,25 @@
+# Fuego
+
+> *Lo que enciende. Lo que transforma.*
+
+El **Fuego** es el dominio de la **inspiración**. Es la chispa que
+convierte una idea en gesto, una frase en ritual, un problema en
+prototipo. Sin fuego, los otros tres elementos se enfrían y se quedan
+contemplándose.
+
+## Inspiración
+
+El fuego no se planea, se **atiende**. Llega — y la respuesta es no
+dejarlo pasar. Acá viven los ensayos, los videos, los manifiestos y
+los experimentos que nacieron porque algo prendió.
+
+## Lo que vive acá
+
+- Charlas, ensayos cortos, posts crudos.
+- Bocetos visuales, exploraciones tipográficas.
+- Documentos de manifiesto sobre cómo trabajar y para qué.
+
+## Próximamente
+
+*Voy a ir enlazando archivos `.md` específicos acá. Por ahora este
+texto sirve para verificar el render bajo el tema **fuego**.*
diff --git a/crates/apps/gioser-web/md/tierra.md b/crates/apps/gioser-web/md/tierra.md
new file mode 100644
index 0000000..49cb2a4
--- /dev/null
+++ b/crates/apps/gioser-web/md/tierra.md
@@ -0,0 +1,26 @@
+# Tierra
+
+> *El cuerpo. La materia. Lo que sostiene.*
+
+La **Tierra** es el dominio del **cuerpo**. Es lo que se toca, lo que
+huele, lo que se siembra. El eje terrestre de GioSer recuerda que
+todo proyecto —por muy abstracto que parezca— pasa por un cuerpo que
+respira, come, descansa y se conmueve.
+
+## Cuerpo
+
+El cuerpo no es una metáfora: es donde aterriza el aire, donde el
+agua se vuelve vida, donde el fuego deja huella. Cuidarlo es parte
+del trabajo.
+
+## Lo que vive acá
+
+- Prácticas, rutinas, recetas.
+- Materialidad: objetos, lugares, oficios.
+- Salud y reposo como infraestructura.
+
+## Próximamente
+
+*Esta sección va a recibir notas, fotos y enlaces a oficios y
+prácticas concretas. Por ahora el placeholder verifica el tema
+**tierra**.*
diff --git a/crates/apps/gioser-web/src/lib.rs b/crates/apps/gioser-web/src/lib.rs
index 034f554..c66dcca 100644
--- a/crates/apps/gioser-web/src/lib.rs
+++ b/crates/apps/gioser-web/src/lib.rs
@@ -1,17 +1,29 @@
//! Entrypoint WASM de la landing GioSer.
//!
-//! Monta el canvas, instala listeners de mouse y resize, corre el loop
-//! con `requestAnimationFrame`, y reposiciona los 4 botones DOM
-//! `#tip-{aire|fuego|tierra|agua}` sobre las puntas proyectadas
-//! de la chacana cada frame.
+//! Responsabilidades:
+//! - Montar canvas WebGL2 + listeners de mouse/resize/RAF loop.
+//! - Reposicionar los 4 tips DOM (botones cardinales) sobre las posiciones
+//! proyectadas del aro de la chacana cada frame.
+//! - Inclinar el título "GioSer" central inyectando CSS vars de tilt.
+//! - Manejar click sobre cada tip → animar drawer expandiéndose desde la
+//! posición del botón hasta fullscreen, cargar el .md asociado vía
+//! `pluma-reader-web` y renderearlo themed por elemento.
+//! - Cerrar drawer con close button, Escape o backdrop click.
use std::cell::RefCell;
use std::rc::Rc;
use gioser_canvas_web::{tips, Renderer};
+use pluma_reader_web::Reader;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
-use web_sys::{Document, HtmlCanvasElement, HtmlElement, MouseEvent, Window};
+use web_sys::{
+ Document, Event, HtmlCanvasElement, HtmlElement, KeyboardEvent, MouseEvent, Window,
+};
+
+/// Factor radial sobre `arm_extent` donde se anclan los botones DOM.
+/// Queda entre la punta de la chacana y el aro grueso.
+const BUTTON_RADIUS_FACTOR: f32 = 1.32;
#[wasm_bindgen(start)]
pub fn boot() -> Result<(), JsValue> {
@@ -34,6 +46,7 @@ pub fn boot() -> Result<(), JsValue> {
install_resize(&window, &canvas, &renderer)?;
install_mouse(&document, &canvas, &renderer)?;
+ install_drawer_handlers(&document)?;
install_raf(&window, &document, &canvas, &renderer);
Ok(())
@@ -66,7 +79,6 @@ fn install_mouse(
let cb = Closure::::new(move |e: MouseEvent| {
let w = canvas.client_width().max(1) as f32;
let h = canvas.client_height().max(1) as f32;
- // Origen en el centro del canvas, +y arriba.
let x = e.client_x() as f32 - w * 0.5;
let y = h * 0.5 - e.client_y() as f32;
r.borrow_mut().set_mouse_px(x, y);
@@ -76,6 +88,116 @@ fn install_mouse(
Ok(())
}
+fn install_drawer_handlers(document: &Document) -> Result<(), JsValue> {
+ let doc = document.clone();
+
+ // Clicks en los 4 tips → open drawer.
+ let tips = document.query_selector_all(".tip[data-md]")?;
+ for i in 0..tips.length() {
+ let Some(node) = tips.item(i) else { continue };
+ let Ok(el) = node.dyn_into::() else { continue };
+ let id = el.id();
+ let element = id.strip_prefix("tip-").unwrap_or("").to_string();
+ let md_url = el.get_attribute("data-md").unwrap_or_default();
+ let d = doc.clone();
+ let el_for_rect = el.clone();
+ let cb = Closure::::new(move |e: Event| {
+ e.prevent_default();
+ open_drawer(&d, &element, &el_for_rect, &md_url);
+ });
+ el.add_event_listener_with_callback("click", cb.as_ref().unchecked_ref())?;
+ cb.forget();
+ }
+
+ // Cualquier elemento marcado con `data-close-drawer` cierra.
+ let closes = document.query_selector_all("[data-close-drawer]")?;
+ for i in 0..closes.length() {
+ let Some(node) = closes.item(i) else { continue };
+ let Ok(el) = node.dyn_into::() else { continue };
+ let d = doc.clone();
+ let cb = Closure::::new(move |e: Event| {
+ e.stop_propagation();
+ close_drawers(&d);
+ });
+ el.add_event_listener_with_callback("click", cb.as_ref().unchecked_ref())?;
+ cb.forget();
+ }
+
+ // Escape cierra.
+ let d = doc.clone();
+ let kcb = Closure::::new(move |e: KeyboardEvent| {
+ if e.key() == "Escape" {
+ close_drawers(&d);
+ }
+ });
+ document.add_event_listener_with_callback("keydown", kcb.as_ref().unchecked_ref())?;
+ kcb.forget();
+
+ Ok(())
+}
+
+fn open_drawer(doc: &Document, element: &str, button: &HtmlElement, md_url: &str) {
+ let rect = button.get_bounding_client_rect();
+ let cx = rect.left() + rect.width() / 2.0;
+ let cy = rect.top() + rect.height() / 2.0;
+ let drawer_id = format!("drawer-{}", element);
+ let Some(drawer_el) = doc.get_element_by_id(&drawer_id) else {
+ return;
+ };
+ let drawer: HtmlElement = drawer_el.unchecked_into();
+ let _ = drawer
+ .style()
+ .set_property("--origin-x", &format!("{:.1}px", cx));
+ let _ = drawer
+ .style()
+ .set_property("--origin-y", &format!("{:.1}px", cy));
+ let _ = drawer.class_list().add_1("open");
+ drawer.set_attribute("aria-hidden", "false").ok();
+
+ if let Some(body) = doc.body() {
+ let _ = body.class_list().add_1("drawer-active");
+ let _ = body
+ .class_list()
+ .add_1(&format!("drawer-active-{}", element));
+ }
+
+ // Carga del .md en background.
+ let content_id = format!("drawer-{}-content", element);
+ if let Some(content_el) = doc.get_element_by_id(&content_id) {
+ let content: HtmlElement = content_el.unchecked_into();
+ let reader = Reader::new(content);
+ let element_owned = element.to_string();
+ let md_url_owned = md_url.to_string();
+ wasm_bindgen_futures::spawn_local(async move {
+ if let Err(e) = reader.open_url(&md_url_owned, &element_owned).await {
+ web_sys::console::warn_1(&e);
+ }
+ });
+ }
+}
+
+fn close_drawers(doc: &Document) {
+ let Ok(drawers) = doc.query_selector_all(".drawer.open") else {
+ return;
+ };
+ for i in 0..drawers.length() {
+ let Some(node) = drawers.item(i) else { continue };
+ let Ok(el) = node.dyn_into::() else {
+ continue;
+ };
+ let _ = el.class_list().remove_1("open");
+ let _ = el.set_attribute("aria-hidden", "true");
+ }
+ if let Some(body) = doc.body() {
+ let _ = body.class_list().remove_1("drawer-active");
+ for e in ["aire", "fuego", "tierra", "agua"] {
+ let _ = body
+ .class_list()
+ .remove_1(&format!("drawer-active-{}", e));
+ }
+ }
+}
+
fn install_raf(
window: &Window,
document: &Document,
@@ -89,8 +211,14 @@ fn install_raf(
let document = document.clone();
let window2 = window.clone();
*g.borrow_mut() = Some(Closure::::new(move |time_ms: f64| {
+ let r = renderer.borrow_mut();
+ // r es Mut, ojo: el render necesita mut, lo hacemos antes de paint.
+ drop(r);
renderer.borrow_mut().render(time_ms);
- position_tips(&document, &canvas, &renderer.borrow());
+ let r = renderer.borrow();
+ position_tips(&document, &canvas, &r);
+ update_tilt_css(&document, &r);
+ drop(r);
if let Some(cb) = f.borrow().as_ref() {
let _ = window2.request_animation_frame(cb.as_ref().unchecked_ref());
}
@@ -119,7 +247,7 @@ fn fit_canvas(canvas: &HtmlCanvasElement, window: &Window) {
}
fn position_tips(document: &Document, canvas: &HtmlCanvasElement, renderer: &Renderer) {
- let clips = renderer.tips_ndc();
+ let clips = renderer.cardinal_positions_ndc(BUTTON_RADIUS_FACTOR);
let cw = canvas.client_width().max(1) as f32;
let ch = canvas.client_height().max(1) as f32;
for (i, (id, _color, _label)) in tips::ORDER.iter().enumerate() {
@@ -138,6 +266,21 @@ fn position_tips(document: &Document, canvas: &HtmlCanvasElement, renderer: &Ren
}
}
+fn update_tilt_css(document: &Document, renderer: &Renderer) {
+ let (pitch, yaw) = renderer.tilt_degrees();
+ if let Some(brand) = document.get_element_by_id("brand") {
+ if let Ok(brand) = brand.dyn_into::() {
+ // CSS rotateX usa el mismo signo que nuestra pitch (mouse up tilts top toward viewer).
+ let _ = brand
+ .style()
+ .set_property("--tilt-x", &format!("{:.2}deg", pitch));
+ let _ = brand
+ .style()
+ .set_property("--tilt-y", &format!("{:.2}deg", yaw));
+ }
+ }
+}
+
fn install_panic_hook() {
static SET: std::sync::Once = std::sync::Once::new();
SET.call_once(|| {
diff --git a/crates/apps/gioser-web/styles.css b/crates/apps/gioser-web/styles.css
index aaa9b5c..fd71309 100644
--- a/crates/apps/gioser-web/styles.css
+++ b/crates/apps/gioser-web/styles.css
@@ -1,11 +1,16 @@
+/* === Tokens === */
:root {
--bg: #06050d;
--fg: #e8eaf5;
+ --gold: #d8a85d;
+ --gold-deep: #b77e34;
--aire: #d0dbff;
--agua: #6cd0f3;
--fuego: #f59056;
--tierra: #d49873;
- --rim: #d8a85d;
+
+ --ease-emerge: cubic-bezier(0.22, 0.61, 0.20, 1);
+ --ease-magma: cubic-bezier(0.32, 0, 0.05, 1);
}
* { box-sizing: border-box; }
@@ -20,9 +25,9 @@ html, body {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
font-weight: 300;
overflow: hidden;
- cursor: default;
}
+/* === Canvas WebGL === */
#gioser-canvas {
position: fixed;
inset: 0;
@@ -30,47 +35,47 @@ html, body {
height: 100vh;
display: block;
z-index: 0;
+ transition: opacity 600ms var(--ease-emerge), filter 600ms var(--ease-emerge);
}
+/* === Marca central: GioSer sobre la superficie de la chacana === */
.brand {
position: fixed;
- top: 4vh;
+ top: 50%;
left: 50%;
- transform: translateX(-50%);
- text-align: center;
- z-index: 10;
+ z-index: 6;
pointer-events: none;
user-select: none;
+ transform-style: preserve-3d;
+ /* perspective hace que la rotación del título dé sensación 3D igual que la chacana */
+ transform:
+ translate(-50%, -50%)
+ perspective(900px)
+ rotateX(var(--tilt-x, 0deg))
+ rotateY(var(--tilt-y, 0deg));
+ transition: transform 30ms linear;
}
-
.brand-title {
font-family: 'Cinzel', serif;
font-weight: 700;
- font-size: clamp(2.4rem, 5vw, 4.4rem);
+ font-size: clamp(2.6rem, 6vw, 5.4rem);
margin: 0;
letter-spacing: 0.08em;
- color: var(--fg);
+ color: #f4eedf;
text-shadow:
- 0 0 18px rgba(108, 208, 243, 0.35),
- 0 0 40px rgba(216, 168, 93, 0.18);
+ 0 0 22px rgba(216, 168, 93, 0.55),
+ 0 0 50px rgba(183, 126, 52, 0.30),
+ 0 0 80px rgba(216, 168, 93, 0.15);
}
-
.brand-dot {
- color: var(--rim);
+ color: var(--gold);
margin: 0 0.05em;
- text-shadow: 0 0 14px var(--rim), 0 0 28px rgba(245, 144, 86, 0.5);
-}
-
-.brand-sub {
- font-family: 'Inter', sans-serif;
- font-weight: 300;
- letter-spacing: 0.5em;
- font-size: clamp(0.65rem, 0.9vw, 0.8rem);
- margin: 0.55rem 0 0;
- color: rgba(232, 234, 245, 0.6);
- text-indent: 0.5em;
+ text-shadow:
+ 0 0 14px var(--gold),
+ 0 0 32px rgba(245, 144, 86, 0.55);
}
+/* === Tips (botones cardinales) === */
#tips {
position: fixed;
inset: 0;
@@ -85,107 +90,409 @@ html, body {
text-decoration: none;
color: var(--fg);
user-select: none;
+ cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
- gap: 0.35rem;
- padding: 0.7rem 1.0rem 0.55rem;
- min-width: 120px;
+ gap: 0.55rem;
+ padding: 1.1rem 1.6rem 0.95rem;
+ min-width: 168px;
- background: rgba(8, 6, 22, 0.42);
- backdrop-filter: blur(8px) saturate(120%);
- -webkit-backdrop-filter: blur(8px) saturate(120%);
- border-radius: 14px;
- border: 1px solid rgba(255, 255, 255, 0.07);
- box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.03);
+ background:
+ radial-gradient(ellipse at top, rgba(255, 255, 255, 0.04), transparent 65%),
+ rgba(8, 6, 22, 0.55);
+ backdrop-filter: blur(12px) saturate(140%);
+ -webkit-backdrop-filter: blur(12px) saturate(140%);
+ border-radius: 20px;
+ border: 1px solid rgba(216, 168, 93, 0.25);
+
+ box-shadow:
+ 0 12px 36px rgba(0, 0, 0, 0.40),
+ inset 0 0 0 1px rgba(255, 255, 255, 0.04);
transition:
- box-shadow 260ms ease,
- border-color 260ms ease,
- background 260ms ease,
- letter-spacing 260ms ease;
+ box-shadow 350ms var(--ease-emerge),
+ border-color 350ms var(--ease-emerge),
+ background 350ms var(--ease-emerge),
+ opacity 300ms ease;
}
-
.tip::before {
content: "";
position: absolute;
- inset: -1px;
- border-radius: inherit;
- padding: 1px;
- background: linear-gradient(135deg,
- rgba(255,255,255,0.18),
- rgba(255,255,255,0.0) 40%,
- rgba(216, 168, 93, 0.18));
- -webkit-mask:
- linear-gradient(#000 0 0) content-box,
- linear-gradient(#000 0 0);
- -webkit-mask-composite: xor;
- mask-composite: exclude;
+ inset: -10px;
+ border-radius: 26px;
+ border: 1px solid currentColor;
+ opacity: 0;
+ transition: opacity 400ms ease, inset 400ms var(--ease-emerge);
pointer-events: none;
- opacity: 0.7;
}
-
.tip:hover {
- border-color: rgba(255, 255, 255, 0.22);
- background: rgba(20, 14, 40, 0.6);
+ border-color: currentColor;
+ background:
+ radial-gradient(ellipse at top, rgba(255, 255, 255, 0.07), transparent 65%),
+ rgba(20, 14, 40, 0.72);
+}
+.tip:hover::before {
+ opacity: 0.45;
+ inset: -16px;
}
-
.tip-glyph {
- width: 42px;
- height: 42px;
+ width: 54px;
+ height: 54px;
color: currentColor;
- filter: drop-shadow(0 0 6px currentColor);
- transition: filter 240ms ease;
+ filter: drop-shadow(0 0 6px currentColor) drop-shadow(0 0 16px currentColor);
+ transition: filter 320ms ease, transform 350ms var(--ease-emerge);
}
.tip:hover .tip-glyph {
- filter: drop-shadow(0 0 12px currentColor) drop-shadow(0 0 20px currentColor);
+ filter: drop-shadow(0 0 14px currentColor) drop-shadow(0 0 28px currentColor);
+ transform: translateY(-3px);
}
-
.tip-label {
font-family: 'Cinzel', serif;
- font-size: 0.74rem;
- letter-spacing: 0.36em;
+ font-size: 0.95rem;
+ letter-spacing: 0.42em;
font-weight: 600;
- text-indent: 0.36em;
+ text-indent: 0.42em;
+ margin-top: 0.15rem;
}
-
.tip-sub {
font-family: 'Inter', sans-serif;
- font-size: 0.62rem;
- letter-spacing: 0.18em;
+ font-size: 0.7rem;
+ letter-spacing: 0.22em;
font-weight: 300;
- color: rgba(232, 234, 245, 0.55);
+ color: rgba(232, 234, 245, 0.62);
text-transform: uppercase;
+ text-indent: 0.22em;
+}
+.tip-aire { color: var(--aire); }
+.tip-fuego { color: var(--fuego); }
+.tip-agua { color: var(--agua); }
+.tip-tierra { color: var(--tierra); }
+
+/* Cuando un drawer está abierto, los tips se ocultan suavemente. */
+body.drawer-active .tip {
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 250ms ease;
+}
+body.drawer-active #gioser-canvas {
+ opacity: 0.35;
+ filter: blur(4px) saturate(80%);
+}
+body.drawer-active .brand {
+ opacity: 0;
+ transition: opacity 250ms ease;
}
-.tip-aire { color: var(--aire); }
-.tip-aire:hover { box-shadow: 0 0 32px rgba(208, 219, 255, 0.45); }
-.tip-fuego { color: var(--fuego); }
-.tip-fuego:hover { box-shadow: 0 0 32px rgba(245, 144, 86, 0.45); }
-.tip-agua { color: var(--agua); }
-.tip-agua:hover { box-shadow: 0 0 32px rgba(108, 208, 243, 0.45); }
-.tip-tierra { color: var(--tierra); }
-.tip-tierra:hover { box-shadow: 0 0 32px rgba(212, 152, 115, 0.45); }
-
-.quote {
+/* === Drawers (visor MD full-screen) === */
+.drawer {
position: fixed;
- bottom: 3vh;
- left: 50%;
- transform: translateX(-50%);
- font-family: 'Cinzel', serif;
+ inset: 0;
+ z-index: 100;
+ pointer-events: none;
+ opacity: 0;
+ visibility: hidden;
+ transform-origin: var(--origin-x, 50%) var(--origin-y, 50%);
+ transform: scale(0.0);
+ background:
+ radial-gradient(ellipse at center, var(--drawer-glow, rgba(216, 168, 93, 0.15)), transparent 65%),
+ rgba(6, 5, 13, 0.96);
+ backdrop-filter: blur(28px) saturate(140%);
+ -webkit-backdrop-filter: blur(28px) saturate(140%);
+ transition:
+ transform 700ms var(--ease-magma),
+ opacity 450ms ease,
+ visibility 0s 700ms;
+ overflow: hidden;
+}
+.drawer.open {
+ pointer-events: auto;
+ opacity: 1;
+ visibility: visible;
+ transform: scale(1);
+ transition:
+ transform 700ms var(--ease-magma),
+ opacity 450ms ease,
+ visibility 0s;
+}
+
+.drawer-aire { --drawer-glow: rgba(208, 219, 255, 0.22); --drawer-accent: var(--aire); }
+.drawer-fuego { --drawer-glow: rgba(245, 144, 86, 0.28); --drawer-accent: var(--fuego); }
+.drawer-agua { --drawer-glow: rgba(108, 208, 243, 0.22); --drawer-accent: var(--agua); }
+.drawer-tierra { --drawer-glow: rgba(212, 152, 115, 0.24); --drawer-accent: var(--tierra); }
+
+/* Ambience: capa decorativa con efectos según elemento. */
+.drawer-ambience {
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ z-index: 0;
+}
+.drawer-aire .drawer-ambience {
+ background:
+ radial-gradient(circle at 18% 22%, rgba(208, 219, 255, 0.20), transparent 38%),
+ radial-gradient(circle at 78% 68%, rgba(208, 219, 255, 0.14), transparent 40%),
+ radial-gradient(circle at 45% 90%, rgba(180, 200, 255, 0.10), transparent 45%);
+ animation: aire-drift 28s ease-in-out infinite alternate;
+}
+.drawer-fuego .drawer-ambience {
+ background:
+ radial-gradient(circle at 50% 100%, rgba(245, 144, 86, 0.35), transparent 55%),
+ radial-gradient(circle at 25% 80%, rgba(255, 90, 40, 0.18), transparent 35%),
+ radial-gradient(circle at 80% 85%, rgba(255, 140, 60, 0.18), transparent 35%);
+ animation: fuego-flicker 5s ease-in-out infinite;
+}
+.drawer-agua .drawer-ambience {
+ background:
+ radial-gradient(ellipse at 50% 95%, rgba(60, 160, 230, 0.30), transparent 60%),
+ radial-gradient(ellipse at 20% 70%, rgba(108, 208, 243, 0.15), transparent 50%),
+ radial-gradient(ellipse at 80% 75%, rgba(108, 208, 243, 0.12), transparent 50%);
+ animation: agua-tide 14s ease-in-out infinite alternate;
+}
+.drawer-tierra .drawer-ambience {
+ background:
+ radial-gradient(ellipse at 50% 100%, rgba(120, 80, 40, 0.40), transparent 60%),
+ radial-gradient(ellipse at 22% 88%, rgba(180, 130, 80, 0.20), transparent 45%),
+ radial-gradient(ellipse at 78% 88%, rgba(150, 100, 60, 0.22), transparent 45%);
+}
+
+@keyframes aire-drift {
+ from { transform: translate(-4%, -1%); }
+ to { transform: translate(4%, 2%); }
+}
+@keyframes fuego-flicker {
+ 0%, 100% { opacity: 0.85; transform: scaleY(1.00); }
+ 35% { opacity: 1.00; transform: scaleY(1.04); }
+ 60% { opacity: 0.92; transform: scaleY(0.98); }
+}
+@keyframes agua-tide {
+ from { transform: translateY(0); }
+ to { transform: translateY(-3%); }
+}
+
+/* === Drawer head + close === */
+.drawer-head {
+ position: relative;
+ z-index: 2;
+ text-align: center;
+ padding: 7vh 8vw 2vh;
+ color: var(--drawer-accent);
+}
+.drawer-mark {
+ display: inline-block;
+ font-family: 'Inter', sans-serif;
font-size: 0.7rem;
letter-spacing: 0.55em;
- color: rgba(232, 234, 245, 0.42);
- pointer-events: none;
- user-select: none;
- z-index: 10;
+ text-transform: uppercase;
+ color: rgba(232, 234, 245, 0.6);
+ margin-bottom: 0.4rem;
text-indent: 0.55em;
}
+.drawer-title {
+ font-family: 'Cinzel', serif;
+ font-weight: 700;
+ font-size: clamp(2.4rem, 6vw, 4.6rem);
+ margin: 0;
+ letter-spacing: 0.08em;
+ color: var(--drawer-accent);
+ text-shadow: 0 0 28px currentColor, 0 0 56px rgba(255, 255, 255, 0.10);
+}
+.drawer-tag {
+ display: block;
+ font-family: 'Inter', sans-serif;
+ font-size: 0.78rem;
+ letter-spacing: 0.32em;
+ text-transform: uppercase;
+ color: rgba(232, 234, 245, 0.55);
+ margin-top: 0.7rem;
+ text-indent: 0.32em;
+}
+.drawer-close {
+ position: absolute;
+ top: 2.4vh;
+ right: 2.4vw;
+ z-index: 5;
+ width: 48px;
+ height: 48px;
+ border-radius: 50%;
+ background: rgba(255, 255, 255, 0.04);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ color: var(--drawer-accent);
+ font-size: 1.6rem;
+ line-height: 1;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: background 200ms ease, transform 250ms var(--ease-emerge), border-color 200ms ease;
+ font-family: 'Inter', sans-serif;
+}
+.drawer-close:hover {
+ background: rgba(255, 255, 255, 0.10);
+ border-color: currentColor;
+ transform: rotate(90deg);
+}
+
+/* === Drawer content (MD render via pluma-reader-web) === */
+.drawer-content {
+ position: relative;
+ z-index: 2;
+ max-height: calc(100vh - 22vh);
+ overflow-y: auto;
+ padding: 1vh 10vw 8vh;
+ opacity: 0;
+ transition: opacity 500ms ease 250ms;
+}
+.drawer.open .drawer-content {
+ opacity: 1;
+}
+.drawer-content::-webkit-scrollbar { width: 6px; }
+.drawer-content::-webkit-scrollbar-thumb {
+ background: var(--drawer-accent);
+ border-radius: 3px;
+ opacity: 0.4;
+}
+
+/* === pluma-doc: estilos del visor MD === */
+.pluma-doc {
+ max-width: 760px;
+ margin: 0 auto;
+ font-family: 'Inter', sans-serif;
+ font-weight: 300;
+ font-size: 1.05rem;
+ line-height: 1.78;
+ color: rgba(232, 234, 245, 0.92);
+}
+.pluma-doc > * + * { margin-top: 1.0em; }
+.pluma-doc h1 {
+ font-family: 'Cinzel', serif;
+ font-weight: 700;
+ font-size: clamp(1.7rem, 3vw, 2.4rem);
+ color: var(--drawer-accent);
+ text-shadow: 0 0 18px currentColor;
+ letter-spacing: 0.04em;
+ margin-top: 1.4em;
+}
+.pluma-doc h2 {
+ font-family: 'Cinzel', serif;
+ font-weight: 500;
+ font-size: 1.5rem;
+ color: var(--drawer-accent);
+ letter-spacing: 0.04em;
+ margin-top: 1.6em;
+ padding-bottom: 0.3em;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.10);
+}
+.pluma-doc h3 {
+ font-family: 'Inter', sans-serif;
+ font-weight: 600;
+ font-size: 1.18rem;
+ color: rgba(255, 255, 255, 0.92);
+ letter-spacing: 0.03em;
+ margin-top: 1.6em;
+}
+.pluma-doc p { margin: 0; }
+.pluma-doc a {
+ color: var(--drawer-accent);
+ text-decoration: none;
+ border-bottom: 1px solid currentColor;
+ transition: opacity 200ms ease;
+}
+.pluma-doc a:hover { opacity: 0.7; }
+.pluma-doc strong { color: rgba(255, 255, 255, 0.98); font-weight: 600; }
+.pluma-doc em { color: rgba(255, 255, 255, 0.92); }
+.pluma-doc code {
+ font-family: 'JetBrains Mono', ui-monospace, monospace;
+ background: rgba(255, 255, 255, 0.06);
+ padding: 0.12em 0.45em;
+ border-radius: 4px;
+ font-size: 0.92em;
+ color: var(--drawer-accent);
+}
+.pluma-doc pre {
+ background: rgba(0, 0, 0, 0.45);
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ border-left: 3px solid var(--drawer-accent);
+ border-radius: 8px;
+ padding: 1rem 1.2rem;
+ overflow-x: auto;
+ font-size: 0.92rem;
+}
+.pluma-doc pre code {
+ background: transparent;
+ color: rgba(232, 234, 245, 0.92);
+ padding: 0;
+}
+.pluma-doc blockquote {
+ border-left: 3px solid var(--drawer-accent);
+ padding: 0.4em 1.2em;
+ color: rgba(232, 234, 245, 0.75);
+ font-style: italic;
+ background: rgba(255, 255, 255, 0.03);
+ border-radius: 0 6px 6px 0;
+}
+.pluma-doc ul, .pluma-doc ol { padding-left: 1.6em; }
+.pluma-doc li { margin: 0.4em 0; }
+.pluma-doc li::marker { color: var(--drawer-accent); }
+.pluma-doc hr {
+ border: none;
+ height: 1px;
+ background: linear-gradient(to right, transparent, var(--drawer-accent), transparent);
+ margin: 2em 0;
+}
+.pluma-doc table {
+ border-collapse: collapse;
+ width: 100%;
+ font-size: 0.95rem;
+}
+.pluma-doc th, .pluma-doc td {
+ padding: 0.55em 0.9em;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
+ text-align: left;
+}
+.pluma-doc th {
+ color: var(--drawer-accent);
+ font-weight: 600;
+ letter-spacing: 0.04em;
+}
+
+.pluma-loading, .pluma-error {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 30vh;
+ color: rgba(232, 234, 245, 0.55);
+ font-family: 'Cinzel', serif;
+ letter-spacing: 0.4em;
+ font-size: 0.9rem;
+}
+.pluma-loading::before {
+ content: "";
+ width: 28px;
+ height: 28px;
+ margin-right: 1rem;
+ border: 1px solid var(--drawer-accent);
+ border-top-color: transparent;
+ border-radius: 50%;
+ animation: pluma-spin 1s linear infinite;
+}
+.pluma-error {
+ color: var(--fuego);
+ font-style: italic;
+}
+@keyframes pluma-spin {
+ to { transform: rotate(360deg); }
+}
+
+/* === Responsive === */
@media (max-width: 720px) {
- .tip { min-width: 90px; padding: 0.5rem 0.7rem; }
- .tip-glyph { width: 32px; height: 32px; }
- .tip-label { font-size: 0.6rem; }
+ .tip { min-width: 110px; padding: 0.7rem 0.9rem; }
+ .tip-glyph { width: 36px; height: 36px; }
+ .tip-label { font-size: 0.72rem; }
.tip-sub { display: none; }
+ .brand-title { font-size: clamp(2rem, 10vw, 3rem); }
+ .drawer-head { padding: 5vh 5vw 1vh; }
+ .drawer-content { padding: 0 5vw 5vh; }
}
diff --git a/crates/modules/gioser/gioser-canvas-web/src/lib.rs b/crates/modules/gioser/gioser-canvas-web/src/lib.rs
index 3b6656d..53e77e3 100644
--- a/crates/modules/gioser/gioser-canvas-web/src/lib.rs
+++ b/crates/modules/gioser/gioser-canvas-web/src/lib.rs
@@ -1,20 +1,14 @@
//! Renderer WebGL2 que compone geometría + física + paleta + shaders en pantalla.
//!
-//! Es agnóstico del DOM: el caller monta el `