diff --git a/README.md b/README.md
index 8ea59b7..ef9f6db 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,10 @@ scientifico** — the four-player, fixed-partnership Italian card game:
and the `tavolo-app` composition root wiring them together.
See [server/README.md](server/README.md).
- **`web/`** — frontend: Rust + [Sycamore](https://sycamore.dev) compiled to
- WebAssembly, built with [Trunk](https://trunkrs.dev). Card images are the
+ WebAssembly, built with [Trunk](https://trunkrs.dev) and styled with
+ [Bootstrap](https://getbootstrap.com) 5.3 (MIT, vendored under
+ `web/vendor/` so builds work offline) plus the custom felt theme in
+ `web/theme.css`. Card images are the
CC0 *woodcut napoletane* deck traced from a 1902 Naples print
([SONDLecT/woodcut-napoletane](https://github.com/SONDLecT/woodcut-napoletane)).
diff --git a/web/Cargo.toml b/web/Cargo.toml
index de8e142..d8cb479 100644
--- a/web/Cargo.toml
+++ b/web/Cargo.toml
@@ -13,7 +13,7 @@ serde_json = "1"
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
futures = "0.3"
-web-sys = { version = "0.3", features = ["Window", "Location", "console"] }
+web-sys = { version = "0.3", features = ["Window", "Location", "Clipboard", "Navigator", "console"] }
js-sys = "0.3"
gloo-timers = "0.3"
console_error_panic_hook = "0.1"
diff --git a/web/index.html b/web/index.html
index 3a01e97..1280622 100644
--- a/web/index.html
+++ b/web/index.html
@@ -1,10 +1,13 @@
-
+
Tavolo
+
+
+
diff --git a/web/src/components/mod.rs b/web/src/components/mod.rs
index 1af74df..106d69f 100644
--- a/web/src/components/mod.rs
+++ b/web/src/components/mod.rs
@@ -1,3 +1,4 @@
pub mod card;
+pub mod nav;
pub mod summary;
pub mod toast;
diff --git a/web/src/components/nav.rs b/web/src/components/nav.rs
new file mode 100644
index 0000000..2414952
--- /dev/null
+++ b/web/src/components/nav.rs
@@ -0,0 +1,38 @@
+//! Shared Bootstrap navbar used by the lobby, history and leaderboard pages.
+use sycamore::prelude::*;
+
+fn nav_link(href: &'static str, label: &'static str, active: &str) -> View {
+ let class = if href == active {
+ "nav-link active"
+ } else {
+ "nav-link"
+ };
+ let aria = if href == active { "page" } else { "" };
+ view! {
+ li(class="nav-item") {
+ a(class=class, href=href, aria-current=aria) { (label) }
+ }
+ }
+}
+
+/// Renders the app navbar. `active` is the current route (`/`, `/history`,
+/// `/leaderboard`); `whoami` builds the right-aligned slot for the
+/// signed-in user and the log-out action.
+pub fn app_nav(active: &'static str, whoami: impl Fn() -> View + 'static) -> View {
+ view! {
+ nav(class="navbar navbar-expand flex-wrap") {
+ div(class="container-fluid") {
+ a(class="navbar-brand", href="/") {
+ span(class="text-warning") { "♠" }
+ " tavolo"
+ }
+ ul(class="navbar-nav me-auto") {
+ (nav_link("/", "Lobby", active))
+ (nav_link("/history", "My matches", active))
+ (nav_link("/leaderboard", "Leaderboard", active))
+ }
+ (move || whoami())
+ }
+ }
+ }
+}
diff --git a/web/src/components/summary.rs b/web/src/components/summary.rs
index 230acb4..0e66c05 100644
--- a/web/src/components/summary.rs
+++ b/web/src/components/summary.rs
@@ -246,7 +246,7 @@ pub fn hand_summary_modal(
// whole modal (which would swap DOM nodes under the user's cursor).
let deadline_ms = js_sys::Date::parse(deadline);
view! {
- p(class="hint") {
+ p(class="text-secondary small") {
"Auto-continuing in "
(move || {
((deadline_ms - now.get_clone()) / 1000.0).ceil().max(0.0) as i32
@@ -263,11 +263,11 @@ pub fn hand_summary_modal(
let action = if already_acked {
let waiting_text = format!("Waiting for {}…", waiting.join(", "));
view! {
- button(class="button primary", disabled=true) { (waiting_text) }
+ button(class="btn btn-warning", disabled=true) { (waiting_text) }
}
} else {
view! {
- button(class="button primary", on:click=move |_| {
+ button(class="btn btn-warning", on:click=move |_| {
if let Some(s) = socket.get_clone() {
s.ack();
}
diff --git a/web/src/components/toast.rs b/web/src/components/toast.rs
index a66a2e0..a0c9f42 100644
--- a/web/src/components/toast.rs
+++ b/web/src/components/toast.rs
@@ -15,6 +15,6 @@ pub fn toast(error: Signal