Skip to main content

logicaffeine_web/ui/pages/
studio.rs

1//! Studio page - multi-mode playground for Logic, Code, and Math.
2//!
3//! The main development environment with three modes:
4//!
5//! - **Logic Mode**: Parse English sentences to First-Order Logic with AST
6//!   visualization and proof checking
7//! - **Code Mode**: Write imperative LOGOS code with REPL execution and
8//!   Rust code generation
9//! - **Math Mode**: Define theorems and types with interactive proofs and
10//!   tactic guidance
11//!
12//! # Layout
13//!
14//! - **Left sidebar**: File browser with example files
15//! - **Center**: Live editor with syntax highlighting
16//! - **Right panel**: Context-sensitive output (FOL, AST, REPL, proofs)
17//!
18//! # Route
19//!
20//! Accessed via [`Route::Studio`](crate::ui::router::Route::Studio).
21
22use dioxus::prelude::*;
23#[cfg(all(feature = "split", target_arch = "wasm32"))]
24use dioxus::wasm_split;
25use std::cell::RefCell;
26use logicaffeine_compile::{
27    compile_for_ui, compile_for_proof, compile_theorem_for_ui, generate_rust_code,
28    extract_math_rust, extract_logic_rust,
29    interpret_for_ui_baseline, interpret_streaming_with_vfs, CompileResult, ProofCompileResult,
30    TheoremCompileResult, SolvedGrid,
31    interpreter::InterpreterResult,
32};
33use logicaffeine_proof::{
34    BackwardChainer, DerivationTree, ProofExpr,
35    hints::{suggest_hint, SuggestedTactic},
36};
37use crate::ui::components::editor::LiveEditor;
38use crate::ui::components::logic_output::{LogicOutput, OutputFormat};
39use crate::ui::components::ast_tree::AstTree;
40use crate::ui::components::socratic_guide::{SocraticGuide, GuideMode, get_success_message, get_context_hint};
41use crate::ui::components::main_nav::{MainNav, ActivePage};
42use crate::ui::components::symbol_dictionary::SymbolDictionary;
43use crate::ui::seo::{JsonLdMultiple, PageHead, organization_schema, software_application_schema, breadcrumb_schema, BreadcrumbItem, pages as seo_pages};
44use crate::ui::components::mode_toggle::ModeToggle;
45use crate::ui::components::file_browser::FileBrowser;
46use crate::ui::components::repl_output::ReplOutput;
47use crate::ui::components::context_view::{ContextView, ContextEntry, EntryKind};
48use crate::ui::components::code_editor::{CodeEditor, CodeView, Language};
49use crate::ui::components::proof_panel::{ProofPanel, ProofStatus, Tactic};
50use crate::ui::components::debug_drawer::{DebugDrawer, IC_BUG};
51use crate::ui::state::{StudioMode, FileNode, ReplLine};
52use crate::ui::responsive::{MOBILE_BASE_STYLES, MOBILE_TAB_BAR_STYLES};
53use logicaffeine_kernel::interface::Repl;
54use crate::ui::examples::seed_examples;
55#[cfg(target_arch = "wasm32")]
56use logicaffeine_system::fs::{get_platform_vfs_with_fallback, WebVfs};
57use logicaffeine_system::fs::{get_platform_vfs, Vfs, DirEntry, VfsResult};
58#[cfg(target_arch = "wasm32")]
59use wasm_bindgen::JsValue;
60use std::rc::Rc;
61
62/// Parse math code into complete statements.
63///
64/// Handles both Coq-style (period-terminated) and Literate syntax (block-based):
65/// - `## To ...` blocks: collect header + all indented lines until non-indented line
66/// - `A X is either:` blocks: collect header + indented variants
67/// - Traditional commands: accumulate until period-terminator
68pub fn parse_math_statements(code: &str) -> Vec<String> {
69    let mut statements = Vec::new();
70    let lines: Vec<&str> = code.lines().collect();
71    let mut i = 0;
72
73    while i < lines.len() {
74        let line = lines[i];
75        let trimmed = line.trim();
76
77        // Skip empty lines and comments
78        if trimmed.is_empty() || trimmed.starts_with("--") {
79            i += 1;
80            continue;
81        }
82
83        // Check for Literate function definition: "## To ..."
84        if trimmed.starts_with("## To ") {
85            let mut block = String::new();
86            block.push_str(trimmed);
87            i += 1;
88
89            // Collect all indented lines (body of the function)
90            while i < lines.len() {
91                let next_line = lines[i];
92                let next_trimmed = next_line.trim();
93
94                // Empty lines within block are OK
95                if next_trimmed.is_empty() {
96                    i += 1;
97                    continue;
98                }
99
100                // Comments within block are skipped
101                if next_trimmed.starts_with("--") {
102                    i += 1;
103                    continue;
104                }
105
106                // Check if line is indented (part of block body)
107                // A line is indented if it starts with whitespace
108                let is_indented = next_line.starts_with(' ') || next_line.starts_with('\t');
109
110                // Also check if it's a continuation keyword (Consider, When, Yield)
111                let is_continuation = next_trimmed.starts_with("Consider ")
112                    || next_trimmed.starts_with("When ")
113                    || next_trimmed.starts_with("Yield ");
114
115                if is_indented || is_continuation {
116                    block.push(' ');
117                    block.push_str(next_trimmed);
118                    i += 1;
119                } else {
120                    // Non-indented, non-continuation line: end of block
121                    break;
122                }
123            }
124
125            statements.push(block);
126            continue;
127        }
128
129        // Check for Literate theorem: "## Theorem: ..."
130        // Collects header + Statement: + Proof: lines as one block
131        if trimmed.starts_with("## Theorem:") {
132            let mut block = String::new();
133            block.push_str(trimmed);
134            i += 1;
135
136            // Collect indented lines (Statement: and Proof:)
137            while i < lines.len() {
138                let next_line = lines[i];
139                let next_trimmed = next_line.trim();
140
141                // Empty lines within block are OK
142                if next_trimmed.is_empty() {
143                    i += 1;
144                    continue;
145                }
146
147                // Comments within block are skipped
148                if next_trimmed.starts_with("--") {
149                    i += 1;
150                    continue;
151                }
152
153                // Check if line is indented (part of theorem block)
154                let is_indented = next_line.starts_with(' ') || next_line.starts_with('\t');
155
156                // Also check for Statement: or Proof: keywords (may not be indented)
157                let is_theorem_part = next_trimmed.starts_with("Statement:")
158                    || next_trimmed.starts_with("Proof:");
159
160                if is_indented || is_theorem_part {
161                    block.push('\n');
162                    block.push_str(next_line);
163                    i += 1;
164
165                    // If we just added a Proof: line that ends with period, block is complete
166                    if next_trimmed.starts_with("Proof:") && next_trimmed.ends_with('.') {
167                        break;
168                    }
169                } else {
170                    // Non-indented, non-theorem-part line: end of block
171                    break;
172                }
173            }
174
175            statements.push(block);
176            continue;
177        }
178
179        // Check for Literate inductive: "A X is either..." or "An X is either..."
180        if (trimmed.starts_with("A ") || trimmed.starts_with("An ")) && trimmed.contains(" is either") {
181            // Check if this is a single-line definition (ends with period and no colon at end)
182            if trimmed.ends_with('.') && !trimmed.trim_end_matches('.').ends_with(':') {
183                statements.push(trimmed.to_string());
184                i += 1;
185                continue;
186            }
187
188            // Multi-line definition: collect header + indented variants
189            let mut block = String::new();
190            block.push_str(trimmed);
191            i += 1;
192
193            // Collect indented variant lines
194            while i < lines.len() {
195                let next_line = lines[i];
196                let next_trimmed = next_line.trim();
197
198                // Empty lines are OK
199                if next_trimmed.is_empty() {
200                    i += 1;
201                    continue;
202                }
203
204                // Comments are skipped
205                if next_trimmed.starts_with("--") {
206                    i += 1;
207                    continue;
208                }
209
210                // Check if indented
211                let is_indented = next_line.starts_with(' ') || next_line.starts_with('\t');
212
213                // Also accept "a Variant" or variant names starting with capital
214                let looks_like_variant = next_trimmed.starts_with("a ")
215                    || next_trimmed.chars().next().map(|c| c.is_uppercase()).unwrap_or(false);
216
217                if is_indented || (looks_like_variant && !next_trimmed.starts_with("A ") && !next_trimmed.starts_with("An ")) {
218                    // For indented lines, join with " or " for the parser
219                    if !block.ends_with(':') {
220                        block.push_str(" or ");
221                    } else {
222                        block.push(' ');
223                    }
224                    block.push_str(next_trimmed.trim_end_matches('.'));
225                    i += 1;
226                } else {
227                    break;
228                }
229            }
230
231            // Ensure ends with period
232            if !block.ends_with('.') {
233                block.push('.');
234            }
235            statements.push(block);
236            continue;
237        }
238
239        // Traditional Coq-style: accumulate until period
240        let mut current_stmt = String::new();
241        while i < lines.len() {
242            let line = lines[i];
243            let trimmed = line.trim();
244
245            if trimmed.is_empty() || trimmed.starts_with("--") {
246                i += 1;
247                continue;
248            }
249
250            if !current_stmt.is_empty() {
251                current_stmt.push(' ');
252            }
253            current_stmt.push_str(trimmed);
254
255            i += 1;
256
257            // Check if statement is complete
258            if trimmed.ends_with('.') {
259                break;
260            }
261        }
262
263        if !current_stmt.is_empty() {
264            statements.push(current_stmt);
265        }
266    }
267
268    statements
269}
270
271/// Recursively load directory contents from VFS into FileNode tree
272async fn load_dir_recursive<V: Vfs>(vfs: &V, path: &str, parent: &mut FileNode) -> VfsResult<()> {
273    let entries = vfs.list_dir(path).await?;
274
275    for entry in entries {
276        let full_path = if path == "/" {
277            format!("/{}", entry.name)
278        } else {
279            format!("{}/{}", path, entry.name)
280        };
281
282        if entry.is_directory {
283            let mut dir_node = FileNode::directory(entry.name.clone(), full_path.clone());
284            // Recursively load subdirectories
285            let _ = Box::pin(load_dir_recursive(vfs, &full_path, &mut dir_node)).await;
286            parent.children.push(dir_node);
287        } else {
288            parent.children.push(FileNode::file(entry.name, full_path));
289        }
290    }
291
292    // Sort: directories first, then alphabetically
293    parent.children.sort_by(|a, b| {
294        match (a.is_directory, b.is_directory) {
295            (true, false) => std::cmp::Ordering::Less,
296            (false, true) => std::cmp::Ordering::Greater,
297            _ => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
298        }
299    });
300
301    Ok(())
302}
303
304/// Count total files and directories in a tree (for debugging)
305fn count_files(node: &FileNode) -> usize {
306    let mut count = node.children.len();
307    for child in &node.children {
308        if child.is_directory {
309            count += count_files(child);
310        }
311    }
312    count
313}
314
315/// Format a DerivationTree as HTML for the proof panel
316fn format_derivation_html(tree: &DerivationTree) -> String {
317    fn format_node(tree: &DerivationTree, depth: usize) -> String {
318        let indent = "  ".repeat(depth);
319        let rule_class = "rule";
320        let conclusion_class = "conclusion";
321
322        let mut result = String::new();
323
324        // Show the conclusion with rule
325        result.push_str(&format!(
326            "{}<span class=\"{}\">{:?}:</span> <span class=\"{}\">{}</span>\n",
327            indent,
328            rule_class,
329            tree.rule,
330            conclusion_class,
331            tree.conclusion
332        ));
333
334        // Recursively format premises
335        if !tree.premises.is_empty() {
336            for premise in &tree.premises {
337                result.push_str(&format_node(premise, depth + 1));
338            }
339        }
340
341        result
342    }
343
344    let mut output = String::new();
345    output.push_str("<div style=\"font-family: monospace; white-space: pre-wrap;\">\n");
346    output.push_str(&format_node(tree, 0));
347    output.push_str("</div>");
348    output
349}
350
351/// Minimal HTML escape for grid/answer cell text.
352fn esc(s: &str) -> String {
353    s.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;")
354}
355
356/// Render a [`SolvedGrid`] as an HTML table for the proof panel — the easter egg that
357/// fills the whole grid once the premises take the grid form. Every shown cell is a
358/// certified entailment (no Z3); an undetermined cell shows a dot.
359fn format_grid_html(grid: &SolvedGrid) -> String {
360    let cell = "border:1px solid var(--studio-border);padding:4px 10px;text-align:center;";
361    let head = "border:1px solid var(--studio-border);padding:4px 10px;text-align:center;color:var(--studio-accent);font-weight:600;";
362    let mut s = String::new();
363    s.push_str("<div style=\"margin-bottom:16px;\">");
364    s.push_str("<div style=\"font-family:monospace;font-size:12px;color:var(--studio-text-secondary);margin-bottom:6px;\">Solved grid — every filled cell certified, no Z3:</div>");
365    s.push_str("<table style=\"border-collapse:collapse;font-family:monospace;font-size:13px;color:var(--studio-text);\">");
366    s.push_str("<tr>");
367    s.push_str(&format!("<th style=\"{head}\">{}</th>", esc(&grid.row_label)));
368    for col in &grid.columns {
369        s.push_str(&format!("<th style=\"{head}\">{}</th>", esc(&col.label)));
370    }
371    s.push_str("</tr>");
372    for (ri, row) in grid.rows.iter().enumerate() {
373        s.push_str("<tr>");
374        s.push_str(&format!("<td style=\"{cell}font-weight:600;\">{}</td>", esc(row)));
375        for col in &grid.columns {
376            match col.cells.get(ri).and_then(|c| c.as_ref()) {
377                Some(v) => s.push_str(&format!("<td style=\"{cell}\">{}</td>", esc(v))),
378                None => s.push_str(&format!("<td style=\"{cell}color:var(--studio-text-muted);\">·</td>")),
379            }
380        }
381        s.push_str("</tr>");
382    }
383    s.push_str("</table></div>");
384    s
385}
386
387/// Render a wh-question's answer ("Who is in Florida?" → "Beta") for the proof panel.
388fn format_answer_html(answers: &[String]) -> String {
389    let body = if answers.is_empty() {
390        "<span style=\"color:var(--studio-text-muted);\">no individual satisfies the question</span>".to_string()
391    } else {
392        format!(
393            "<span style=\"color:#34d399;font-weight:600;\">{}</span>",
394            esc(&answers.join(", "))
395        )
396    };
397    format!(
398        "<div style=\"font-family:monospace;font-size:14px;margin-bottom:12px;\"><span style=\"color:var(--studio-text-secondary);\">Answer: </span>{body}</div>"
399    )
400}
401
402/// Assemble the proof-panel HTML for a compiled theorem: the solved grid (when the form
403/// triggers it), then the wh-question answer or the certified derivation.
404fn theorem_proof_html(result: &TheoremCompileResult) -> String {
405    let mut html = String::new();
406    if let Some(grid) = &result.grid {
407        html.push_str(&format_grid_html(grid));
408    }
409    if let Some(answer) = &result.answer {
410        html.push_str(&format_answer_html(answer));
411    } else if let Some(derivation) = &result.derivation {
412        html.push_str(&format_derivation_html(derivation));
413    }
414    html
415}
416
417/// The proof-panel status line for a compiled theorem.
418fn theorem_proof_hint(result: &TheoremCompileResult) -> String {
419    if let Some(answer) = &result.answer {
420        if answer.is_empty() {
421            "No answer found.".to_string()
422        } else {
423            format!("Answer: {}", answer.join(", "))
424        }
425    } else if result.verified {
426        format!("Theorem '{}' proved!", result.name)
427    } else {
428        format!("Theorem '{}' — grid solved.", result.name)
429    }
430}
431
432/// Code mode output toggle - interpret output vs generated Rust
433#[derive(Clone, Copy, PartialEq, Eq, Default)]
434enum CodeOutputMode {
435    #[default]
436    Interpret,
437    Rust,
438}
439
440/// Logic mode output view — FOL interpretation or generated Rust. (SVA synthesis lives in
441/// Hardware mode, where signals/clocks/cycles have meaning; general FOL has none to assert.)
442#[derive(Clone, Copy, PartialEq, Eq, Default)]
443enum LogicView {
444    #[default]
445    Logic,
446    Rust,
447}
448
449/// Studio-specific styles that extend the shared responsive styles
450const STUDIO_STYLE: &str = r#"
451/* ============================================ */
452/* STUDIO PAGE - Design Tokens                  */
453/* ============================================ */
454:root {
455    --studio-bg: #0f1419;
456    --studio-panel-bg: #12161c;
457    --studio-elevated: #1a1f27;
458    --studio-border: rgba(255, 255, 255, 0.08);
459    --studio-border-hover: rgba(255, 255, 255, 0.15);
460    --studio-text: #e8eaed;
461    --studio-text-secondary: #9ca3af;
462    --studio-text-muted: #6b7280;
463    --studio-accent: #667eea;
464    /* Heights for positioning fixed sidebar below header */
465    --nav-height: 97px;
466    --toolbar-height: 49px;
467    --header-height: calc(var(--nav-height) + var(--toolbar-height));
468}
469
470@media (max-width: 980px) {
471    :root {
472        --nav-height: 89px;
473    }
474}
475
476@media (max-width: 768px) {
477    :root {
478        --nav-height: 81px;
479        --toolbar-height: 90px;
480    }
481}
482
483@media (max-width: 640px) {
484    :root {
485        --nav-height: 65px;
486    }
487}
488
489/* ============================================ */
490/* STUDIO PAGE - Desktop Layout                 */
491/* ============================================ */
492.studio-container {
493    display: flex;
494    flex-direction: column;
495    height: 100vh;
496    height: 100dvh;
497    background: var(--studio-bg);
498    color: var(--studio-text);
499    overflow: hidden;
500}
501
502/* Toolbar with mode toggle */
503.studio-toolbar {
504    display: flex;
505    align-items: center;
506    justify-content: space-between;
507    height: var(--toolbar-height);
508    padding: 0 16px;
509    background: var(--studio-panel-bg);
510    border-bottom: 1px solid var(--studio-border);
511    gap: 12px;
512    flex-shrink: 0;
513}
514
515.studio-toolbar-left {
516    display: flex;
517    align-items: center;
518    gap: 12px;
519}
520
521.studio-toolbar-center {
522    flex: 1;
523    display: flex;
524    justify-content: center;
525}
526
527.studio-toolbar-right {
528    display: flex;
529    align-items: center;
530    gap: 8px;
531}
532
533.sidebar-toggle-btn {
534    padding: 8px 12px;
535    background: rgba(255, 255, 255, 0.04);
536    border: 1px solid rgba(255, 255, 255, 0.08);
537    border-radius: 6px;
538    color: rgba(255, 255, 255, 0.7);
539    font-size: 14px;
540    cursor: pointer;
541    transition: all 0.15s ease;
542}
543
544.sidebar-toggle-btn:hover {
545    background: rgba(255, 255, 255, 0.08);
546    color: rgba(255, 255, 255, 0.9);
547}
548
549/* Main content area with optional sidebar */
550.studio-content {
551    flex: 1;
552    display: flex;
553    overflow: hidden;
554    position: relative;
555}
556
557/* Overlay to close sidebar when clicking outside (mobile) */
558.sidebar-overlay {
559    display: none;
560}
561
562@media (max-width: 768px) {
563    .sidebar-overlay {
564        display: block;
565        position: fixed;
566        top: calc(var(--header-height) + 10px);
567        left: 0;
568        right: 0;
569        bottom: 0;
570        background: rgba(0, 0, 0, 0.5);
571        z-index: 99;
572    }
573}
574
575/* Sidebar wrapper for controlled width */
576.studio-sidebar {
577    display: flex;
578    flex-shrink: 0;
579    overflow: hidden;
580}
581
582@media (max-width: 768px) {
583    .studio-sidebar {
584        position: fixed;
585        left: 0;
586        top: calc(var(--header-height) + 10px);
587        bottom: 0;
588        z-index: 100;
589        width: 280px !important;
590        min-width: 280px !important;
591        max-width: 280px !important;
592        box-shadow: 4px 0 20px rgba(0, 0, 0, 0.3);
593        background: #12161c;
594    }
595
596    /* Text color overrides for Safari compatibility */
597    .studio-sidebar .file-tree-item {
598        color: rgba(255, 255, 255, 0.9) !important;
599        -webkit-text-fill-color: rgba(255, 255, 255, 0.9);
600    }
601
602    .studio-sidebar .file-tree-item.selected {
603        color: #00d4ff !important;
604        -webkit-text-fill-color: #00d4ff;
605    }
606
607    .studio-sidebar .file-tree-item .name {
608        color: inherit;
609        -webkit-text-fill-color: inherit;
610    }
611}
612
613/* Desktop: 3-column panel layout */
614.studio-main {
615    flex: 1;
616    display: flex;
617    overflow: hidden;
618    gap: 1px;
619    background: var(--studio-border);
620}
621
622.studio-panel {
623    background: var(--studio-panel-bg);
624    display: flex;
625    flex-direction: column;
626    overflow: hidden;
627    min-width: 200px;
628}
629
630.studio-panel .panel-header {
631    padding: 0 20px;
632    height: 52px;
633    background: rgba(255, 255, 255, 0.02);
634    border-bottom: 1px solid var(--studio-border);
635    font-size: 16px;
636    font-weight: 600;
637    letter-spacing: 0.3px;
638    color: var(--studio-text);
639    display: flex;
640    justify-content: space-between;
641    align-items: center;
642    flex-shrink: 0;
643}
644
645.studio-panel .panel-content {
646    flex: 1;
647    min-height: 0;
648    overflow: auto;
649    -webkit-overflow-scrolling: touch;
650}
651
652/* Panel Resizers (desktop only) */
653.panel-resizer {
654    width: 4px;
655    background: var(--studio-border);
656    cursor: col-resize;
657    transition: background 0.2s ease;
658    flex-shrink: 0;
659}
660
661.panel-resizer:hover,
662.panel-resizer.active {
663    background: var(--studio-accent);
664}
665
666/* Format Toggle (Unicode/LaTeX) */
667.format-toggle {
668    display: flex;
669    gap: 4px;
670    background: rgba(255, 255, 255, 0.04);
671    border: 1px solid var(--studio-border);
672    border-radius: 6px;
673    padding: 2px;
674}
675
676.format-btn {
677    padding: 4px 10px;
678    border: none;
679    background: transparent;
680    color: var(--studio-text-muted);
681    font-size: 12px;
682    border-radius: 4px;
683    cursor: pointer;
684    transition: all 0.15s ease;
685    line-height: 1;
686}
687
688.format-btn:hover {
689    color: var(--studio-text);
690    background: rgba(255, 255, 255, 0.04);
691}
692
693.format-btn.active {
694    background: rgba(255, 255, 255, 0.08);
695    color: var(--studio-text);
696}
697
698/* Guide Bar - above panels */
699.studio-guide {
700    background: var(--studio-panel-bg);
701    border-bottom: 1px solid var(--studio-border);
702    flex-shrink: 0;
703}
704
705/* Execute button for Code mode */
706.execute-btn {
707    padding: 8px 14px;
708    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
709    border: none;
710    border-radius: 8px;
711    color: white;
712    font-size: 13px;
713    font-weight: 500;
714    cursor: pointer;
715    transition: all 0.15s ease;
716}
717
718/* Desktop: show short labels */
719.execute-btn .mobile-label {
720    display: none;
721}
722
723.execute-btn .desktop-label {
724    display: inline;
725}
726
727.execute-btn:hover {
728    transform: translateY(-1px);
729    box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
730}
731
732.execute-btn:active {
733    transform: translateY(0);
734}
735
736/* Mobile execute buttons */
737@media (max-width: 768px) {
738    .execute-btn {
739        padding: 6px 10px;
740        font-size: 12px;
741        white-space: nowrap;
742    }
743}
744
745@media (max-width: 480px) {
746    .execute-btn {
747        padding: 5px 8px;
748        font-size: 11px;
749    }
750}
751
752/* Output mode toggle for Code mode */
753.output-mode-toggle {
754    display: flex;
755    gap: 4px;
756    background: rgba(255, 255, 255, 0.04);
757    border: 1px solid var(--studio-border);
758    border-radius: 6px;
759    padding: 2px;
760}
761
762.output-mode-btn {
763    padding: 4px 10px;
764    border: none;
765    background: transparent;
766    color: var(--studio-text-muted);
767    font-size: 12px;
768    border-radius: 4px;
769    cursor: pointer;
770    transition: all 0.15s ease;
771    line-height: 1;
772}
773
774.output-mode-btn:hover {
775    color: var(--studio-text);
776    background: rgba(255, 255, 255, 0.04);
777}
778
779.output-mode-btn.active {
780    background: rgba(255, 255, 255, 0.08);
781    color: var(--studio-text);
782}
783
784/* Interpreter output display */
785.interpreter-output {
786    padding: 16px;
787    font-family: 'SF Mono', 'Fira Code', monospace;
788    font-size: 14px;
789    line-height: 1.6;
790}
791
792.interpreter-line {
793    margin-bottom: 4px;
794    color: #e8eaed;
795}
796
797.interpreter-error {
798    color: #e06c75;
799    padding: 12px;
800    background: rgba(224, 108, 117, 0.1);
801    border-radius: 6px;
802    margin-top: 8px;
803}
804
805.interpreter-empty {
806    color: rgba(255, 255, 255, 0.4);
807    text-align: center;
808    padding: 40px 20px;
809}
810
811/* ============================================ */
812/* STUDIO PAGE - Mobile Overrides               */
813/* ============================================ */
814
815/* Mode label: hidden on desktop, shown on mobile */
816.mode-label {
817    display: none;
818}
819
820@media (max-width: 768px) {
821    .studio-toolbar {
822        flex-wrap: wrap;
823        height: auto;
824        padding: 10px 12px;
825        gap: 10px;
826        position: relative;
827        z-index: 101;
828    }
829
830    /* First row: file toggle, mode selector */
831    .studio-toolbar-left {
832        flex-shrink: 0;
833        order: 1;
834    }
835
836    .studio-toolbar-center {
837        flex: 1;
838        align-items: center;
839        min-width: 0;
840        order: 2;
841    }
842
843    /* Second row: action buttons - full width */
844    .studio-toolbar-right {
845        flex-basis: 100%;
846        justify-content: center;
847        gap: 12px;
848        order: 3;
849        padding-top: 8px;
850        border-top: 1px solid rgba(255, 255, 255, 0.06);
851    }
852
853    /* Show mode toggle with "Mode:" label */
854    .mode-label {
855        display: block;
856        font-size: 12px;
857        font-weight: 500;
858        color: rgba(255, 255, 255, 0.5);
859        margin-right: 6px;
860        white-space: nowrap;
861    }
862
863    /* Full text on mobile buttons */
864    .execute-btn {
865        padding: 10px 16px;
866        font-size: 14px;
867    }
868
869    .execute-btn .mobile-label {
870        display: inline;
871    }
872
873    .execute-btn .desktop-label {
874        display: none;
875    }
876
877    /* Hide mobile tab bar - panels stack instead */
878    .mobile-tabs {
879        display: none !important;
880    }
881
882    /* Hide desktop resizers */
883    .panel-resizer {
884        display: none;
885    }
886
887    /* Stacked vertical panel layout */
888    .studio-main {
889        flex-direction: column;
890        gap: 0;
891        background: var(--studio-bg);
892    }
893
894    /* Both panels visible, stacked vertically */
895    .studio-panel {
896        min-width: unset;
897        min-height: 0;
898        width: 100% !important;
899    }
900
901    .studio-panel.mobile-expanded {
902        flex: var(--panel-flex, 1);
903        overflow: hidden;
904    }
905
906    .studio-panel.mobile-collapsed {
907        flex: 0 0 auto;
908    }
909
910    .studio-panel.mobile-collapsed .panel-content {
911        display: none;
912    }
913
914    /* Show panel headers with collapse affordance */
915    .studio-panel .panel-header {
916        display: flex !important;
917        cursor: pointer;
918        padding: 0 14px;
919        height: 44px;
920        font-size: 14px;
921    }
922
923    /* Collapse chevron indicator */
924    .studio-panel .panel-header::after {
925        content: '\25BC';
926        font-size: 10px;
927        color: rgba(255, 255, 255, 0.4);
928        margin-left: 8px;
929        transition: transform 0.15s ease;
930    }
931
932    .studio-panel.mobile-collapsed .panel-header::after {
933        content: '\25B6';
934    }
935
936    /* Divider between stacked panels */
937    .studio-panel + .studio-panel {
938        border-top: 1px solid var(--studio-border);
939    }
940
941    /* Hide Panel 3 (Console/Tree/Context) on mobile */
942    .studio-main > aside {
943        display: none !important;
944    }
945
946    /* Mobile-sized format toggle */
947    .format-toggle {
948        gap: 4px;
949        padding: 2px;
950        border-radius: 6px;
951    }
952
953    .format-btn {
954        padding: 6px 10px;
955        font-size: 12px;
956        border-radius: 4px;
957        min-height: 32px;
958        min-width: 32px;
959        display: flex;
960        align-items: center;
961        justify-content: center;
962    }
963
964    /* Footer constraints */
965    .studio-footer {
966        max-height: 30vh;
967        overflow: auto;
968    }
969
970    .mobile-panel-resizer {
971        display: flex;
972        align-items: center;
973        justify-content: center;
974        height: 16px;
975        cursor: row-resize;
976        flex-shrink: 0;
977        touch-action: none;
978        -webkit-tap-highlight-color: transparent;
979        z-index: 2;
980        position: relative;
981    }
982
983    .mobile-panel-resizer::after {
984        content: "";
985        position: absolute;
986        left: 50%;
987        top: 50%;
988        transform: translate(-50%, -50%);
989        width: 48px;
990        height: 4px;
991        border-radius: 2px;
992        background: rgba(255, 255, 255, 0.15);
993        transition: background 0.15s ease;
994    }
995
996    .mobile-panel-resizer:hover::after,
997    .mobile-panel-resizer.active::after {
998        background: rgba(102, 126, 234, 0.6);
999    }
1000}
1001
1002/* Extra small screens */
1003@media (max-width: 480px) {
1004    .format-btn {
1005        padding: 4px 8px;
1006        font-size: 11px;
1007    }
1008}
1009
1010/* Desktop: hide mobile resizer */
1011.mobile-panel-resizer {
1012    display: none;
1013}
1014
1015.studio-container.mobile-resizing {
1016    user-select: none;
1017    -webkit-user-select: none;
1018}
1019
1020/* Landscape mobile */
1021@media (max-height: 500px) and (orientation: landscape) {
1022    .studio-footer {
1023        max-height: 25vh;
1024    }
1025}
1026"#;
1027
1028/// Mobile tab options - changes based on mode
1029#[derive(Clone, Copy, PartialEq, Default)]
1030enum MobileTab {
1031    #[default]
1032    Panel1,
1033    Panel2,
1034    Panel3,
1035}
1036
1037/// Render the hardware knowledge graph as an animated radial SVG: signal nodes on a ring,
1038/// relations as directed edges. Pure SVG + CSS — edges animate a flowing dash ("signal
1039/// current") and nodes pulse, so a static spec reads like a live circuit. Returns "" if empty.
1040fn kg_svg(kg: &logicaffeine_compile::codegen_sva::hw_pipeline::KgSummary) -> String {
1041    use std::fmt::Write as _;
1042    let n = kg.nodes.len();
1043    if n == 0 {
1044        return String::new();
1045    }
1046    let (w, h) = (340.0_f64, 300.0_f64);
1047    let (cx, cy) = (w / 2.0, h / 2.0);
1048    let ring = (w.min(h) / 2.0 - 50.0).max(34.0);
1049    let node_r = 15.0_f64;
1050    let pos: Vec<(f64, f64)> = (0..n)
1051        .map(|i| {
1052            let ang = (i as f64) / (n as f64) * std::f64::consts::TAU - std::f64::consts::FRAC_PI_2;
1053            (cx + ring * ang.cos(), cy + ring * ang.sin())
1054        })
1055        .collect();
1056    let color = |role: &str| match role {
1057        "input" => "#60a5fa",
1058        "output" => "#4ade80",
1059        "clock" => "#fbbf24",
1060        _ => "#a78bfa",
1061    };
1062    let esc = |s: &str| s.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;");
1063
1064    let mut svg = String::new();
1065    let _ = write!(
1066        svg,
1067        r##"<svg viewBox="0 0 {w} {h}" width="100%" style="max-height:300px" xmlns="http://www.w3.org/2000/svg">"##
1068    );
1069    svg.push_str(
1070        r##"<defs><marker id="kgar" markerWidth="9" markerHeight="9" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="rgba(255,255,255,0.45)"/></marker></defs>"##,
1071    );
1072    svg.push_str(
1073        r##"<style>
1074.kge{stroke:rgba(167,139,250,0.55);stroke-width:1.5;fill:none;stroke-dasharray:5 4;animation:kgflow .9s linear infinite}
1075.kgn{stroke:rgba(255,255,255,0.9);stroke-width:1.5;animation:kgpulse 2.6s ease-in-out infinite}
1076.kgr{fill:rgba(255,255,255,0.5);font:9px ui-monospace,monospace;text-anchor:middle}
1077.kgl{fill:#e5e7eb;font:11px ui-sans-serif,system-ui;text-anchor:middle;font-weight:600}
1078@keyframes kgflow{to{stroke-dashoffset:-18}}
1079@keyframes kgpulse{0%,100%{opacity:1}50%{opacity:.72}}
1080</style>"##,
1081    );
1082    for l in &kg.links {
1083        let (x1, y1) = pos[l.from];
1084        let (x2, y2) = pos[l.to];
1085        let (dx, dy) = (x2 - x1, y2 - y1);
1086        let len = (dx * dx + dy * dy).sqrt().max(1.0);
1087        let (ux, uy) = (dx / len, dy / len);
1088        let (sx, sy) = (x1 + ux * node_r, y1 + uy * node_r);
1089        let (ex, ey) = (x2 - ux * (node_r + 6.0), y2 - uy * (node_r + 6.0));
1090        let _ = write!(
1091            svg,
1092            r##"<line class="kge" x1="{sx:.1}" y1="{sy:.1}" x2="{ex:.1}" y2="{ey:.1}" marker-end="url(#kgar)"/>"##
1093        );
1094        let (mx, my) = ((sx + ex) / 2.0, (sy + ey) / 2.0 - 2.0);
1095        let _ = write!(svg, r##"<text class="kgr" x="{mx:.1}" y="{my:.1}">{}</text>"##, esc(&l.relation));
1096    }
1097    for (i, node) in kg.nodes.iter().enumerate() {
1098        let (x, y) = pos[i];
1099        let _ = write!(
1100            svg,
1101            r##"<circle class="kgn" cx="{x:.1}" cy="{y:.1}" r="{node_r}" fill="{}"/>"##,
1102            color(&node.role)
1103        );
1104        let label = if node.width > 1 {
1105            format!("{}[{}]", node.name, node.width)
1106        } else {
1107            node.name.clone()
1108        };
1109        let _ = write!(svg, r##"<text class="kgl" x="{x:.1}" y="{:.1}">{}</text>"##, y + 28.0, esc(&label));
1110    }
1111    svg.push_str("</svg>");
1112    svg
1113}
1114
1115/// Render a counterexample as a logic-analyzer waveform SVG: boolean signals as digital
1116/// square waves, multi-bit registers as value-labeled buses, with a sweeping playhead. Pure
1117/// SVG + CSS. Returns "" if empty.
1118fn waveform_svg(wf: &logicaffeine_compile::codegen_sva::hw_pipeline::Waveform) -> String {
1119    use std::fmt::Write as _;
1120    let (ts, n) = (wf.timesteps, wf.signals.len());
1121    if ts == 0 || n == 0 {
1122        return String::new();
1123    }
1124    let (gutter, col_w, row_h, top) = (72.0_f64, 46.0_f64, 34.0_f64, 18.0_f64);
1125    let span = (ts as f64) * col_w;
1126    let (w, h) = (gutter + span + 10.0, top + (n as f64) * row_h + 6.0);
1127    let dur = (0.45 * ts as f64).max(1.2);
1128    let esc = |s: &str| s.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;");
1129
1130    let mut s = String::new();
1131    let _ = write!(s, r##"<svg viewBox="0 0 {w:.0} {h:.0}" width="100%" xmlns="http://www.w3.org/2000/svg">"##);
1132    let _ = write!(
1133        s,
1134        r##"<style>
1135.wfg{{stroke:rgba(255,255,255,0.07);stroke-width:1}}
1136.wft{{fill:none;stroke:#22d3ee;stroke-width:2;stroke-linejoin:round}}
1137.wfb{{fill:rgba(96,165,250,0.12);stroke:#60a5fa;stroke-width:1.5}}
1138.wfv{{fill:#dbeafe;font:11px ui-monospace,monospace;text-anchor:middle}}
1139.wfn{{fill:#e5e7eb;font:11px ui-monospace,monospace;text-anchor:end}}
1140.wftl{{fill:rgba(255,255,255,0.4);font:9px ui-monospace,monospace;text-anchor:middle}}
1141.wfph{{stroke:#f472b6;stroke-width:1.5;opacity:.8;animation:wfsweep {dur:.1}s linear infinite}}
1142@keyframes wfsweep{{from{{transform:translateX(0)}}to{{transform:translateX({span:.0}px)}}}}
1143</style>"##
1144    );
1145    for t in 0..ts {
1146        let x = gutter + (t as f64) * col_w;
1147        let _ = write!(s, r##"<line class="wfg" x1="{x:.1}" y1="{top:.1}" x2="{x:.1}" y2="{:.1}"/>"##, h - 4.0);
1148        let _ = write!(s, r##"<text class="wftl" x="{:.1}" y="12">t{t}</text>"##, x + col_w / 2.0);
1149    }
1150    let xe = gutter + span;
1151    let _ = write!(s, r##"<line class="wfg" x1="{xe:.1}" y1="{top:.1}" x2="{xe:.1}" y2="{:.1}"/>"##, h - 4.0);
1152
1153    for (ri, sig) in wf.signals.iter().enumerate() {
1154        let rtop = top + (ri as f64) * row_h;
1155        let (hi, lo) = (rtop + 6.0, rtop + row_h - 12.0);
1156        let mid = (hi + lo) / 2.0;
1157        let label = if sig.width > 1 { format!("{}[{}]", sig.name, sig.width) } else { sig.name.clone() };
1158        let _ = write!(s, r##"<text class="wfn" x="{:.1}" y="{:.1}">{}</text>"##, gutter - 8.0, mid + 4.0, esc(&label));
1159        if sig.width == 1 {
1160            let mut pts = String::new();
1161            for t in 0..ts {
1162                let x0 = gutter + (t as f64) * col_w;
1163                let lvl = match sig.values.get(t as usize) {
1164                    Some(Some(v)) => {
1165                        if *v != 0 {
1166                            hi
1167                        } else {
1168                            lo
1169                        }
1170                    }
1171                    _ => mid,
1172                };
1173                let _ = write!(pts, "{x0:.1},{lvl:.1} {:.1},{lvl:.1} ", x0 + col_w);
1174            }
1175            let _ = write!(s, r##"<polyline class="wft" points="{}"/>"##, pts.trim());
1176        } else {
1177            for t in 0..ts {
1178                if let Some(Some(v)) = sig.values.get(t as usize) {
1179                    let x0 = gutter + (t as f64) * col_w;
1180                    let _ = write!(
1181                        s,
1182                        r##"<rect class="wfb" x="{:.1}" y="{hi:.1}" width="{:.1}" height="{:.1}" rx="3"/>"##,
1183                        x0 + 2.0,
1184                        col_w - 4.0,
1185                        lo - hi
1186                    );
1187                    let _ = write!(s, r##"<text class="wfv" x="{:.1}" y="{:.1}">{v}</text>"##, x0 + col_w / 2.0, mid + 4.0);
1188                }
1189            }
1190        }
1191    }
1192    let _ = write!(s, r##"<line class="wfph" x1="{gutter:.1}" y1="{top:.1}" x2="{gutter:.1}" y2="{:.1}"/>"##, h - 4.0);
1193    s.push_str("</svg>");
1194    s
1195}
1196
1197fn wf_val(sig: &logicaffeine_compile::codegen_sva::hw_pipeline::WaveSignal, t: usize) -> u64 {
1198    sig.values.get(t).and_then(|v| *v).unwrap_or(0)
1199}
1200
1201/// One signalized-intersection PHASE: a maximal run of the trace with one identical signal
1202/// state, plus the human wall-clock duration we give it for the animation.
1203#[derive(Clone, Copy, Debug, PartialEq)]
1204struct TrafficPhase {
1205    ns: u64,
1206    ew: u64,
1207    nsl: u64,
1208    ewl: u64,
1209    ped: u64,
1210    start: f64,
1211    dur: f64,
1212}
1213
1214impl TrafficPhase {
1215    fn val(&self, pfx: &str) -> u64 {
1216        match pfx {
1217            "ns" => self.ns,
1218            "ew" => self.ew,
1219            "nsl" => self.nsl,
1220            "ewl" => self.ewl,
1221            "ped" => self.ped,
1222            _ => 0,
1223        }
1224    }
1225}
1226
1227/// The whole animation plan derived from a counterexample/witness trace: the phase sequence on a
1228/// seconds timeline, plus the index of the phase whose state violates safety (the crash).
1229struct TrafficPlan {
1230    phases: Vec<TrafficPhase>,
1231    /// First trace timestep of each phase (so callers can still sample the raw signals).
1232    firsts: Vec<usize>,
1233    total: f64,
1234    conflict: Option<usize>,
1235}
1236
1237/// What the NS-through platoon does during phase `i`: nothing visible (red/yellow), drive straight
1238/// through (a normal green), or drive to the crosswalk and stop (the crash phase).
1239#[derive(Clone, Copy, Debug, PartialEq)]
1240enum NsDrive {
1241    Hidden,
1242    Through,
1243    CrashStop,
1244}
1245
1246/// Collapse a trace into the phase plan. `None` unless both `ns` and `ew` are present.
1247fn traffic_plan(wf: &logicaffeine_compile::codegen_sva::hw_pipeline::Waveform) -> Option<TrafficPlan> {
1248    use logicaffeine_compile::codegen_sva::hw_pipeline::WaveSignal;
1249    let get = |name: &str| wf.signals.iter().find(|s| s.name.eq_ignore_ascii_case(name));
1250    let ns = get("ns")?;
1251    let ew = get("ew")?;
1252    let nsl = get("nsl");
1253    let ewl = get("ewl");
1254    let ped = get("ped");
1255    let n = (wf.timesteps as usize).max(1);
1256    let sv = |sig: &WaveSignal, t: usize| wf_val(sig, t);
1257    let svo = |sig: Option<&WaveSignal>, t: usize| sig.map_or(0u64, |s| wf_val(s, t));
1258    let state = |t: usize| (sv(ns, t), sv(ew, t), svo(nsl, t), svo(ewl, t), svo(ped, t));
1259
1260    let mut firsts: Vec<usize> = Vec::new();
1261    let mut prev: Option<(u64, u64, u64, u64, u64)> = None;
1262    for t in 0..n {
1263        let k = state(t);
1264        if Some(k) != prev {
1265            firsts.push(t);
1266            prev = Some(k);
1267        }
1268    }
1269    let states: Vec<(u64, u64, u64, u64, u64)> = firsts.iter().map(|&t| state(t)).collect();
1270    let nsc = states.len();
1271    let conflict = (0..nsc).find(|&i| {
1272        let (a, b, c, d, e) = states[i];
1273        (a != 0 && b != 0) || (e == 1 && (a != 0 || b != 0 || c != 0 || d != 0))
1274    });
1275    let mut phases = Vec::with_capacity(nsc);
1276    let mut acc = 0.0f64;
1277    for i in 0..nsc {
1278        let (a, b, c, d, e) = states[i];
1279        let dur = if Some(i) == conflict {
1280            3.6
1281        } else if e == 1 {
1282            7.0
1283        } else if a == 2 || b == 2 || c == 2 || d == 2 {
1284            1.3
1285        } else if a == 1 || b == 1 || c == 1 || d == 1 {
1286            2.6
1287        } else {
1288            1.0
1289        };
1290        phases.push(TrafficPhase { ns: a, ew: b, nsl: c, ewl: d, ped: e, start: acc, dur });
1291        acc += dur;
1292    }
1293    Some(TrafficPlan { phases, firsts, total: acc.max(1.0), conflict })
1294}
1295
1296fn ns_drive(plan: &TrafficPlan, i: usize) -> NsDrive {
1297    if plan.phases[i].ns != 1 {
1298        NsDrive::Hidden
1299    } else if plan.conflict == Some(i) {
1300        NsDrive::CrashStop
1301    } else {
1302        NsDrive::Through
1303    }
1304}
1305
1306/// The whole-second countdown shown on a phase's timer: `ceil(dur)`, `ceil(dur)-1`, … 1 (clamped
1307/// to a single digit).
1308fn countdown_seq(dur: f64) -> Vec<u32> {
1309    let secs = dur.ceil() as i32;
1310    (0..secs).map(|j| (secs - j).clamp(0, 9) as u32).collect()
1311}
1312
1313/// Easter egg: if a counterexample carries the signals of a signalized intersection
1314/// (`ns`/`ew` through, optional `nsl`/`ewl` protected left turns, optional `ped` pedestrian),
1315/// render a live intersection whose lamps step through the REAL trace — then flash CONFLICT at
1316/// the exact cycle the safety property is violated. Pure SVG + CSS, no JS. `None` otherwise.
1317///
1318/// Vehicle signals encode 0=red, 1=green, 2=yellow; `ped` encodes 0=don't-walk, 1=walk.
1319fn traffic_svg(wf: &logicaffeine_compile::codegen_sva::hw_pipeline::Waveform) -> Option<String> {
1320    use std::fmt::Write as _;
1321    use logicaffeine_compile::codegen_sva::hw_pipeline::WaveSignal;
1322    let get = |name: &str| wf.signals.iter().find(|s| s.name.eq_ignore_ascii_case(name));
1323    let ns = get("ns")?;
1324    let ew = get("ew")?;
1325    let nsl = get("nsl");
1326    let ewl = get("ewl");
1327    let ped = get("ped");
1328    let sv = |sig: &WaveSignal, t: usize| wf_val(sig, t);
1329
1330    // The phase plan + per-phase predicates are pure and unit-tested (see `traffic_viz_tests`).
1331    let plan = traffic_plan(wf)?;
1332    let scenes = &plan.firsts;
1333    let nsc = plan.phases.len();
1334    let conflict_scene = plan.conflict;
1335    let total = plan.total;
1336    let starts: Vec<f64> = plan.phases.iter().map(|ph| ph.start).collect();
1337    let durs: Vec<f64> = plan.phases.iter().map(|ph| ph.dur).collect();
1338    let p = |sec: f64| (sec / total * 100.0).clamp(0.0, 100.0);
1339
1340    // (prefix, signal, head_x, head_y, label, countdown_x, countdown_y)
1341    let mut vheads: Vec<(&str, &WaveSignal, f64, f64, &str, f64, f64)> = vec![
1342        ("ns", ns, 18.0, 16.0, "NS", 100.0, 17.0),
1343        ("ew", ew, 18.0, 278.0, "EW", 100.0, 279.0),
1344    ];
1345    if let Some(s) = nsl {
1346        vheads.push(("nsl", s, 184.0, 16.0, "NS \u{21B0}", 266.0, 17.0));
1347    }
1348    if let Some(s) = ewl {
1349        vheads.push(("ewl", s, 184.0, 278.0, "EW \u{21B0}", 266.0, 279.0));
1350    }
1351
1352    // A single seven-segment digit (lit segments only) at top-left (x, y).
1353    let seven_seg = |x: f64, y: f64, d: u32, color: &str| -> String {
1354        let segs: &str = match d {
1355            0 => "abcdef",
1356            1 => "bc",
1357            2 => "abged",
1358            3 => "abgcd",
1359            4 => "fgbc",
1360            5 => "afgcd",
1361            6 => "afgecd",
1362            7 => "abc",
1363            8 => "abcdefg",
1364            9 => "abcdfg",
1365            _ => "",
1366        };
1367        let (w, h, t) = (13.0f64, 22.0f64, 2.6f64);
1368        let half = h / 2.0;
1369        let mut g = String::new();
1370        for c in segs.chars() {
1371            let (rx, ry, rw, rh) = match c {
1372                'a' => (x + t, y, w - 2.0 * t, t),
1373                'b' => (x + w - t, y + t, t, half - 1.5 * t),
1374                'c' => (x + w - t, y + half + 0.5 * t, t, half - 1.5 * t),
1375                'd' => (x + t, y + h - t, w - 2.0 * t, t),
1376                'e' => (x, y + half + 0.5 * t, t, half - 1.5 * t),
1377                'f' => (x, y + t, t, half - 1.5 * t),
1378                'g' => (x + t, y + half - 0.5 * t, w - 2.0 * t, t),
1379                _ => (0.0, 0.0, 0.0, 0.0),
1380            };
1381            let _ = write!(g, r##"<rect x="{rx:.1}" y="{ry:.1}" width="{rw:.1}" height="{rh:.1}" rx="1" fill="{color}"/>"##);
1382        }
1383        g
1384    };
1385    // A small upright "walking person" glyph in `color`, centred on (cx, cy).
1386    let walk_icon = |cx: f64, cy: f64, color: &str| -> String {
1387        let mut g = String::new();
1388        let _ = write!(g, r##"<g stroke="{color}" stroke-width="2.6" stroke-linecap="round" fill="none">"##);
1389        let _ = write!(g, r##"<circle cx="{cx:.1}" cy="{:.1}" r="2.7" fill="{color}" stroke="none"/>"##, cy - 9.0);
1390        let _ = write!(g, r##"<line x1="{cx:.1}" y1="{:.1}" x2="{cx:.1}" y2="{:.1}"/>"##, cy - 6.0, cy + 1.0);
1391        let _ = write!(g, r##"<line x1="{cx:.1}" y1="{:.1}" x2="{:.1}" y2="{:.1}"/>"##, cy - 4.0, cx - 4.0, cy - 1.0);
1392        let _ = write!(g, r##"<line x1="{cx:.1}" y1="{:.1}" x2="{:.1}" y2="{:.1}"/>"##, cy - 4.0, cx + 4.0, cy - 2.0);
1393        let _ = write!(g, r##"<line x1="{cx:.1}" y1="{:.1}" x2="{:.1}" y2="{:.1}"/>"##, cy + 1.0, cx - 4.0, cy + 8.0);
1394        let _ = write!(g, r##"<line x1="{cx:.1}" y1="{:.1}" x2="{:.1}" y2="{:.1}"/>"##, cy + 1.0, cx + 4.0, cy + 7.0);
1395        g.push_str("</g>");
1396        g
1397    };
1398    // The orange raised-palm "DON'T WALK / STOP" hand, centred on (cx, cy).
1399    let hand_icon = |cx: f64, cy: f64| -> String {
1400        let mut g = String::from(r##"<g fill="#f59e0b">"##);
1401        let _ = write!(g, r##"<rect x="{:.1}" y="{:.1}" width="14" height="13" rx="4.5"/>"##, cx - 7.0, cy - 5.0);
1402        let _ = write!(g, r##"<rect x="{:.1}" y="{:.1}" width="3" height="8" rx="1.5"/>"##, cx - 7.0, cy - 11.0);
1403        let _ = write!(g, r##"<rect x="{:.1}" y="{:.1}" width="3" height="9" rx="1.5"/>"##, cx - 3.5, cy - 12.0);
1404        let _ = write!(g, r##"<rect x="{:.1}" y="{:.1}" width="3" height="9" rx="1.5"/>"##, cx + 0.5, cy - 12.0);
1405        let _ = write!(g, r##"<rect x="{:.1}" y="{:.1}" width="3" height="8" rx="1.5"/>"##, cx + 4.0, cy - 11.0);
1406        let _ = write!(g, r##"<rect x="{:.1}" y="{:.1}" width="5" height="3" rx="1.5"/>"##, cx - 10.5, cy - 1.0);
1407        g.push_str("</g>");
1408        g
1409    };
1410
1411    let mut style = String::from("<style>");
1412    // Lamp tracks (step-end): bright on the active colour, dim otherwise — one key per phase.
1413    for (pfx, sig, _, _, _, _, _) in &vheads {
1414        for (suffix, want) in [("R", 0u64), ("Y", 2), ("G", 1)] {
1415            let _ = write!(style, "@keyframes {pfx}{suffix}{{");
1416            for i in 0..nsc {
1417                let op = if sv(sig, scenes[i]) == want { "1" } else { "0.12" };
1418                let _ = write!(style, "{:.2}%{{opacity:{op}}}", p(starts[i]));
1419            }
1420            let last = if sv(sig, scenes[nsc - 1]) == want { "1" } else { "0.12" };
1421            let _ = write!(style, "100%{{opacity:{last}}}}}");
1422        }
1423    }
1424    // Pedestrian WALK / STOP icon swap (full on/off).
1425    if let Some(pp) = ped {
1426        for (name, want) in [("pedWALK", 1u64), ("pedSTOP", 0)] {
1427            let _ = write!(style, "@keyframes {name}{{");
1428            for i in 0..nsc {
1429                let op = if sv(pp, scenes[i]) == want { "1" } else { "0" };
1430                let _ = write!(style, "{:.2}%{{opacity:{op}}}", p(starts[i]));
1431            }
1432            let last = if sv(pp, scenes[nsc - 1]) == want { "1" } else { "0" };
1433            let _ = write!(style, "100%{{opacity:{last}}}}}");
1434        }
1435    }
1436    // Vehicle platoons — cars are released on GREEN, drive straight THROUGH, and are hidden on
1437    // red (none left drifting/parked in the road). Opacity snaps per phase (step-end); position
1438    // glides (linear); nested groups compose them. On a crash run the NS platoon drives to the
1439    // crosswalk and FREEZES on impact, so its real lead car is the one that strikes the
1440    // pedestrian. Lead glyph base y=-30 / x=-30 → +240px lands on the south crosswalk, +360px
1441    // drives clear off-screen.
1442    {
1443        let _ = write!(style, "@keyframes nsplat_op{{");
1444        for i in 0..nsc {
1445            let o = if ns_drive(&plan, i) == NsDrive::Hidden { "0" } else { "1" };
1446            let _ = write!(style, "{:.2}%{{opacity:{o}}}", p(starts[i]));
1447        }
1448        let lo = if ns_drive(&plan, nsc - 1) == NsDrive::Hidden { "0" } else { "1" };
1449        let _ = write!(style, "100%{{opacity:{lo}}}}}");
1450        let _ = write!(style, "@keyframes nsplat_mv{{");
1451        for i in 0..nsc {
1452            let t0 = starts[i];
1453            match ns_drive(&plan, i) {
1454                NsDrive::Through => {
1455                    let _ = write!(style, "{:.2}%{{transform:translateY(0px)}}", p(t0));
1456                    let _ = write!(style, "{:.2}%{{transform:translateY(360px)}}", p(t0 + durs[i]));
1457                }
1458                NsDrive::CrashStop => {
1459                    let _ = write!(style, "{:.2}%{{transform:translateY(0px)}}", p(t0));
1460                    let _ = write!(style, "{:.2}%{{transform:translateY(240px)}}", p(t0 + 1.8));
1461                    let _ = write!(style, "{:.2}%{{transform:translateY(240px)}}", p(t0 + durs[i]));
1462                }
1463                NsDrive::Hidden => {
1464                    let _ = write!(style, "{:.2}%{{transform:translateY(0px)}}", p(t0));
1465                }
1466            }
1467        }
1468        let lty = match ns_drive(&plan, nsc - 1) {
1469            NsDrive::Through => "360px",
1470            NsDrive::CrashStop => "240px",
1471            NsDrive::Hidden => "0px",
1472        };
1473        let _ = write!(style, "100%{{transform:translateY({lty})}}}}");
1474    }
1475    {
1476        let _ = write!(style, "@keyframes ewplat_op{{");
1477        for i in 0..nsc {
1478            let o = if sv(ew, scenes[i]) == 1 { "1" } else { "0" };
1479            let _ = write!(style, "{:.2}%{{opacity:{o}}}", p(starts[i]));
1480        }
1481        let lo = if sv(ew, scenes[nsc - 1]) == 1 { "1" } else { "0" };
1482        let _ = write!(style, "100%{{opacity:{lo}}}}}");
1483        let _ = write!(style, "@keyframes ewplat_mv{{");
1484        for i in 0..nsc {
1485            let t0 = starts[i];
1486            if sv(ew, scenes[i]) == 1 {
1487                let _ = write!(style, "{:.2}%{{transform:translateX(0px)}}", p(t0));
1488                let _ = write!(style, "{:.2}%{{transform:translateX(360px)}}", p(t0 + durs[i]));
1489            } else {
1490                let _ = write!(style, "{:.2}%{{transform:translateX(0px)}}", p(t0));
1491            }
1492        }
1493        let lty = if sv(ew, scenes[nsc - 1]) == 1 { "360px" } else { "0px" };
1494        let _ = write!(style, "100%{{transform:translateX({lty})}}}}");
1495    }
1496    // Safe runs: the pedestrian crosses the south crosswalk and reaches the far side unharmed.
1497    if ped.is_some() && conflict_scene.is_none() {
1498        let pp = ped.unwrap();
1499        let mut off = 0.0f64;
1500        let _ = write!(style, "@keyframes pedcross{{");
1501        for i in 0..nsc {
1502            let vis = if sv(pp, scenes[i]) == 1 { "1" } else { "0" };
1503            let _ = write!(style, "{:.2}%{{transform:translateX({off:.1}px);opacity:{vis}}}", p(starts[i]));
1504            if sv(pp, scenes[i]) == 1 {
1505                off += 11.0 * durs[i];
1506            }
1507        }
1508        let _ = write!(style, "100%{{transform:translateX({off:.1}px);opacity:0}}}}");
1509    }
1510    // Crash runs: the pedestrian walks into the lane and is struck as the queue reaches them.
1511    if let Some(csi) = conflict_scene {
1512        let c0 = starts[csi];
1513        let impact = c0 + 1.8;
1514        let _ = write!(style, "@keyframes doomwalk{{0%{{opacity:0;transform:translateX(0px)}}{:.2}%{{opacity:0;transform:translateX(0px)}}{:.2}%{{opacity:1;transform:translateX(0px)}}{:.2}%{{opacity:1;transform:translateX(30px)}}{:.2}%{{opacity:0}}100%{{opacity:0}}}}", p(c0), p(c0 + 0.2), p(impact), p(impact + 0.25));
1515        let _ = write!(style, "@keyframes impact{{0%{{opacity:0}}{:.2}%{{opacity:0}}{:.2}%{{opacity:1}}100%{{opacity:0.95}}}}", p(impact), p(impact + 0.2));
1516    }
1517    // Protected-left turn cars: one vehicle curves through the intersection during each nsl/ewl
1518    // GREEN, so the left phases aren't visually dead. An L-path (approach, then turn).
1519    let nsl_g = nsl.and_then(|sig| (0..nsc).find(|&i| sv(sig, scenes[i]) == 1));
1520    let ewl_g = ewl.and_then(|sig| (0..nsc).find(|&i| sv(sig, scenes[i]) == 1));
1521    if let Some(i) = nsl_g {
1522        let (w0, wm, w1) = (p(starts[i]), p(starts[i] + durs[i] * 0.5), p(starts[i] + durs[i]));
1523        let we = (w1 + 1.0).min(100.0);
1524        let _ = write!(style, "@keyframes nslturn{{0%{{opacity:0;transform:translate(0px,-210px)}}{w0:.2}%{{opacity:1;transform:translate(0px,-210px)}}{wm:.2}%{{opacity:1;transform:translate(0px,0px)}}{w1:.2}%{{opacity:1;transform:translate(175px,0px)}}{we:.2}%{{opacity:0;transform:translate(175px,0px)}}100%{{opacity:0}}}}");
1525    }
1526    if let Some(i) = ewl_g {
1527        let (w0, wm, w1) = (p(starts[i]), p(starts[i] + durs[i] * 0.5), p(starts[i] + durs[i]));
1528        let we = (w1 + 1.0).min(100.0);
1529        let _ = write!(style, "@keyframes ewlturn{{0%{{opacity:0;transform:translate(-210px,0px)}}{w0:.2}%{{opacity:1;transform:translate(-210px,0px)}}{wm:.2}%{{opacity:1;transform:translate(0px,0px)}}{w1:.2}%{{opacity:1;transform:translate(0px,175px)}}{we:.2}%{{opacity:0;transform:translate(0px,175px)}}100%{{opacity:0}}}}");
1530    }
1531    // Seven-segment countdown timers: one digit per second of each lit phase, on the pedestrian
1532    // signal (amber) and beside each vehicle head (green while go, amber while caution).
1533    let mut cds: Vec<(f64, f64, &str, usize)> = Vec::new();
1534    if let Some(pp) = ped {
1535        for i in 0..nsc {
1536            if sv(pp, scenes[i]) == 1 {
1537                cds.push((286.0, 68.0, "#fbbf24", i));
1538            }
1539        }
1540    }
1541    for (_pfx, sig, _hx, _hy, _lab, cdx, cdy) in &vheads {
1542        for i in 0..nsc {
1543            let v = sv(sig, scenes[i]);
1544            if v == 1 || v == 2 {
1545                cds.push((*cdx, *cdy, if v == 1 { "#22c55e" } else { "#fbbf24" }, i));
1546            }
1547        }
1548    }
1549    let mut cd_body = String::new();
1550    let mut cdc = 0usize;
1551    for (x, y, color, i) in &cds {
1552        for (k, val) in countdown_seq(durs[*i]).into_iter().enumerate() {
1553            let t0 = starts[*i] + k as f64;
1554            let t1 = (t0 + 1.0).min(starts[*i] + durs[*i]);
1555            let name = format!("cd{cdc}");
1556            cdc += 1;
1557            let _ = write!(style, "@keyframes {name}{{0%{{opacity:0}}{:.2}%{{opacity:0}}{:.2}%{{opacity:1}}{:.2}%{{opacity:0}}100%{{opacity:0}}}}", p(t0), p(t0), p(t1));
1558            let _ = write!(cd_body, r##"<g class="st" style="animation-name:{name}">{}</g>"##, seven_seg(*x, *y, val, *color));
1559        }
1560    }
1561    let _ = write!(style, ".st{{animation-duration:{total:.1}s;animation-timing-function:step-end;animation-iteration-count:infinite}}");
1562    let _ = write!(style, ".fl{{animation-duration:{total:.1}s;animation-timing-function:linear;animation-iteration-count:infinite}}");
1563    style.push_str("</style>");
1564
1565    let mut s = String::new();
1566    let _ = write!(s, r##"<svg viewBox="0 0 320 320" width="100%" style="max-height:320px" xmlns="http://www.w3.org/2000/svg">"##);
1567    s.push_str(&style);
1568    // cross roads + lane markings
1569    s.push_str(r##"<rect x="0" y="128" width="320" height="64" fill="#26262b"/><rect x="128" y="0" width="64" height="320" fill="#26262b"/>"##);
1570    s.push_str(r##"<line x1="0" y1="160" x2="320" y2="160" stroke="#6b5d2e" stroke-width="2" stroke-dasharray="11 9"/><line x1="160" y1="0" x2="160" y2="320" stroke="#6b5d2e" stroke-width="2" stroke-dasharray="11 9"/>"##);
1571    // crosswalk hatching across the south leg
1572    if ped.is_some() {
1573        for i in 0..6 {
1574            let x = 133.0 + i as f64 * 10.0;
1575            let _ = write!(s, r##"<rect x="{x:.0}" y="202" width="5" height="22" fill="rgba(255,255,255,0.22)"/>"##);
1576        }
1577    }
1578    // NS platoon (cyan, southbound) + EW platoon (pink, eastbound). Outer group snaps opacity per
1579    // phase (released on green / hidden on red); inner group glides the position. Three cars each
1580    // so a green release reads as a small platoon; lead car base y=-30 / x=-30.
1581    s.push_str(r##"<g class="st" style="animation-name:nsplat_op"><g class="fl" style="animation-name:nsplat_mv">"##);
1582    for k in 0..3 {
1583        let y = -30 - k * 52;
1584        let _ = write!(s, r##"<rect x="139" y="{y}" width="18" height="30" rx="3" fill="#38bdf8"/><rect x="143" y="{}" width="10" height="4" rx="1" fill="rgba(0,0,0,0.35)"/>"##, y + 6);
1585    }
1586    s.push_str("</g></g>");
1587    s.push_str(r##"<g class="st" style="animation-name:ewplat_op"><g class="fl" style="animation-name:ewplat_mv">"##);
1588    for k in 0..3 {
1589        let x = -30 - k * 52;
1590        let _ = write!(s, r##"<rect x="{x}" y="163" width="30" height="18" rx="3" fill="#f472b6"/><rect x="{}" y="167" width="4" height="10" rx="1" fill="rgba(0,0,0,0.35)"/>"##, x + 6);
1591    }
1592    s.push_str("</g></g>");
1593    // Safe run: the pedestrian crosses unharmed.
1594    if ped.is_some() && conflict_scene.is_none() {
1595        let _ = write!(s, r##"<g class="fl" style="animation-name:pedcross">{}</g>"##, walk_icon(120.0, 209.0, "#fde047"));
1596    }
1597    // Protected-left turn cars (one per left phase) — cyan for NS-left, pink for EW-left.
1598    if nsl_g.is_some() {
1599        s.push_str(r##"<g class="fl" style="animation-name:nslturn"><rect x="140" y="142" width="16" height="16" rx="3" fill="#38bdf8"/><rect x="143" y="145" width="10" height="4" rx="1" fill="rgba(0,0,0,0.35)"/></g>"##);
1600    }
1601    if ewl_g.is_some() {
1602        s.push_str(r##"<g class="fl" style="animation-name:ewlturn"><rect x="140" y="142" width="16" height="16" rx="3" fill="#f472b6"/><rect x="143" y="145" width="10" height="4" rx="1" fill="rgba(0,0,0,0.35)"/></g>"##);
1603    }
1604    // Crash run: the doomed pedestrian who walks into the lane and is struck.
1605    if conflict_scene.is_some() && ped.is_some() {
1606        let _ = write!(s, r##"<g class="fl" style="animation-name:doomwalk">{}</g>"##, walk_icon(120.0, 209.0, "#fde047"));
1607    }
1608    // vehicle signal heads + dim "8" backdrop for each countdown
1609    for (pfx, _sig, x, y, label, cdx, cdy) in &vheads {
1610        let (rx, yx, gx) = (x + 16.0, x + 39.0, x + 62.0);
1611        let cy = y + 14.0;
1612        let lab_y = if *y < 100.0 { y - 4.0 } else { y + 42.0 };
1613        let _ = write!(s, r##"<rect x="{x:.0}" y="{y:.0}" width="78" height="28" rx="7" fill="#141418" stroke="rgba(255,255,255,0.15)"/>"##);
1614        let _ = write!(s, r##"<circle class="st" style="animation-name:{pfx}R" cx="{rx:.0}" cy="{cy:.0}" r="9" fill="#ef4444"/><circle class="st" style="animation-name:{pfx}Y" cx="{yx:.0}" cy="{cy:.0}" r="9" fill="#fbbf24"/><circle class="st" style="animation-name:{pfx}G" cx="{gx:.0}" cy="{cy:.0}" r="9" fill="#22c55e"/>"##);
1615        let _ = write!(s, r##"<text x="{:.0}" y="{lab_y:.0}" fill="rgba(255,255,255,0.6)" text-anchor="middle" font-size="11" font-family="ui-monospace,monospace">{label}</text>"##, x + 39.0);
1616        s.push_str(&seven_seg(*cdx, *cdy, 8, "rgba(255,255,255,0.06)"));
1617    }
1618    // pedestrian signal head: WALK person / STOP hand + a dim "8" countdown backdrop
1619    if ped.is_some() {
1620        s.push_str(r##"<rect x="238" y="54" width="72" height="52" rx="6" fill="#141418" stroke="rgba(255,255,255,0.15)"/>"##);
1621        let _ = write!(s, r##"<g class="st" style="animation-name:pedWALK">{}</g>"##, walk_icon(258.0, 80.0, "#34d399"));
1622        let _ = write!(s, r##"<g class="st" style="animation-name:pedSTOP">{}</g>"##, hand_icon(258.0, 80.0));
1623        s.push_str(&seven_seg(286.0, 68.0, 8, "rgba(255,255,255,0.06)"));
1624        s.push_str(r##"<text x="274" y="100" fill="rgba(255,255,255,0.55)" text-anchor="middle" font-size="9" font-family="ui-monospace,monospace">PED</text>"##);
1625    }
1626    // the ticking countdown digits, drawn over their dim backdrops
1627    s.push_str(&cd_body);
1628    // Impact: the felled pedestrian (red), an impact burst, and the banner.
1629    if conflict_scene.is_some() {
1630        let mut g = String::from(r##"<g class="st" style="animation-name:impact">"##);
1631        g.push_str(r##"<circle cx="150" cy="207" r="15" fill="rgba(251,191,36,0.6)"/>"##);
1632        g.push_str(r##"<path d="M150,190 L155,202 L168,207 L155,212 L150,224 L145,212 L132,207 L145,202 Z" fill="#fbbf24"/>"##);
1633        if ped.is_some() {
1634            g.push_str(r##"<ellipse cx="174" cy="216" rx="12" ry="4.5" fill="#ef4444"/><circle cx="160" cy="214" r="4" fill="#ef4444"/>"##);
1635            g.push_str(r##"<text x="160" y="116" fill="#fca5a5" text-anchor="middle" font-size="13" font-weight="700" font-family="ui-sans-serif">⚠ CAR HITS PEDESTRIAN</text>"##);
1636        } else {
1637            g.push_str(r##"<text x="160" y="116" fill="#fca5a5" text-anchor="middle" font-size="14" font-weight="700" font-family="ui-sans-serif">⚠ CONFLICT</text>"##);
1638        }
1639        g.push_str("</g>");
1640        s.push_str(&g);
1641    }
1642    s.push_str("</svg>");
1643    Some(s)
1644}
1645
1646#[cfg(test)]
1647mod traffic_viz_tests {
1648    use super::{countdown_seq, ns_drive, traffic_plan, traffic_svg, NsDrive};
1649    use logicaffeine_compile::codegen_sva::hw_pipeline::{Waveform, WaveSignal};
1650
1651    fn sig(name: &str, vals: &[u64]) -> WaveSignal {
1652        WaveSignal {
1653            name: name.to_string(),
1654            width: 2,
1655            values: vals.iter().map(|&v| Some(v)).collect(),
1656        }
1657    }
1658    fn wf(signals: Vec<WaveSignal>, n: u32) -> Waveform {
1659        Waveform { timesteps: n, signals }
1660    }
1661
1662    /// A non-conflicting trace: NS green (2 clocks), NS yellow (1), all-red (1), WALK (2).
1663    fn safe_trace() -> Waveform {
1664        wf(
1665            vec![
1666                sig("ns", &[1, 1, 2, 0, 0, 0]),
1667                sig("ew", &[0, 0, 0, 0, 0, 0]),
1668                sig("ped", &[0, 0, 0, 0, 1, 1]),
1669            ],
1670            6,
1671        )
1672    }
1673
1674    #[test]
1675    fn collapses_consecutive_identical_states_into_phases() {
1676        let plan = traffic_plan(&safe_trace()).unwrap();
1677        assert_eq!(plan.phases.len(), 4, "ns-green, ns-yellow, all-red, walk");
1678        assert_eq!(plan.firsts, vec![0, 2, 3, 4]);
1679        assert_eq!(plan.phases[0].ns, 1);
1680        assert_eq!(plan.phases[1].ns, 2);
1681        assert_eq!(plan.phases[3].ped, 1);
1682    }
1683
1684    #[test]
1685    fn phase_durations_are_human_paced_and_cumulative() {
1686        let plan = traffic_plan(&safe_trace()).unwrap();
1687        assert_eq!(plan.phases[0].dur, 2.6, "green");
1688        assert_eq!(plan.phases[1].dur, 1.3, "yellow");
1689        assert_eq!(plan.phases[2].dur, 1.0, "all-red");
1690        assert_eq!(plan.phases[3].dur, 7.0, "walk");
1691        assert_eq!(plan.phases[0].start, 0.0);
1692        assert_eq!(plan.phases[1].start, 2.6);
1693        assert!((plan.phases[3].start - (2.6 + 1.3 + 1.0)).abs() < 1e-9);
1694        assert!((plan.total - (2.6 + 1.3 + 1.0 + 7.0)).abs() < 1e-9);
1695    }
1696
1697    #[test]
1698    fn exclusive_movements_have_no_conflict() {
1699        assert_eq!(traffic_plan(&safe_trace()).unwrap().conflict, None);
1700    }
1701
1702    #[test]
1703    fn pedestrian_walking_into_green_traffic_is_a_conflict() {
1704        let plan = traffic_plan(&wf(
1705            vec![sig("ns", &[1, 1]), sig("ew", &[0, 0]), sig("ped", &[1, 1])],
1706            2,
1707        ))
1708        .unwrap();
1709        assert_eq!(plan.phases.len(), 1);
1710        assert_eq!(plan.conflict, Some(0));
1711        assert_eq!(plan.phases[0].dur, 3.6, "the crash phase lingers");
1712    }
1713
1714    #[test]
1715    fn ns_cars_are_hidden_unless_ns_through_is_green() {
1716        // This is the bug the user saw: cars must NEVER sit in the road on red/yellow/walk.
1717        let plan = traffic_plan(&safe_trace()).unwrap();
1718        assert_eq!(ns_drive(&plan, 0), NsDrive::Through, "ns green → drive through");
1719        assert_eq!(ns_drive(&plan, 1), NsDrive::Hidden, "ns yellow → hidden");
1720        assert_eq!(ns_drive(&plan, 2), NsDrive::Hidden, "all-red → hidden");
1721        assert_eq!(ns_drive(&plan, 3), NsDrive::Hidden, "pedestrian walk → hidden");
1722    }
1723
1724    #[test]
1725    fn the_crash_car_stops_at_the_crosswalk_not_drives_through() {
1726        let plan = traffic_plan(&wf(
1727            vec![sig("ns", &[1, 1]), sig("ew", &[0, 0]), sig("ped", &[1, 1])],
1728            2,
1729        ))
1730        .unwrap();
1731        assert_eq!(ns_drive(&plan, 0), NsDrive::CrashStop);
1732    }
1733
1734    #[test]
1735    fn countdown_counts_down_whole_seconds() {
1736        assert_eq!(countdown_seq(7.0), vec![7, 6, 5, 4, 3, 2, 1]);
1737        assert_eq!(countdown_seq(2.6), vec![3, 2, 1]);
1738        assert_eq!(countdown_seq(1.3), vec![2, 1]);
1739        assert_eq!(countdown_seq(1.0), vec![1]);
1740    }
1741
1742    #[test]
1743    fn every_animation_name_used_in_the_svg_has_a_matching_keyframes_block() {
1744        // Guards against the dangling-reference class of bug (body using a keyframe that the
1745        // style section never defined).
1746        let svg = traffic_svg(&wf(
1747            vec![
1748                sig("ns", &[1, 1, 0, 0, 0, 0]),
1749                sig("ew", &[0, 0, 0, 1, 1, 0]),
1750                sig("nsl", &[0, 0, 0, 0, 0, 1]),
1751                sig("ewl", &[0, 0, 0, 0, 0, 0]),
1752                sig("ped", &[0, 0, 0, 0, 0, 0]),
1753            ],
1754            6,
1755        ))
1756        .expect("renders an intersection");
1757        let mut rest = svg.as_str();
1758        while let Some(pos) = rest.find("animation-name:") {
1759            rest = &rest[pos + "animation-name:".len()..];
1760            let end = rest
1761                .find(|c: char| !(c.is_ascii_alphanumeric() || c == '_'))
1762                .unwrap_or(rest.len());
1763            let name = &rest[..end];
1764            assert!(
1765                svg.contains(&format!("@keyframes {name}{{"))
1766                    || svg.contains(&format!("@keyframes {name} {{")),
1767                "animation-name {name} has no @keyframes definition"
1768            );
1769        }
1770    }
1771
1772    #[test]
1773    fn non_intersection_traces_do_not_render() {
1774        // No ns/ew signals → not a traffic trace.
1775        assert!(traffic_svg(&wf(vec![sig("a", &[0, 1]), sig("b", &[1, 0])], 2)).is_none());
1776    }
1777}
1778
1779/// Keep the address bar on the canonical shareable URL for the open file
1780/// without pushing a history entry.
1781#[cfg(target_arch = "wasm32")]
1782fn sync_studio_url(vfs_path: &str) {
1783    crate::ui::router::replace_bar_url(&crate::ui::router::studio_file_url(vfs_path));
1784}
1785
1786#[component(lazy)]
1787pub fn Studio(file: Option<String>) -> Element {
1788    // Mode state
1789    let mut mode = use_signal(|| StudioMode::Logic);
1790
1791    // File browser state
1792    let mut sidebar_open = use_signal(|| true);
1793    let mut file_tree = use_signal(FileNode::root); // Start empty - no fallback, show real errors
1794    let mut current_file = use_signal(|| None::<String>);
1795    let mut vfs_error = use_signal(|| None::<String>); // Track VFS errors for display
1796    let mut vfs_is_fallback = use_signal(|| false); // Track if using IndexedDB fallback
1797
1798    // Logic mode state
1799    let mut input = use_signal(String::new);
1800    let mut result = use_signal(|| CompileResult {
1801        logic: None,
1802        simple_logic: None,
1803        kripke_logic: None,
1804        ast: None,
1805        readings: Vec::new(),
1806        simple_readings: Vec::new(),
1807        kripke_readings: Vec::new(),
1808        tokens: Vec::new(),
1809        error: None,
1810    });
1811    let mut format = use_signal(|| OutputFormat::SimpleFOL);
1812
1813    // Proof panel state for Logic mode
1814    let mut proof_text = use_signal(String::new);
1815    let mut proof_status = use_signal(|| ProofStatus::Idle);
1816    let mut proof_hint = use_signal(|| None::<String>);
1817    // The current ProofExpr for the proof engine
1818    let mut current_proof_expr = use_signal(|| None::<ProofExpr>);
1819    // Knowledge base (axioms/premises) for the proof engine
1820    let mut knowledge_base = use_signal(Vec::<ProofExpr>::new);
1821    // Logic mode output view: FOL (Logic) vs extracted Rust
1822    let mut logic_output_mode = use_signal(|| LogicView::Logic);
1823    let mut generated_logic_rust = use_signal(String::new);
1824
1825    // Code mode state (imperative .logos)
1826    let mut code_input = use_signal(String::new);
1827    let mut code_output_mode = use_signal(|| CodeOutputMode::Interpret);
1828    let mut interpreter_result = use_signal(|| InterpreterResult {
1829        lines: vec![],
1830        error: None,
1831    });
1832    let mut generated_rust = use_signal(String::new);
1833    // Code-mode debugger drawer (bottom-docked, additive — see `DebugDrawer`).
1834    let mut debugging = use_signal(|| false);
1835
1836    // Math mode state (vernacular/theorem proving)
1837    let mut math_input = use_signal(String::new);
1838    let mut math_repl = use_signal(Repl::new);
1839    let mut math_output = use_signal(Vec::<ReplLine>::new);
1840    // Math mode output view: REPL output (Interpret) vs extracted Rust
1841    let mut math_output_mode = use_signal(|| CodeOutputMode::Interpret);
1842    let mut generated_math_rust = use_signal(String::new);
1843
1844    // Hardware mode state (English hardware spec -> SVA + in-browser proving)
1845    let mut hw_input = use_signal(String::new);
1846    // Output view: SVA (Interpret) vs extracted Rust runtime monitor.
1847    let mut hw_output_mode = use_signal(|| CodeOutputMode::Interpret);
1848    let mut hw_sva = use_signal(String::new);
1849    let mut hw_psl = use_signal(String::new);
1850    let mut hw_signals = use_signal(Vec::<String>::new);
1851    // Certified-equivalence verdict (and counterexample) from our in-browser prover.
1852    let mut hw_proof = use_signal(String::new);
1853    let mut hw_proof_ok = use_signal(|| None::<bool>);
1854    // Raw counterexample bindings (name@t -> "0"/"1"), rendered as a waveform when present.
1855    let mut hw_counterexample = use_signal(Vec::<(String, String)>::new);
1856    // Knowledge graph of the spec (signals + relations), rendered as an animated SVG.
1857    let mut hw_kg = use_signal(logicaffeine_compile::codegen_sva::hw_pipeline::KgSummary::default);
1858    let mut generated_hw_rust = use_signal(String::new);
1859    let mut hw_error = use_signal(|| None::<String>);
1860
1861    // Shows a "Working…" affordance while a Run/Compile handler is in flight, so a
1862    // medium-length operation doesn't look like a dead tab.
1863    let mut busy = use_signal(|| false);
1864
1865    // Desktop panel resizing state
1866    let mut sidebar_width = use_signal(|| 240.0f64);
1867    let mut left_width = use_signal(|| 35.0f64);
1868    let mut right_width = use_signal(|| 25.0f64);
1869    let mut resizing = use_signal(|| None::<&'static str>);
1870
1871    // Mobile tab state
1872    let mut active_tab = use_signal(|| MobileTab::Panel1);
1873
1874    // Mobile panel collapse state
1875    let mut editor_expanded = use_signal(|| true);
1876    let mut output_expanded = use_signal(|| true);
1877
1878    // Touch gesture state for swipe detection
1879    let mut touch_start_x = use_signal(|| 0.0f64);
1880    let mut touch_start_y = use_signal(|| 0.0f64);
1881
1882    // Mobile vertical split: Panel 1 height as % of studio-main
1883    let mut mobile_split = use_signal(|| 50.0f64);
1884
1885    // VFS initialization flag
1886    let mut vfs_initialized = use_signal(|| false);
1887
1888    // Cached VFS handle: acquire the OPFS worker once, then reuse it for file
1889    // switches instead of spawning (and leaking) a fresh worker each time.
1890    #[cfg(target_arch = "wasm32")]
1891    let mut vfs_handle = use_signal(|| None::<WebVfs>);
1892
1893    // Initialize VFS and seed examples on mount
1894    #[cfg(target_arch = "wasm32")]
1895    use_effect(move || {
1896        if *vfs_initialized.read() {
1897            return;
1898        }
1899        vfs_initialized.set(true);
1900
1901        let file = file.clone();
1902        spawn(async move {
1903            // Get platform VFS with automatic fallback (OPFS -> IndexedDB)
1904            match get_platform_vfs_with_fallback().await {
1905                Ok(vfs) => {
1906                    // Cache the handle so file switches reuse this worker instead
1907                    // of spawning (and leaking) a fresh one each time.
1908                    vfs_handle.set(Some(vfs.clone()));
1909                    if vfs.is_fallback() {
1910                        vfs_is_fallback.set(true);
1911                    }
1912
1913                    // Seed example files if they don't exist
1914                    if let Err(e) = seed_examples(&vfs).await {
1915                        vfs_error.set(Some(format!("Failed to seed examples: {:?}", e)));
1916                    }
1917
1918                    // Build file tree from VFS
1919                    let mut root = FileNode::root();
1920                    match load_dir_recursive(&vfs, "/", &mut root).await {
1921                        Ok(()) => {
1922                            if !root.children.is_empty() {
1923                                file_tree.set(root);
1924                            } else {
1925                                vfs_error.set(Some("VFS returned empty tree - no files found".to_string()));
1926                            }
1927                        }
1928                        Err(e) => {
1929                            vfs_error.set(Some(format!("Failed to load file tree: {:?}", e)));
1930                        }
1931                    }
1932
1933                    // Open the file from the route's `file` query prop, or the default
1934                    let file_to_load = file
1935                        .map(|f| {
1936                            // Normalize path - ensure it starts with /
1937                            if f.starts_with('/') {
1938                                f
1939                            } else {
1940                                format!("/{}", f)
1941                            }
1942                        })
1943                        .unwrap_or_else(|| "/examples/logic/prover-demo.logic".to_string());
1944
1945                    // Load the file and detect mode from path/extension
1946                    if let Ok(content) = vfs.read_to_string(&file_to_load).await {
1947                        current_file.set(Some(file_to_load.clone()));
1948                        sync_studio_url(&file_to_load);
1949
1950                        let ext = file_to_load.rsplit('.').next().unwrap_or("").to_lowercase();
1951                        let is_math_dir = file_to_load.contains("/math/") || file_to_load.contains("/examples/math");
1952
1953                        if is_math_dir || ext == "math" || ext == "vernac" {
1954                            // Math mode
1955                            mode.set(StudioMode::Math);
1956                            math_input.set(content);
1957                        } else if ext == "logos" {
1958                            // Code mode - auto-run
1959                            mode.set(StudioMode::Code);
1960                            code_input.set(content.clone());
1961                            let interp_result = interpret_for_ui_baseline(&content).await;
1962                            interpreter_result.set(interp_result);
1963                        } else {
1964                            // Logic mode (default, handles .logic files)
1965                            mode.set(StudioMode::Logic);
1966                            input.set(content.clone());
1967
1968                            if content.contains("## Theorem:") {
1969                                // Compile as theorem
1970                                let theorem_result = compile_theorem_for_ui(&content);
1971                                if theorem_result.error.is_none() {
1972                                    result.set(CompileResult {
1973                                        logic: theorem_result.goal_string.clone(),
1974                                        simple_logic: theorem_result.goal_string.clone(),
1975                                        kripke_logic: None,
1976                                        ast: None,
1977                                        readings: Vec::new(),
1978                                        simple_readings: Vec::new(),
1979                                        kripke_readings: Vec::new(),
1980                                        tokens: Vec::new(),
1981                                        error: None,
1982                                    });
1983
1984                                    knowledge_base.write().clear();
1985                                    for premise in &theorem_result.premises {
1986                                        knowledge_base.write().push(premise.clone());
1987                                    }
1988
1989                                    if let Some(goal) = theorem_result.goal.clone() {
1990                                        current_proof_expr.set(Some(goal));
1991                                    }
1992
1993                                    let html = theorem_proof_html(&theorem_result);
1994                                    if !html.is_empty() {
1995                                        proof_text.set(html);
1996                                        proof_status.set(if theorem_result.verified {
1997                                            ProofStatus::Success
1998                                        } else {
1999                                            ProofStatus::Idle
2000                                        });
2001                                        proof_hint.set(Some(theorem_proof_hint(&theorem_result)));
2002                                    } else {
2003                                        proof_status.set(ProofStatus::Idle);
2004                                        proof_hint.set(Some(format!(
2005                                            "Theorem '{}' ready. {} premise(s) loaded.",
2006                                            theorem_result.name,
2007                                            knowledge_base.read().len()
2008                                        )));
2009                                    }
2010                                }
2011                            } else {
2012                                // Plain English sentences
2013                                let sentences: Vec<&str> = content
2014                                    .lines()
2015                                    .filter(|line| {
2016                                        let trimmed = line.trim();
2017                                        !trimmed.is_empty()
2018                                        && !trimmed.starts_with('#')
2019                                        && !trimmed.starts_with("--")
2020                                    })
2021                                    .collect();
2022
2023                                if !sentences.is_empty() {
2024                                    let all_text = sentences.join("\n");
2025                                    let compiled = compile_for_ui(&all_text);
2026                                    result.set(compiled);
2027
2028                                    let first_sentence = sentences[0];
2029                                    let proof_result = compile_for_proof(first_sentence);
2030                                    if let Some(expr) = proof_result.proof_expr {
2031                                        current_proof_expr.set(Some(expr));
2032                                    }
2033                                }
2034                            }
2035                        }
2036                    }
2037                }
2038                Err(e) => {
2039                    vfs_error.set(Some(format!("VFS INIT FAILED: {:?}", e)));
2040                }
2041            }
2042        });
2043    });
2044
2045    // On native, no VFS - just show empty tree with message
2046    #[cfg(not(target_arch = "wasm32"))]
2047    use_effect(move || {
2048        if *vfs_initialized.read() {
2049            return;
2050        }
2051        vfs_initialized.set(true);
2052        vfs_error.set(Some("VFS not available on native".to_string()));
2053    });
2054
2055    // Close sidebar on mobile by default (runs once on mount)
2056    #[cfg(target_arch = "wasm32")]
2057    {
2058        let mut sidebar_init_done = use_signal(|| false);
2059        use_effect(move || {
2060            if *sidebar_init_done.read() {
2061                return;
2062            }
2063            sidebar_init_done.set(true);
2064
2065            if let Some(window) = web_sys::window() {
2066                let width = window.inner_width().ok().and_then(|v| v.as_f64()).unwrap_or(1024.0);
2067                if width <= 768.0 {
2068                    sidebar_open.set(false);
2069                }
2070            }
2071        });
2072    }
2073
2074    // Logic mode input handler - compiles for both UI and proof engine
2075    // Logic mode keystroke handler: just store the text. The heavy compile/prove/grid runs
2076    // on the Execute button (`handle_logic_execute`), mirroring Code mode's Run — so typing
2077    // never triggers a per-keystroke solve.
2078    let handle_logic_input = move |new_value: String| {
2079        input.set(new_value);
2080    };
2081
2082    // Code mode: Run button handler (interpret) with streaming output
2083    let handle_code_run = move |_| {
2084        let code = code_input.read().clone();
2085        // Switch to Output tab (Panel2) on mobile and switch to Output mode
2086        active_tab.set(MobileTab::Panel2);
2087        code_output_mode.set(CodeOutputMode::Interpret);
2088        // Clear previous output
2089        interpreter_result.set(InterpreterResult {
2090            lines: vec![],
2091            error: None,
2092        });
2093        spawn(async move {
2094            // Create streaming callback that updates the signal as output arrives
2095            let callback = Rc::new(RefCell::new(move |line: String| {
2096                // Update the signal with the new line
2097                interpreter_result.write().lines.push(line);
2098            }));
2099
2100            // Route the interpreter's file I/O to the Studio's VFS (OPFS/IndexedDB),
2101            // so the standard-library I/O vocabulary works in the browser. The
2102            // worker-backed handle is wasm-only; the native build does no file I/O.
2103            #[cfg(target_arch = "wasm32")]
2104            let vfs = vfs_handle
2105                .read()
2106                .clone()
2107                .map(|w| std::sync::Arc::new(w) as std::sync::Arc<dyn Vfs>);
2108            #[cfg(not(target_arch = "wasm32"))]
2109            let vfs: Option<std::sync::Arc<dyn Vfs>> = None;
2110            let result = interpret_streaming_with_vfs(&code, callback, vfs).await;
2111            // Set final result (includes any error)
2112            interpreter_result.set(result);
2113        });
2114    };
2115
2116    // Code mode: Compile button handler (generate Rust)
2117    // Uses generate_rust_code which works on WASM
2118    let handle_code_compile = move |_| {
2119        let code = code_input.read().clone();
2120        // Switch to Output tab (Panel2) on mobile
2121        active_tab.set(MobileTab::Panel2);
2122        busy.set(true);
2123        spawn(async move {
2124            // Yield once so the "Working…" affordance paints before the work.
2125            #[cfg(target_arch = "wasm32")]
2126            gloo_timers::future::TimeoutFuture::new(0).await;
2127            match generate_rust_code(&code) {
2128                Ok(rust_code) => {
2129                    generated_rust.set(rust_code);
2130                    code_output_mode.set(CodeOutputMode::Rust);
2131                }
2132                Err(e) => {
2133                    interpreter_result.set(InterpreterResult {
2134                        lines: vec![],
2135                        error: Some(format!("Compile error: {:?}", e)),
2136                    });
2137                    code_output_mode.set(CodeOutputMode::Interpret);
2138                }
2139            }
2140            busy.set(false);
2141        });
2142    };
2143
2144    // Math mode execute handler (vernacular REPL).
2145    // Re-runs from a fresh kernel each press so output is REPLACED, not appended
2146    // (mirrors Code mode's Run — no manual Clear needed). The editor holds the
2147    // full program, so a fresh run is idempotent.
2148    let handle_math_execute = move |_| {
2149        active_tab.set(MobileTab::Panel2);
2150        math_output_mode.set(CodeOutputMode::Interpret);
2151        let code = math_input.read().clone();
2152        busy.set(true);
2153        spawn(async move {
2154            #[cfg(target_arch = "wasm32")]
2155            gloo_timers::future::TimeoutFuture::new(0).await;
2156
2157            let statements = parse_math_statements(&code);
2158            let mut repl = Repl::new();
2159            let mut lines = Vec::new();
2160            for stmt in statements {
2161                match repl.execute(&stmt) {
2162                    Ok(output) => lines.push(ReplLine::success(stmt, output)),
2163                    Err(e) => lines.push(ReplLine::error(stmt, e.to_string())),
2164                }
2165            }
2166            math_repl.set(repl);
2167            math_output.set(lines);
2168            busy.set(false);
2169        });
2170    };
2171
2172    // Math mode compile handler: rebuild the kernel context from the editor, then
2173    // extract every user-defined Definition/Inductive into one Rust module.
2174    let handle_math_compile = move |_| {
2175        active_tab.set(MobileTab::Panel2);
2176        let code = math_input.read().clone();
2177        busy.set(true);
2178        spawn(async move {
2179            #[cfg(target_arch = "wasm32")]
2180            gloo_timers::future::TimeoutFuture::new(0).await;
2181
2182            let statements = parse_math_statements(&code);
2183            let mut repl = Repl::new();
2184            for stmt in statements {
2185                let _ = repl.execute(&stmt);
2186            }
2187            let rust = match extract_math_rust(repl.context()) {
2188                Ok(rust) => rust,
2189                Err(e) => format!("// extraction error: {e}"),
2190            };
2191            math_repl.set(repl);
2192            generated_math_rust.set(rust);
2193            math_output_mode.set(CodeOutputMode::Rust);
2194            busy.set(false);
2195        });
2196    };
2197
2198    // Logic mode execute handler: compile the theorem (or sentences), prove it, and render
2199    // the result — the solved grid, the wh-question answer, or the certified derivation.
2200    // Like Code mode's Run, the heavy work happens on this button, not on every keystroke.
2201    let handle_logic_execute = move |_| {
2202        active_tab.set(MobileTab::Panel2);
2203        logic_output_mode.set(LogicView::Logic);
2204        let new_value = input.read().clone();
2205
2206        if new_value.contains("## Theorem:") {
2207            let theorem_result = compile_theorem_for_ui(&new_value);
2208
2209            if let Some(err) = theorem_result.error {
2210                result.set(CompileResult {
2211                    logic: None,
2212                    simple_logic: None,
2213                    kripke_logic: None,
2214                    ast: None,
2215                    readings: Vec::new(),
2216                    simple_readings: Vec::new(),
2217                    kripke_readings: Vec::new(),
2218                    tokens: Vec::new(),
2219                    error: Some(err.clone()),
2220                });
2221                proof_status.set(ProofStatus::Failed(err));
2222                current_proof_expr.set(None);
2223                knowledge_base.write().clear();
2224            } else {
2225                result.set(CompileResult {
2226                    logic: theorem_result.goal_string.clone(),
2227                    simple_logic: theorem_result.goal_string.clone(),
2228                    kripke_logic: None,
2229                    ast: None,
2230                    readings: Vec::new(),
2231                    simple_readings: Vec::new(),
2232                    kripke_readings: Vec::new(),
2233                    tokens: Vec::new(),
2234                    error: None,
2235                });
2236
2237                knowledge_base.write().clear();
2238                for premise in &theorem_result.premises {
2239                    knowledge_base.write().push(premise.clone());
2240                }
2241
2242                if let Some(goal) = theorem_result.goal.clone() {
2243                    current_proof_expr.set(Some(goal));
2244                }
2245
2246                let html = theorem_proof_html(&theorem_result);
2247                if !html.is_empty() {
2248                    proof_text.set(html);
2249                    proof_status.set(if theorem_result.verified {
2250                        ProofStatus::Success
2251                    } else {
2252                        ProofStatus::Idle
2253                    });
2254                    proof_hint.set(Some(theorem_proof_hint(&theorem_result)));
2255                } else {
2256                    proof_status.set(ProofStatus::Idle);
2257                    proof_hint.set(Some(format!(
2258                        "Theorem '{}' ready. {} premise(s) loaded. Click Auto to prove.",
2259                        theorem_result.name,
2260                        knowledge_base.read().len()
2261                    )));
2262                    proof_text.set(String::new());
2263                }
2264            }
2265        } else {
2266            let sentences: Vec<&str> = new_value
2267                .lines()
2268                .filter(|line| {
2269                    let trimmed = line.trim();
2270                    !trimmed.is_empty()
2271                        && !trimmed.starts_with('#')
2272                        && !trimmed.starts_with("--")
2273                })
2274                .collect();
2275
2276            if !sentences.is_empty() {
2277                let all_text = sentences.join("\n");
2278                let compiled = compile_for_ui(&all_text);
2279                result.set(compiled);
2280
2281                knowledge_base.write().clear();
2282
2283                let first_sentence = sentences[0];
2284                let proof_result = compile_for_proof(first_sentence);
2285                if let Some(expr) = proof_result.proof_expr {
2286                    current_proof_expr.set(Some(expr));
2287                    proof_status.set(ProofStatus::Idle);
2288                    proof_hint.set(Some("Enter premises or click Auto to prove.".to_string()));
2289                } else if let Some(err) = proof_result.error {
2290                    current_proof_expr.set(None);
2291                    proof_status.set(ProofStatus::Failed(err));
2292                }
2293            } else {
2294                result.set(CompileResult {
2295                    logic: None,
2296                    simple_logic: None,
2297                    kripke_logic: None,
2298                    ast: None,
2299                    readings: Vec::new(),
2300                    simple_readings: Vec::new(),
2301                    kripke_readings: Vec::new(),
2302                    tokens: Vec::new(),
2303                    error: None,
2304                });
2305                current_proof_expr.set(None);
2306                knowledge_base.write().clear();
2307                proof_text.set(String::new());
2308                proof_status.set(ProofStatus::Idle);
2309            }
2310        }
2311    };
2312
2313    // Logic mode compile handler: extract Rust where there's constructive content
2314    // (a `## Theorem:` block / definitions); honest note otherwise.
2315    let handle_logic_compile = move |_| {
2316        active_tab.set(MobileTab::Panel2);
2317        let text = input.read().clone();
2318        busy.set(true);
2319        spawn(async move {
2320            #[cfg(target_arch = "wasm32")]
2321            gloo_timers::future::TimeoutFuture::new(0).await;
2322
2323            let rust = match extract_logic_rust(&text) {
2324                Ok(rust) => rust,
2325                Err(e) => format!("// extraction error: {e}"),
2326            };
2327            generated_logic_rust.set(rust);
2328            logic_output_mode.set(LogicView::Rust);
2329            busy.set(false);
2330        });
2331    };
2332
2333    // Logic mode: Tactic button handler - FULL PROOF ENGINE INTEGRATION
2334    let handle_tactic = {
2335        let result_signal = result.clone();
2336        move |tactic: Tactic| {
2337            // Get the current ProofExpr (compiled earlier)
2338            let maybe_goal = current_proof_expr.read().clone();
2339            let kb = knowledge_base.read().clone();
2340            let logic_str = result_signal.read().simple_logic.clone();
2341
2342            match maybe_goal {
2343                Some(goal) => {
2344                    proof_status.set(ProofStatus::Proving);
2345
2346                    match tactic {
2347                        Tactic::Auto => {
2348                            // Run the actual BackwardChainer proof engine!
2349                            let mut engine = BackwardChainer::new();
2350
2351                            // Add knowledge base as axioms
2352                            for axiom in &kb {
2353                                engine.add_axiom(axiom.clone());
2354                            }
2355
2356                            // Attempt to prove
2357                            match engine.prove(goal.clone()) {
2358                                Ok(derivation) => {
2359                                    // Format the derivation tree for display
2360                                    let tree_display = format_derivation_html(&derivation);
2361                                    proof_text.set(tree_display);
2362                                    proof_status.set(ProofStatus::Success);
2363                                    proof_hint.set(Some("Proof found! The derivation tree shows the inference steps.".to_string()));
2364                                }
2365                                Err(e) => {
2366                                    // Get a Socratic hint for why it failed
2367                                    let hint = suggest_hint(&goal, &kb, &[]);
2368                                    proof_text.set(format!(
2369                                        "<span class=\"rule\">Auto-prove failed</span>\n\nGoal: {}\n\nError: {}",
2370                                        logic_str.as_deref().unwrap_or("(no expression)"),
2371                                        e
2372                                    ));
2373                                    proof_status.set(ProofStatus::Failed(format!("{}", e)));
2374                                    proof_hint.set(Some(hint.text));
2375                                }
2376                            }
2377                        }
2378                        Tactic::ModusPonens => {
2379                            let hint = suggest_hint(&goal, &kb, &[]);
2380                            proof_text.set(format!(
2381                                "<span class=\"rule\">Modus Ponens</span>\n\nFrom P\u{2192}Q and P, derive Q\n\nCurrent goal: {}\n\nKnowledge base has {} axiom(s)",
2382                                logic_str.as_deref().unwrap_or("(none)"),
2383                                kb.len()
2384                            ));
2385                            proof_status.set(ProofStatus::Idle);
2386                            proof_hint.set(Some(hint.text));
2387                        }
2388                        Tactic::UniversalInst => {
2389                            let hint = suggest_hint(&goal, &kb, &[]);
2390                            proof_text.set(format!(
2391                                "<span class=\"rule\">\u{2200} Elimination</span>\n\nFrom \u{2200}x.P(x), derive P(c)\n\nCurrent goal: {}",
2392                                logic_str.as_deref().unwrap_or("(none)")
2393                            ));
2394                            proof_status.set(ProofStatus::Idle);
2395                            proof_hint.set(Some(hint.text));
2396                        }
2397                        Tactic::ExistentialIntro => {
2398                            let hint = suggest_hint(&goal, &kb, &[]);
2399                            proof_text.set(format!(
2400                                "<span class=\"rule\">\u{2203} Introduction</span>\n\nFrom P(c), derive \u{2203}x.P(x)\n\nCurrent goal: {}",
2401                                logic_str.as_deref().unwrap_or("(none)")
2402                            ));
2403                            proof_status.set(ProofStatus::Idle);
2404                            proof_hint.set(Some(hint.text));
2405                        }
2406                        Tactic::Induction => {
2407                            let hint = suggest_hint(&goal, &kb, &[]);
2408                            proof_text.set(format!(
2409                                "<span class=\"rule\">Induction</span>\n\nBase case + Inductive step\n\nCurrent goal: {}",
2410                                logic_str.as_deref().unwrap_or("(none)")
2411                            ));
2412                            proof_status.set(ProofStatus::Idle);
2413                            proof_hint.set(Some(hint.text));
2414                        }
2415                        Tactic::Rewrite => {
2416                            let hint = suggest_hint(&goal, &kb, &[]);
2417                            proof_text.set(format!(
2418                                "<span class=\"rule\">Rewrite</span>\n\nUse equality to substitute\n\nCurrent goal: {}",
2419                                logic_str.as_deref().unwrap_or("(none)")
2420                            ));
2421                            proof_status.set(ProofStatus::Idle);
2422                            proof_hint.set(Some(hint.text));
2423                        }
2424                    }
2425                }
2426                None => {
2427                    proof_status.set(ProofStatus::Failed("No logic expression to prove. Enter a sentence first.".to_string()));
2428                    proof_text.set(String::new());
2429                    proof_hint.set(Some("Enter an English sentence above to generate a logical formula.".to_string()));
2430                }
2431            }
2432        }
2433    };
2434
2435    // Build context entries from Math REPL
2436    let (definitions, inductives) = {
2437        let repl_guard = math_repl.read();
2438        let ctx = repl_guard.context();
2439        let mut defs = Vec::new();
2440        let mut inds = Vec::new();
2441
2442        for (name, ty, body) in ctx.iter_definitions() {
2443            defs.push(ContextEntry {
2444                name: name.to_string(),
2445                ty: format!("{}", ty),
2446                body: Some(format!("{}", body)),
2447                kind: EntryKind::Definition,
2448            });
2449        }
2450
2451        for (name, ty) in ctx.iter_inductives() {
2452            inds.push(ContextEntry {
2453                name: name.to_string(),
2454                ty: format!("{}", ty),
2455                body: None,
2456                kind: EntryKind::Inductive,
2457            });
2458        }
2459
2460        (defs, inds)
2461    };
2462
2463    // Hardware mode handlers.
2464    //
2465    // `synthesize_sva_from_spec` is pure Rust (no Z3), so synthesis and the
2466    // PSL / Rust-monitor emission all run in the browser. A `SynthesizedSva`
2467    // carries the body + signals + kind, which we lift into an `SvaProperty`
2468    // for the emitters.
2469    fn hw_property_from_synth(
2470        synth: &logicaffeine_compile::codegen_sva::fol_to_sva::SynthesizedSva,
2471    ) -> logicaffeine_compile::codegen_sva::SvaProperty {
2472        use logicaffeine_compile::codegen_sva::{SvaAssertionKind, SvaProperty, sanitize_property_name};
2473        let kind = match synth.kind.as_str() {
2474            "cover" => SvaAssertionKind::Cover,
2475            "assume" => SvaAssertionKind::Assume,
2476            _ => SvaAssertionKind::Assert,
2477        };
2478        let name = if synth.signals.is_empty() {
2479            "p_property".to_string()
2480        } else {
2481            sanitize_property_name(&synth.signals.join("_"))
2482        };
2483        SvaProperty { name, clock: "clk".to_string(), body: synth.body.clone(), kind }
2484    }
2485
2486    // Synthesize a spec and push the SVA / PSL / signals (or error) into the
2487    // Hardware-mode signals. Shared by the Execute button and the file loader.
2488    #[allow(clippy::too_many_arguments)]
2489    fn load_hardware_spec(
2490        content: &str,
2491        mut hw_sva: Signal<String>,
2492        mut hw_psl: Signal<String>,
2493        mut hw_signals: Signal<Vec<String>>,
2494        mut hw_proof: Signal<String>,
2495        mut hw_proof_ok: Signal<Option<bool>>,
2496        mut hw_counterexample: Signal<Vec<(String, String)>>,
2497        mut hw_kg: Signal<logicaffeine_compile::codegen_sva::hw_pipeline::KgSummary>,
2498        mut hw_error: Signal<Option<String>>,
2499    ) {
2500        if content.trim().is_empty() {
2501            hw_sva.set(String::new());
2502            hw_psl.set(String::new());
2503            hw_signals.write().clear();
2504            hw_proof.set(String::new());
2505            hw_proof_ok.set(None);
2506            hw_counterexample.write().clear();
2507            hw_kg.set(Default::default());
2508            hw_error.set(None);
2509            return;
2510        }
2511        // Pigeonhole spec (`pigeons: N`) → the live viz panel solves PHP(N) directly from the editor.
2512        // Execute clears the other Hardware outputs and posts the certified verdict to the proof line.
2513        if let Some(pspec) = crate::ui::pages::pigeonhole_viz::parse_pigeonhole_spec(content) {
2514            hw_sva.set(String::new());
2515            hw_psl.set(String::new());
2516            hw_signals.write().clear();
2517            hw_counterexample.write().clear();
2518            hw_kg.set(Default::default());
2519            hw_error.set(None);
2520            hw_proof.set(format!(
2521                "\u{2717} PHP({0}) \u{2014} {0} pigeons can't fit {1} holes. Certified UNSAT by our prover (maximum matching + symmetry breaking), no Z3.",
2522                pspec.pigeons,
2523                pspec.holes()
2524            ));
2525            hw_proof_ok.set(Some(false));
2526            return;
2527        }
2528        // Register-allocation spec (`registers:` + `name: start-end`) → the live viz panel renders
2529        // the certified allocation directly from the editor, so Execute just clears the other
2530        // Hardware outputs (no SVA synthesis, no spurious "not a hardware property" error).
2531        if content.contains("registers:") || content.contains("Registers:") {
2532            hw_sva.set(String::new());
2533            hw_psl.set(String::new());
2534            hw_signals.write().clear();
2535            hw_proof.set(String::new());
2536            hw_proof_ok.set(None);
2537            hw_counterexample.write().clear();
2538            hw_kg.set(Default::default());
2539            hw_error.set(None);
2540            return;
2541        }
2542        // RTL/Verilog input → bounded model checking + k-induction (no Z3), distinct from the
2543        // English-spec → SVA synthesis path.
2544        if content.contains("module") && content.contains("endmodule") {
2545            use logicaffeine_compile::codegen_sva::rtl::parse_transition_system;
2546            use logicaffeine_proof::bmc::{BmcOutcome, InductionOutcome};
2547            hw_psl.set(String::new());
2548            hw_signals.write().clear();
2549            hw_kg.set(Default::default());
2550            let to_ce = |trace: Vec<(String, bool)>| -> Vec<(String, String)> {
2551                trace
2552                    .into_iter()
2553                    .map(|(n, v)| (n, if v { "1" } else { "0" }.to_string()))
2554                    .collect()
2555            };
2556            match parse_transition_system(content) {
2557                Ok(ts) => {
2558                    hw_sva.set(format!(
2559                        "// RTL transition system \u{2014} {} register(s)\n// bounded model checking + k-induction (no Z3)",
2560                        ts.registers.len()
2561                    ));
2562                    match ts.prove_invariant(4) {
2563                        InductionOutcome::Proven => {
2564                            hw_proof.set("\u{2713} Always holds \u{2014} proven for every reachable state (k-induction, no Z3)".to_string());
2565                            hw_proof_ok.set(Some(true));
2566                            // No counterexample to show — animate a concrete WITNESS run of the
2567                            // proven-safe machine so the diagram still comes alive (no conflict).
2568                            match ts.witness_trace(18) {
2569                                Some(trace) => hw_counterexample.set(to_ce(trace)),
2570                                None => hw_counterexample.write().clear(),
2571                            }
2572                        }
2573                        InductionOutcome::CounterexampleAt { k, trace } => {
2574                            hw_proof.set(format!("\u{2717} Can break at step {k} \u{2014} counterexample below"));
2575                            hw_proof_ok.set(Some(false));
2576                            hw_counterexample.set(to_ce(trace));
2577                        }
2578                        InductionOutcome::NotInductive => match ts.bmc(28) {
2579                            BmcOutcome::CounterexampleAt { k, trace } => {
2580                                hw_proof.set(format!("\u{2717} Can break at step {k} \u{2014} counterexample below"));
2581                                hw_proof_ok.set(Some(false));
2582                                hw_counterexample.set(to_ce(trace));
2583                            }
2584                            BmcOutcome::NoneWithin(n) => {
2585                                hw_proof.set(format!("No failure found within {n} steps \u{2014} couldn't prove it always holds at this depth"));
2586                                hw_proof_ok.set(None);
2587                                hw_counterexample.write().clear();
2588                            }
2589                            BmcOutcome::Unsupported => {
2590                                hw_proof.set("This property isn't expressible yet \u{2014} try a simpler one".to_string());
2591                                hw_proof_ok.set(None);
2592                                hw_counterexample.write().clear();
2593                            }
2594                        },
2595                        InductionOutcome::Unsupported => {
2596                            hw_proof.set("This property isn't expressible yet \u{2014} try a simpler one".to_string());
2597                            hw_proof_ok.set(None);
2598                            hw_counterexample.write().clear();
2599                        }
2600                    }
2601                    hw_error.set(None);
2602                }
2603                Err(e) => {
2604                    hw_sva.set(String::new());
2605                    hw_proof.set(String::new());
2606                    hw_proof_ok.set(None);
2607                    hw_counterexample.write().clear();
2608                    hw_error.set(Some(format!("RTL parse error: {}", e.message)));
2609                }
2610            }
2611            return;
2612        }
2613        // Signal-design input ("<movement> conflicts with <movement>, …") → synthesize a
2614        // conflict-free phase plan with our own certified SAT solver (fewest phases, Z3-free).
2615        if content.to_lowercase().contains("conflict") {
2616            use logicaffeine_compile::codegen_sva::signal_design::design_from_spec;
2617            hw_psl.set(String::new());
2618            hw_kg.set(Default::default());
2619            hw_counterexample.write().clear();
2620            match design_from_spec(content) {
2621                Ok((intersection, plan)) => {
2622                    let mut out = String::new();
2623                    for (p, group) in plan.groups().iter().enumerate() {
2624                        out.push_str(&format!(
2625                            "Phase {}: {}\n",
2626                            p + 1,
2627                            intersection.names(group).join(", ")
2628                        ));
2629                    }
2630                    // Close the loop: generate a Verilog controller for the plan and certify it
2631                    // conflict-free with the same prover (design → generate → prove, all ours).
2632                    use logicaffeine_compile::codegen_sva::controller_gen::generate_controller;
2633                    use logicaffeine_compile::codegen_sva::rtl::parse_transition_system;
2634                    use logicaffeine_proof::bmc::InductionOutcome;
2635                    let verilog = generate_controller(&intersection, &plan);
2636                    let controller_proven = matches!(
2637                        parse_transition_system(&verilog).map(|ts| ts.prove_invariant(4)),
2638                        Ok(InductionOutcome::Proven)
2639                    );
2640                    out.push_str("\n\n// Generated controller (provably conflict-free):\n");
2641                    out.push_str(&verilog);
2642                    hw_sva.set(out.trim_end().to_string());
2643                    hw_signals.set(intersection.movements.clone());
2644                    let minimal = if !plan.minimal_certified {
2645                        String::new()
2646                    } else if plan.num_phases <= 1 {
2647                        " \u{2014} a single phase suffices".to_string()
2648                    } else {
2649                        format!(" \u{2014} provably minimal ({}-phase impossible)", plan.num_phases - 1)
2650                    };
2651                    let controller_note = if controller_proven {
2652                        " \u{00b7} generated controller PROVEN conflict-free (k-induction)"
2653                    } else {
2654                        ""
2655                    };
2656                    hw_proof.set(format!(
2657                        "\u{2713} Conflict-free {}-phase plan synthesized{}{} (certified by our SAT solver, no Z3)",
2658                        plan.num_phases, minimal, controller_note
2659                    ));
2660                    hw_proof_ok.set(Some(true));
2661                    hw_error.set(None);
2662                }
2663                Err(e) => {
2664                    hw_sva.set(String::new());
2665                    hw_signals.write().clear();
2666                    hw_proof.set(String::new());
2667                    hw_proof_ok.set(None);
2668                    hw_error.set(Some(e));
2669                }
2670            }
2671            return;
2672        }
2673        match logicaffeine_compile::codegen_sva::fol_to_sva::synthesize_sva_from_spec(content, "clk") {
2674            Ok(synth) => {
2675                let prop = hw_property_from_synth(&synth);
2676                hw_psl.set(logicaffeine_compile::codegen_sva::emit_psl_property(&prop));
2677                hw_signals.set(synth.signals.clone());
2678                // Certified, Z3-free equivalence: does the synthesized SVA capture the spec?
2679                use logicaffeine_compile::codegen_sva::hw_pipeline::{
2680                    check_spec_vacuity, prove_spec_sva_equivalence, VacuityReport,
2681                };
2682                // Vacuity (dead-trigger) check — appended to the verdict line.
2683                let vacuity_note = match check_spec_vacuity(content, 8) {
2684                    Ok(VacuityReport::Vacuous) => {
2685                        " \u{00b7} \u{26A0} never triggers (vacuous)".to_string()
2686                    }
2687                    Ok(VacuityReport::NonVacuous) => {
2688                        " \u{00b7} the trigger can actually fire".to_string()
2689                    }
2690                    _ => String::new(),
2691                };
2692                match prove_spec_sva_equivalence(content, &synth.body, 8) {
2693                    Ok(r) if r.equivalent => {
2694                        hw_proof.set(format!(
2695                            "\u{2713} Matches the spec \u{2014} certified, kernel-checked, no Z3{}",
2696                            vacuity_note
2697                        ));
2698                        hw_proof_ok.set(Some(true));
2699                        hw_counterexample.write().clear();
2700                    }
2701                    Ok(r) => {
2702                        let ce_vec = r.counterexample.unwrap_or_default();
2703                        let ce = ce_vec
2704                            .iter()
2705                            .map(|(k, v)| format!("{}={}", k, v))
2706                            .collect::<Vec<_>>()
2707                            .join(", ");
2708                        hw_proof.set(format!(
2709                            "\u{2717} Not equivalent to the spec \u{2014} counterexample: {}{}",
2710                            ce, vacuity_note
2711                        ));
2712                        hw_proof_ok.set(Some(false));
2713                        hw_counterexample.set(ce_vec);
2714                    }
2715                    Err(e) => {
2716                        hw_proof.set(format!("Proof unavailable: {}{}", e, vacuity_note));
2717                        hw_proof_ok.set(None);
2718                        hw_counterexample.write().clear();
2719                    }
2720                }
2721                hw_sva.set(synth.sva_text);
2722                // Extract the knowledge graph for the animated diagram (best-effort).
2723                hw_kg.set(
2724                    logicaffeine_compile::codegen_sva::hw_pipeline::kg_summary(content)
2725                        .unwrap_or_default(),
2726                );
2727                hw_error.set(None);
2728            }
2729            Err(e) => {
2730                hw_sva.set(String::new());
2731                hw_psl.set(String::new());
2732                hw_signals.write().clear();
2733                hw_proof.set(String::new());
2734                hw_proof_ok.set(None);
2735                hw_counterexample.write().clear();
2736                hw_kg.set(Default::default());
2737                hw_error.set(Some(e));
2738            }
2739        }
2740    }
2741
2742    let handle_hardware_execute = move |_| {
2743        let spec = hw_input.read().clone();
2744        load_hardware_spec(&spec, hw_sva, hw_psl, hw_signals, hw_proof, hw_proof_ok, hw_counterexample, hw_kg, hw_error);
2745        hw_output_mode.set(CodeOutputMode::Interpret);
2746    };
2747
2748    let handle_hardware_compile = move |_| {
2749        let spec = hw_input.read().clone();
2750        if spec.trim().is_empty() {
2751            generated_hw_rust.set(String::new());
2752            return;
2753        }
2754        match logicaffeine_compile::codegen_sva::fol_to_sva::synthesize_sva_from_spec(&spec, "clk") {
2755            Ok(synth) => {
2756                let prop = hw_property_from_synth(&synth);
2757                generated_hw_rust.set(logicaffeine_compile::codegen_sva::emit_rust_monitor(&prop));
2758                hw_error.set(None);
2759                hw_output_mode.set(CodeOutputMode::Rust);
2760            }
2761            Err(e) => {
2762                generated_hw_rust.set(String::new());
2763                hw_error.set(Some(e));
2764            }
2765        }
2766    };
2767
2768    // Logic mode guide
2769    let current_result = result.read();
2770    let guide_mode = if *mode.read() == StudioMode::Logic {
2771        if let Some(err) = &current_result.error {
2772            GuideMode::Error(err.clone())
2773        } else if current_result.logic.is_some() {
2774            let msg = get_success_message(current_result.readings.len());
2775            if let Some(hint) = get_context_hint(&input.read()) {
2776                GuideMode::Info(format!("{} {}", msg, hint))
2777            } else {
2778                GuideMode::Success(msg)
2779            }
2780        } else {
2781            GuideMode::Idle
2782        }
2783    } else {
2784        GuideMode::Idle
2785    };
2786
2787    // Determine if Panel 3 should be shown based on mode and content
2788    // A register-allocation spec (`registers:` + live ranges) routes Hardware mode to the certified
2789    // linear-scan easter egg: the output panel shows the allocation report and panel 3 the live-range
2790    // timeline. Computed once so panel visibility, the panel-2 header, and the panel-2 content agree.
2791    let hw_is_regalloc = matches!(*mode.read(), StudioMode::Hardware)
2792        && crate::ui::pages::register_alloc_viz::is_register_alloc_spec(&hw_input.read());
2793    // A pigeonhole spec (`pigeons: N`) routes Hardware mode to the live PHP(N) crusher easter egg:
2794    // panel 2 shows the certified refutation report, panel 3 the animated flight. Computed once so the
2795    // panel-3 gate, the panel-2 header, and the panel-2 content all agree on which egg is active.
2796    let hw_is_pigeonhole = matches!(*mode.read(), StudioMode::Hardware)
2797        && crate::ui::pages::pigeonhole_viz::is_pigeonhole_spec(&hw_input.read());
2798
2799    let show_panel3 = match *mode.read() {
2800        StudioMode::Logic => current_result.ast.is_some(),
2801        StudioMode::Code => interpreter_result.read().error.is_some(),
2802        StudioMode::Math => !definitions.is_empty() || !inductives.is_empty(),
2803        StudioMode::Hardware => {
2804            hw_is_regalloc
2805                || hw_is_pigeonhole
2806                || !hw_signals.read().is_empty()
2807                || !hw_counterexample.read().is_empty()
2808                || !hw_kg.read().nodes.is_empty()
2809        }
2810    };
2811
2812    let sidebar_w = *sidebar_width.read();
2813    let left_w = *left_width.read();
2814    let right_w = if show_panel3 { *right_width.read() } else { 0.0 };
2815    let center_w = 100.0 - left_w - right_w;
2816
2817    // Desktop mouse handlers for panel resizing
2818    let handle_mouse_move = move |evt: MouseEvent| {
2819        if let Some(which) = *resizing.read() {
2820            let window = web_sys::window().unwrap();
2821            let width = window.inner_width().unwrap().as_f64().unwrap();
2822            let coords = evt.data().client_coordinates();
2823            let x: f64 = coords.x;
2824            let pct: f64 = (x / width) * 100.0;
2825
2826            match which {
2827                "sidebar" => {
2828                    let new_sidebar: f64 = x.clamp(150.0, 400.0);
2829                    sidebar_width.set(new_sidebar);
2830                }
2831                "left" => {
2832                    let new_left: f64 = pct.clamp(15.0, 60.0);
2833                    left_width.set(new_left);
2834                }
2835                "right" => {
2836                    let new_right: f64 = (100.0 - pct).clamp(15.0, 40.0);
2837                    right_width.set(new_right);
2838                }
2839                "mobile" => {
2840                    let document = window.document().unwrap();
2841                    if let Ok(Some(el)) = document.query_selector(".studio-main") {
2842                        let rect = el.get_bounding_client_rect();
2843                        let y: f64 = coords.y;
2844                        let relative_y = y - rect.top();
2845                        let pct = (relative_y / rect.height()) * 100.0;
2846                        mobile_split.set(pct.clamp(15.0, 85.0));
2847                    }
2848                }
2849                _ => {}
2850            }
2851        }
2852    };
2853
2854    let handle_mouse_up = move |_: MouseEvent| {
2855        resizing.set(None);
2856    };
2857
2858    // Mobile touch handlers for swipe gestures
2859    let handle_touch_start = move |evt: TouchEvent| {
2860        let touches = evt.data().touches();
2861        if let Some(touch) = touches.first() {
2862            let coords = touch.client_coordinates();
2863            touch_start_x.set(coords.x);
2864            touch_start_y.set(coords.y);
2865        }
2866    };
2867
2868    let handle_touch_end = move |evt: TouchEvent| {
2869        if *resizing.read() == Some("mobile") {
2870            resizing.set(None);
2871            return;
2872        }
2873        let changed = evt.data().touches_changed();
2874        if let Some(touch) = changed.first() {
2875            let coords = touch.client_coordinates();
2876            let end_x = coords.x;
2877            let end_y = coords.y;
2878            let dx = end_x - *touch_start_x.read();
2879            let dy = end_y - *touch_start_y.read();
2880
2881            if dx.abs() > dy.abs() && dx.abs() > 50.0 {
2882                let current = *active_tab.read();
2883                if dx < 0.0 {
2884                    match current {
2885                        MobileTab::Panel1 => active_tab.set(MobileTab::Panel2),
2886                        MobileTab::Panel2 => active_tab.set(MobileTab::Panel3),
2887                        MobileTab::Panel3 => {}
2888                    }
2889                } else {
2890                    match current {
2891                        MobileTab::Panel1 => {}
2892                        MobileTab::Panel2 => active_tab.set(MobileTab::Panel1),
2893                        MobileTab::Panel3 => active_tab.set(MobileTab::Panel2),
2894                    }
2895                }
2896            }
2897        }
2898    };
2899
2900    let handle_touch_move = move |evt: TouchEvent| {
2901        if *resizing.read() == Some("mobile") {
2902            evt.prevent_default();
2903            let touches = evt.data().touches();
2904            if let Some(touch) = touches.first() {
2905                let window = web_sys::window().unwrap();
2906                let document = window.document().unwrap();
2907                if let Ok(Some(el)) = document.query_selector(".studio-main") {
2908                    let rect = el.get_bounding_client_rect();
2909                    let coords = touch.client_coordinates();
2910                    let y: f64 = coords.y;
2911                    let relative_y = y - rect.top();
2912                    let pct = (relative_y / rect.height()) * 100.0;
2913                    mobile_split.set(pct.clamp(15.0, 85.0));
2914                }
2915            }
2916        }
2917    };
2918
2919    let current_format = *format.read();
2920    let current_tab = *active_tab.read();
2921    let current_mode = *mode.read();
2922
2923    // Panel classes: mobile-expanded/collapsed for stacked layout, desktop ignores these
2924    let editor_exp = *editor_expanded.read();
2925    let output_exp = *output_expanded.read();
2926    let panel1_class = if editor_exp { "studio-panel mobile-expanded" } else { "studio-panel mobile-collapsed" };
2927    let panel2_class = if output_exp { "studio-panel mobile-expanded" } else { "studio-panel mobile-collapsed" };
2928    let panel3_class = "studio-panel";
2929
2930    // Mobile vertical split: compute panel flex proportions
2931    let mobile_pct = *mobile_split.read();
2932    let panel1_flex = mobile_pct / 50.0;
2933    let panel2_flex = (100.0 - mobile_pct) / 50.0;
2934    let both_expanded = editor_exp && output_exp;
2935
2936    let panel1_style = if both_expanded {
2937        format!("width: {left_w}%; --panel-flex: {panel1_flex};")
2938    } else {
2939        format!("width: {left_w}%;")
2940    };
2941    let panel2_style = if both_expanded {
2942        format!("width: {center_w}%; --panel-flex: {panel2_flex};")
2943    } else {
2944        format!("width: {center_w}%;")
2945    };
2946
2947    let container_class = if *resizing.read() == Some("mobile") {
2948        "studio-container mobile-resizing"
2949    } else {
2950        "studio-container"
2951    };
2952
2953    // Read output view modes for rendering
2954    let current_code_output_mode = *code_output_mode.read();
2955    let current_math_output_mode = *math_output_mode.read();
2956    let current_logic_output_mode = *logic_output_mode.read();
2957    let current_hw_output_mode = *hw_output_mode.read();
2958
2959    // Mobile tab labels based on mode
2960    let (tab1_icon, tab1_label, tab2_icon, tab2_label, tab3_icon, tab3_label) = match current_mode {
2961        StudioMode::Logic => ("\u{270F}", "Input", "\u{2200}", "Logic", "\u{1F333}", "Tree"),
2962        StudioMode::Code => ("\u{03BB}", "Editor", "\u{276F}", "Output", "\u{1F4CB}", "Console"),
2963        StudioMode::Math => ("\u{2200}", "Editor", "\u{276F}", "Output", "\u{1F4CB}", "Context"),
2964        StudioMode::Hardware => ("\u{270F}", "Spec", "\u{22A8}", "SVA", "\u{25A6}", "Graph"),
2965    };
2966
2967    rsx! {
2968        PageHead {
2969            title: seo_pages::STUDIO.title,
2970            description: seo_pages::STUDIO.description,
2971            canonical_path: seo_pages::STUDIO.canonical_path,
2972        }
2973        style { "{MOBILE_BASE_STYLES}" }
2974        style { "{MOBILE_TAB_BAR_STYLES}" }
2975        style { "{STUDIO_STYLE}" }
2976        JsonLdMultiple { schemas: vec![
2977            organization_schema(),
2978            software_application_schema(),
2979            breadcrumb_schema(&[
2980                BreadcrumbItem { name: "Home", path: "/" },
2981                BreadcrumbItem { name: "Studio", path: "/studio" },
2982            ]),
2983        ] }
2984
2985        div {
2986            class: "{container_class}",
2987            onmousemove: handle_mouse_move,
2988            onmouseup: handle_mouse_up,
2989            onmouseleave: handle_mouse_up,
2990            ontouchstart: handle_touch_start,
2991            ontouchend: handle_touch_end,
2992            ontouchmove: handle_touch_move,
2993
2994            MainNav { active: ActivePage::Studio, subtitle: Some("Your logic workspace") }
2995
2996            // Toolbar with mode toggle
2997            div { class: "studio-toolbar",
2998                div { class: "studio-toolbar-left",
2999                    button {
3000                        class: "sidebar-toggle-btn",
3001                        onclick: move |_| {
3002                            let current = *sidebar_open.read();
3003                            sidebar_open.set(!current);
3004                        },
3005                        title: "Toggle file browser",
3006                        if *sidebar_open.read() { "\u{2630}" } else { "\u{1F4C1}" }
3007                    }
3008                }
3009                div { class: "studio-toolbar-center",
3010                    span { class: "mode-label", "Mode:" }
3011                    ModeToggle {
3012                        mode: current_mode,
3013                        on_change: move |new_mode| {
3014                            mode.set(new_mode);
3015                            active_tab.set(MobileTab::Panel1);
3016                        },
3017                    }
3018                }
3019                div { class: "studio-toolbar-right",
3020                    if busy() {
3021                        span {
3022                            class: "execute-btn",
3023                            style: "opacity: 0.75; cursor: default; pointer-events: none;",
3024                            "\u{23F3} Working\u{2026}"
3025                        }
3026                    }
3027                    if current_mode == StudioMode::Code {
3028                        button {
3029                            class: "execute-btn",
3030                            onclick: handle_code_run,
3031                            span { class: "desktop-label", "\u{25B6} Run" }
3032                            span { class: "mobile-label", "\u{25B6} Run" }
3033                        }
3034                        button {
3035                            class: "execute-btn compile-btn",
3036                            style: "background: linear-gradient(135deg, #56b6c2 0%, #61afef 100%);",
3037                            onclick: handle_code_compile,
3038                            span { class: "desktop-label", "\u{1F980} Compile" }
3039                            span { class: "mobile-label", "\u{1F980} Compile to Rust" }
3040                        }
3041                        button {
3042                            class: "execute-btn",
3043                            style: "background: linear-gradient(135deg, #f59e0b 0%, #ef4444 100%); display:inline-flex; align-items:center; gap:6px;",
3044                            onclick: move |_| debugging.set(true),
3045                            span { style: "display:inline-flex;", dangerous_inner_html: IC_BUG }
3046                            span { class: "desktop-label", "Debug" }
3047                            span { class: "mobile-label", "Debug" }
3048                        }
3049                    }
3050                    if current_mode == StudioMode::Math {
3051                        button {
3052                            class: "execute-btn",
3053                            onclick: handle_math_execute,
3054                            span { class: "desktop-label", "\u{25B6} Execute" }
3055                            span { class: "mobile-label", "\u{25B6} Execute" }
3056                        }
3057                        button {
3058                            class: "execute-btn compile-btn",
3059                            style: "background: linear-gradient(135deg, #56b6c2 0%, #61afef 100%);",
3060                            onclick: handle_math_compile,
3061                            span { class: "desktop-label", "\u{1F980} Compile" }
3062                            span { class: "mobile-label", "\u{1F980} Compile to Rust" }
3063                        }
3064                    }
3065                    if current_mode == StudioMode::Logic {
3066                        button {
3067                            class: "execute-btn",
3068                            onclick: handle_logic_execute,
3069                            span { class: "desktop-label", "\u{25B6} Execute" }
3070                            span { class: "mobile-label", "\u{25B6} Execute" }
3071                        }
3072                        button {
3073                            class: "execute-btn compile-btn",
3074                            style: "background: linear-gradient(135deg, #56b6c2 0%, #61afef 100%);",
3075                            onclick: handle_logic_compile,
3076                            span { class: "desktop-label", "\u{1F980} Compile" }
3077                            span { class: "mobile-label", "\u{1F980} Compile to Rust" }
3078                        }
3079                    }
3080                    if current_mode == StudioMode::Hardware {
3081                        button {
3082                            class: "execute-btn",
3083                            onclick: handle_hardware_execute,
3084                            span { class: "desktop-label", "\u{25B6} Execute" }
3085                            span { class: "mobile-label", "\u{25B6} Execute" }
3086                        }
3087                        button {
3088                            class: "execute-btn compile-btn",
3089                            style: "background: linear-gradient(135deg, #56b6c2 0%, #61afef 100%);",
3090                            onclick: handle_hardware_compile,
3091                            span { class: "desktop-label", "\u{1F980} Compile" }
3092                            span { class: "mobile-label", "\u{1F980} Compile to Rust" }
3093                        }
3094                    }
3095                }
3096            }
3097
3098            // Mobile Tab Bar
3099            nav { class: "mobile-tabs",
3100                button {
3101                    class: if current_tab == MobileTab::Panel1 { "mobile-tab active" } else { "mobile-tab" },
3102                    onclick: move |_| active_tab.set(MobileTab::Panel1),
3103                    span { class: "mobile-tab-icon", "{tab1_icon}" }
3104                    span { class: "mobile-tab-label", "{tab1_label}" }
3105                }
3106                button {
3107                    class: if current_tab == MobileTab::Panel2 { "mobile-tab active" } else { "mobile-tab" },
3108                    onclick: move |_| active_tab.set(MobileTab::Panel2),
3109                    span { class: "mobile-tab-icon", "{tab2_icon}" }
3110                    span { class: "mobile-tab-label", "{tab2_label}" }
3111                }
3112                button {
3113                    class: if current_tab == MobileTab::Panel3 { "mobile-tab active" } else { "mobile-tab" },
3114                    onclick: move |_| active_tab.set(MobileTab::Panel3),
3115                    span { class: "mobile-tab-icon", "{tab3_icon}" }
3116                    span { class: "mobile-tab-label", "{tab3_label}" }
3117                }
3118                // Mode toggle in mobile tabs
3119                div { style: "margin-left: auto;",
3120                    ModeToggle {
3121                        mode: current_mode,
3122                        on_change: move |new_mode| {
3123                            mode.set(new_mode);
3124                            active_tab.set(MobileTab::Panel1);
3125                        },
3126                    }
3127                }
3128            }
3129
3130            // Socratic Guide (Logic mode only)
3131            if current_mode == StudioMode::Logic {
3132                div { class: "studio-guide",
3133                    SocraticGuide {
3134                        mode: guide_mode.clone(),
3135                        on_hint_request: None,
3136                    }
3137                }
3138            }
3139
3140            // Main content with optional sidebar
3141            div { class: "studio-content",
3142                // File browser sidebar
3143                // Mobile overlay to close sidebar when clicking outside
3144                if *sidebar_open.read() {
3145                    div {
3146                        class: "sidebar-overlay",
3147                        onclick: move |_| sidebar_open.set(false),
3148                    }
3149                }
3150
3151                if *sidebar_open.read() {
3152                    div {
3153                        class: "studio-sidebar",
3154                        style: "width: {sidebar_w}px; flex-shrink: 0;",
3155
3156                        // Show VFS error if any (critical errors only)
3157                        if let Some(err) = vfs_error.read().as_ref() {
3158                            div {
3159                                style: "background: #ff4444; color: white; padding: 6px 12px; font-size: 11px; border-radius: 0;",
3160                                "VFS Error: {err}"
3161                            }
3162                        }
3163
3164                        FileBrowser {
3165                        show_private_mode: *vfs_is_fallback.read(),
3166                        tree: file_tree.read().clone(),
3167                        selected_path: current_file.read().clone(),
3168                        on_select: EventHandler::new(move |path: String| {
3169                            // Close sidebar on mobile
3170                            #[cfg(target_arch = "wasm32")]
3171                            {
3172                                let window = web_sys::window().unwrap();
3173                                let width = window.inner_width().unwrap().as_f64().unwrap_or(1024.0);
3174                                if width <= 768.0 {
3175                                    sidebar_open.set(false);
3176                                }
3177                            }
3178                            current_file.set(Some(path.clone()));
3179
3180                            // Update URL with file parameter for shareable links
3181                            #[cfg(target_arch = "wasm32")]
3182                            sync_studio_url(&path);
3183
3184                            // Load file content from VFS
3185                            #[cfg(target_arch = "wasm32")]
3186                            {
3187                                let path_clone = path.clone();
3188                                spawn(async move {
3189                                    // Reuse the cached VFS (one worker) instead of spawning a
3190                                    // fresh one per file switch; acquire once if not ready yet.
3191                                    // Bind the clone first so the peek() read-guard drops before
3192                                    // the None arm calls vfs_handle.set() (avoids a borrow panic).
3193                                    let cached = vfs_handle.peek().clone();
3194                                    let vfs_result = match cached {
3195                                        Some(vfs) => Ok(vfs),
3196                                        None => get_platform_vfs_with_fallback().await.map(|vfs| {
3197                                            vfs_handle.set(Some(vfs.clone()));
3198                                            vfs
3199                                        }),
3200                                    };
3201                                    match vfs_result {
3202                                        Ok(vfs) => {
3203                                            match vfs.read_to_string(&path_clone).await {
3204                                                Ok(content) => {
3205                                            // Load into appropriate editor based on file path/extension
3206                                            // Math files are .logos but in /examples/math/ directory
3207                                            let ext = path_clone.rsplit('.').next().unwrap_or("").to_lowercase();
3208                                            let is_math_dir = path_clone.contains("/math/") || path_clone.contains("/examples/math");
3209                                            let is_hardware_dir = path_clone.contains("/hardware/") || path_clone.contains("/examples/hardware");
3210
3211                                            // Check for math directory first (takes precedence over .logos extension)
3212                                            if is_math_dir || ext == "math" || ext == "vernac" {
3213                                                // Switch to Math mode and Output tab
3214                                                mode.set(StudioMode::Math);
3215                                                active_tab.set(MobileTab::Panel2);
3216                                                math_input.set(content);
3217                                            } else if ext == "logic" {
3218                                                    // Switch to Logic mode and Output tab
3219                                                    mode.set(StudioMode::Logic);
3220                                                    active_tab.set(MobileTab::Panel2);
3221
3222                                                    // Load into editor
3223                                                    input.set(content.clone());
3224
3225                                                    // Check if this is a theorem file
3226                                                    if content.contains("## Theorem:") {
3227                                                        // Handle as theorem block with prover syntax
3228                                                        let theorem_result = compile_theorem_for_ui(&content);
3229
3230                                                        if let Some(err) = theorem_result.error {
3231                                                            // Parsing failed
3232                                                            result.set(CompileResult {
3233                                                                logic: None,
3234                                                                simple_logic: None,
3235                                                                kripke_logic: None,
3236                                                                ast: None,
3237                                                                readings: Vec::new(),
3238                                                                simple_readings: Vec::new(),
3239                                                                kripke_readings: Vec::new(),
3240                                                                tokens: Vec::new(),
3241                                                                error: Some(err.clone()),
3242                                                            });
3243                                                            proof_status.set(ProofStatus::Failed(err));
3244                                                            current_proof_expr.set(None);
3245                                                            knowledge_base.write().clear();
3246                                                        } else {
3247                                                            // Successfully parsed theorem
3248                                                            result.set(CompileResult {
3249                                                                logic: theorem_result.goal_string.clone(),
3250                                                                simple_logic: theorem_result.goal_string.clone(),
3251                                                                kripke_logic: None,
3252                                                                ast: None,
3253                                                                readings: Vec::new(),
3254                                                                simple_readings: Vec::new(),
3255                                                                kripke_readings: Vec::new(),
3256                                                                tokens: Vec::new(),
3257                                                                error: None,
3258                                                            });
3259
3260                                                            // Set up knowledge base from premises
3261                                                            knowledge_base.write().clear();
3262                                                            for premise in &theorem_result.premises {
3263                                                                knowledge_base.write().push(premise.clone());
3264                                                            }
3265
3266                                                            // Set goal for proof engine
3267                                                            if let Some(goal) = theorem_result.goal.clone() {
3268                                                                current_proof_expr.set(Some(goal));
3269                                                            }
3270
3271                                                            // Solved grid / wh-answer / derivation
3272                                                            let html = theorem_proof_html(&theorem_result);
3273                                                            if !html.is_empty() {
3274                                                                proof_text.set(html);
3275                                                                proof_status.set(if theorem_result.verified {
3276                                                                    ProofStatus::Success
3277                                                                } else {
3278                                                                    ProofStatus::Idle
3279                                                                });
3280                                                                proof_hint.set(Some(theorem_proof_hint(&theorem_result)));
3281                                                            } else {
3282                                                                proof_status.set(ProofStatus::Idle);
3283                                                                proof_hint.set(Some(format!(
3284                                                                    "Theorem '{}' ready. {} premise(s) loaded.",
3285                                                                    theorem_result.name,
3286                                                                    knowledge_base.read().len()
3287                                                                )));
3288                                                                proof_text.set(String::new());
3289                                                            }
3290                                                        }
3291                                                    } else {
3292                                                        // Filter out markdown headers (#) and LOGOS comments (--)
3293                                                        let sentences: Vec<&str> = content
3294                                                            .lines()
3295                                                            .filter(|line| {
3296                                                                let trimmed = line.trim();
3297                                                                !trimmed.is_empty()
3298                                                                && !trimmed.starts_with('#')
3299                                                                && !trimmed.starts_with("--")
3300                                                            })
3301                                                            .collect();
3302
3303                                                        if !sentences.is_empty() {
3304                                                            // Join all sentences and compile together
3305                                                            let all_text = sentences.join("\n");
3306                                                            let compiled = compile_for_ui(&all_text);
3307                                                            result.set(compiled);
3308
3309                                                            // Use first sentence for proof engine
3310                                                            let first_sentence = sentences[0];
3311                                                            let proof_result = compile_for_proof(first_sentence);
3312                                                            if let Some(expr) = proof_result.proof_expr {
3313                                                                current_proof_expr.set(Some(expr));
3314                                                            }
3315                                                        }
3316                                                    }
3317                                            } else if ext == "logos" {
3318                                                // Switch to Code mode and Output tab
3319                                                mode.set(StudioMode::Code);
3320                                                active_tab.set(MobileTab::Panel2);
3321
3322                                                code_input.set(content.clone());
3323                                                // Auto-run the code
3324                                                let interp_result = interpret_for_ui_baseline(&content).await;
3325                                                interpreter_result.set(interp_result);
3326                                            } else if is_hardware_dir || ext == "hw" {
3327                                                // Switch to Hardware mode and synthesize the spec.
3328                                                mode.set(StudioMode::Hardware);
3329                                                active_tab.set(MobileTab::Panel2);
3330                                                hw_input.set(content.clone());
3331                                                load_hardware_spec(
3332                                                    &content,
3333                                                    hw_sva, hw_psl, hw_signals, hw_proof, hw_proof_ok, hw_counterexample, hw_kg, hw_error,
3334                                                );
3335                                            } else {
3336                                                // Default: load based on current mode, switch to output tab
3337                                                active_tab.set(MobileTab::Panel2);
3338                                                let current_mode = *mode.read();
3339                                                match current_mode {
3340                                                    StudioMode::Logic => {
3341                                                        input.set(content.clone());
3342                                                        let compiled = compile_for_ui(&content);
3343                                                        result.set(compiled);
3344                                                    }
3345                                                    StudioMode::Code => {
3346                                                        code_input.set(content.clone());
3347                                                        let interp_result = interpret_for_ui_baseline(&content).await;
3348                                                        interpreter_result.set(interp_result);
3349                                                    }
3350                                                    StudioMode::Math => math_input.set(content),
3351                                                    StudioMode::Hardware => {
3352                                                        hw_input.set(content.clone());
3353                                                        load_hardware_spec(
3354                                                            &content,
3355                                                            hw_sva, hw_psl, hw_signals, hw_proof, hw_proof_ok, hw_counterexample, hw_kg, hw_error,
3356                                                        );
3357                                                    }
3358                                                }
3359                                            }
3360                                                }
3361                                                Err(e) => {
3362                                                    vfs_error.set(Some(format!("Failed to read file: {:?}", e)));
3363                                                }
3364                                            }
3365                                        }
3366                                        Err(e) => {
3367                                            vfs_error.set(Some(format!("VFS INIT FAILED: {:?}", e)));
3368                                        }
3369                                    }
3370                                });
3371                            }
3372                        }),
3373                        on_toggle_dir: EventHandler::new(move |path: String| {
3374                            if let Some(node) = file_tree.write().find_mut(&path) {
3375                                node.toggle_expanded();
3376                            }
3377                        }),
3378                        on_new_file: EventHandler::new(move |_: ()| {
3379                            // TODO: Show new file dialog
3380                        }),
3381                    }
3382                    }
3383
3384                    // Sidebar resizer
3385                    div {
3386                        class: if resizing.read().is_some() { "panel-resizer active" } else { "panel-resizer" },
3387                        onmousedown: move |_| resizing.set(Some("sidebar")),
3388                    }
3389                }
3390
3391                // Panels - content changes based on mode
3392                main { class: "studio-main",
3393                    // Panel 1
3394                    section {
3395                        class: "{panel1_class}",
3396                        style: "{panel1_style}",
3397
3398                        match current_mode {
3399                            StudioMode::Logic => rsx! {
3400                                div {
3401                                    class: "panel-header",
3402                                    onclick: move |_| {
3403                                        let v = *editor_expanded.read();
3404                                        editor_expanded.set(!v);
3405                                    },
3406                                    span { "English Input" }
3407                                }
3408                                div { class: "panel-content",
3409                                    LiveEditor {
3410                                        value: input.read().clone(),
3411                                        on_change: handle_logic_input,
3412                                        placeholder: Some("Type an English sentence...".to_string()),
3413                                    }
3414                                    // Proof Panel - below the input editor
3415                                    ProofPanel {
3416                                        proof_text: proof_text.read().clone(),
3417                                        status: proof_status.read().clone(),
3418                                        hint: proof_hint.read().clone(),
3419                                        on_tactic: handle_tactic.clone(),
3420                                    }
3421                                }
3422                            },
3423                            StudioMode::Code => rsx! {
3424                                div {
3425                                    class: "panel-header",
3426                                    onclick: move |_| {
3427                                        let v = *editor_expanded.read();
3428                                        editor_expanded.set(!v);
3429                                    },
3430                                    span { "Code Editor" }
3431                                }
3432                                div { class: "panel-content",
3433                                    CodeEditor {
3434                                        value: code_input.read().clone(),
3435                                        on_change: move |v| code_input.set(v),
3436                                        language: Language::Logos,
3437                                        placeholder: "-- Imperative LOGOS code\n\n## Main\n\nLet x be 1.\nLet y be 2.\nShow x + y.".to_string(),
3438                                    }
3439                                }
3440                            },
3441                            StudioMode::Math => rsx! {
3442                                div {
3443                                    class: "panel-header",
3444                                    onclick: move |_| {
3445                                        let v = *editor_expanded.read();
3446                                        editor_expanded.set(!v);
3447                                    },
3448                                    span { "Theorem Editor" }
3449                                }
3450                                div { class: "panel-content",
3451                                    CodeEditor {
3452                                        value: math_input.read().clone(),
3453                                        on_change: move |v| math_input.set(v),
3454                                        language: Language::Vernacular,
3455                                        placeholder: "-- Define natural numbers\nInductive Nat := Zero : Nat | Succ : Nat -> Nat.\n\nDefinition one : Nat := Succ Zero.\n\nCheck one.".to_string(),
3456                                    }
3457                                }
3458                            },
3459                            StudioMode::Hardware => rsx! {
3460                                div {
3461                                    class: "panel-header",
3462                                    onclick: move |_| {
3463                                        let v = *editor_expanded.read();
3464                                        editor_expanded.set(!v);
3465                                    },
3466                                    span { "Hardware Spec" }
3467                                }
3468                                div { class: "panel-content",
3469                                    LiveEditor {
3470                                        value: hw_input.read().clone(),
3471                                        on_change: move |v: String| hw_input.set(v),
3472                                        placeholder: Some("Describe hardware in English (\"Always, if request is high, then acknowledge is high.\"), design a signal plan (\"NS-left conflicts with the EW crossing.\"), or paste Verilog (module \u{2026} endmodule) to model-check.".to_string()),
3473                                    }
3474                                }
3475                            },
3476                        }
3477                    }
3478
3479                    // Mobile vertical resizer between Panel 1 and Panel 2
3480                    if both_expanded {
3481                        div {
3482                            class: if *resizing.read() == Some("mobile") {
3483                                "mobile-panel-resizer active"
3484                            } else {
3485                                "mobile-panel-resizer"
3486                            },
3487                            onmousedown: move |e| {
3488                                e.prevent_default();
3489                                resizing.set(Some("mobile"));
3490                            },
3491                            ontouchstart: move |e| {
3492                                e.prevent_default();
3493                                resizing.set(Some("mobile"));
3494                            },
3495                        }
3496                    }
3497
3498                    // Left resizer (desktop)
3499                    div {
3500                        class: if resizing.read().is_some() { "panel-resizer active" } else { "panel-resizer" },
3501                        onmousedown: move |_| resizing.set(Some("left")),
3502                    }
3503
3504                    // Panel 2
3505                    section {
3506                        class: "{panel2_class}",
3507                        style: "{panel2_style}",
3508
3509                        match current_mode {
3510                            StudioMode::Logic => rsx! {
3511                                div {
3512                                    class: "panel-header",
3513                                    onclick: move |_| {
3514                                        let v = *output_expanded.read();
3515                                        output_expanded.set(!v);
3516                                    },
3517                                    span { "Logic Output" }
3518                                    div {
3519                                        class: "output-mode-toggle",
3520                                        onclick: move |evt| evt.stop_propagation(),
3521                                        button {
3522                                            class: if current_logic_output_mode == LogicView::Logic { "output-mode-btn active" } else { "output-mode-btn" },
3523                                            onclick: move |_| logic_output_mode.set(LogicView::Logic),
3524                                            "Logic"
3525                                        }
3526                                        button {
3527                                            class: if current_logic_output_mode == LogicView::Rust { "output-mode-btn active" } else { "output-mode-btn" },
3528                                            onclick: move |_| logic_output_mode.set(LogicView::Rust),
3529                                            "Rust"
3530                                        }
3531                                    }
3532                                    if current_logic_output_mode == LogicView::Logic {
3533                                        div {
3534                                            class: "format-toggle",
3535                                            onclick: move |evt| evt.stop_propagation(),
3536                                            button {
3537                                                class: if current_format == OutputFormat::SimpleFOL { "format-btn active" } else { "format-btn" },
3538                                                onclick: move |_| format.set(OutputFormat::SimpleFOL),
3539                                                "Simple"
3540                                            }
3541                                            button {
3542                                                class: if current_format == OutputFormat::Unicode { "format-btn active" } else { "format-btn" },
3543                                                onclick: move |_| format.set(OutputFormat::Unicode),
3544                                                "Full"
3545                                            }
3546                                            button {
3547                                                class: if current_format == OutputFormat::LaTeX { "format-btn active" } else { "format-btn" },
3548                                                onclick: move |_| format.set(OutputFormat::LaTeX),
3549                                                "LaTeX"
3550                                            }
3551                                            button {
3552                                                class: if current_format == OutputFormat::Kripke { "format-btn active" } else { "format-btn" },
3553                                                onclick: move |_| format.set(OutputFormat::Kripke),
3554                                                "Deep"
3555                                            }
3556                                        }
3557                                    }
3558                                }
3559                                div { class: "panel-content",
3560                                    if current_logic_output_mode == LogicView::Logic {
3561                                        {
3562                                            rsx! {
3563                                                LogicOutput {
3564                                                    logic: current_result.logic.clone(),
3565                                                    simple_logic: current_result.simple_logic.clone(),
3566                                                    kripke_logic: current_result.kripke_logic.clone(),
3567                                                    readings: current_result.readings.clone(),
3568                                                    simple_readings: current_result.simple_readings.clone(),
3569                                                    kripke_readings: current_result.kripke_readings.clone(),
3570                                                    error: current_result.error.clone(),
3571                                                    format: current_format,
3572                                                }
3573                                                if let Some(ref logic) = current_result.logic {
3574                                                    SymbolDictionary {
3575                                                        logic: logic.clone(),
3576                                                        collapsed: false,
3577                                                        inline: false,
3578                                                    }
3579                                                }
3580                                            }
3581                                        }
3582                                    } else {
3583                                        {
3584                                            let rust_code = generated_logic_rust.read().clone();
3585                                            rsx! {
3586                                                if rust_code.is_empty() {
3587                                                    div { class: "interpreter-empty",
3588                                                        "Compile to see generated Rust"
3589                                                    }
3590                                                } else {
3591                                                    CodeView {
3592                                                        code: rust_code,
3593                                                        language: Language::Rust,
3594                                                    }
3595                                                }
3596                                            }
3597                                        }
3598                                    }
3599                                }
3600                            },
3601                            StudioMode::Code => rsx! {
3602                                div {
3603                                    class: "panel-header",
3604                                    onclick: move |_| {
3605                                        let v = *output_expanded.read();
3606                                        output_expanded.set(!v);
3607                                    },
3608                                    span { "Output" }
3609                                    div {
3610                                        class: "output-mode-toggle",
3611                                        onclick: move |evt| evt.stop_propagation(),
3612                                        button {
3613                                            class: if current_code_output_mode == CodeOutputMode::Interpret { "output-mode-btn active" } else { "output-mode-btn" },
3614                                            onclick: move |_| code_output_mode.set(CodeOutputMode::Interpret),
3615                                            "Output"
3616                                        }
3617                                        button {
3618                                            class: if current_code_output_mode == CodeOutputMode::Rust { "output-mode-btn active" } else { "output-mode-btn" },
3619                                            onclick: move |_| code_output_mode.set(CodeOutputMode::Rust),
3620                                            "Rust"
3621                                        }
3622                                    }
3623                                }
3624                                div { class: "panel-content",
3625                                    if current_code_output_mode == CodeOutputMode::Interpret {
3626                                        {
3627                                            let result = interpreter_result.read();
3628                                            rsx! {
3629                                                div { class: "interpreter-output",
3630                                                    if result.lines.is_empty() && result.error.is_none() {
3631                                                        div { class: "interpreter-empty",
3632                                                            "Run your code to see output"
3633                                                        }
3634                                                    } else {
3635                                                        for line in result.lines.iter() {
3636                                                            div { class: "interpreter-line", "{line}" }
3637                                                        }
3638                                                        if let Some(ref err) = result.error {
3639                                                            div { class: "interpreter-error", "{err}" }
3640                                                        }
3641                                                    }
3642                                                }
3643                                            }
3644                                        }
3645                                    } else {
3646                                        {
3647                                            let rust_code = generated_rust.read().clone();
3648                                            rsx! {
3649                                                if rust_code.is_empty() {
3650                                                    div { class: "interpreter-empty",
3651                                                        "Compile your code to see generated Rust"
3652                                                    }
3653                                                } else {
3654                                                    CodeView {
3655                                                        code: rust_code,
3656                                                        language: Language::Rust,
3657                                                    }
3658                                                }
3659                                            }
3660                                        }
3661                                    }
3662                                }
3663                            },
3664                            StudioMode::Math => rsx! {
3665                                div {
3666                                    class: "panel-header",
3667                                    onclick: move |_| {
3668                                        let v = *output_expanded.read();
3669                                        output_expanded.set(!v);
3670                                    },
3671                                    span { "Output" }
3672                                    div {
3673                                        class: "output-mode-toggle",
3674                                        onclick: move |evt| evt.stop_propagation(),
3675                                        button {
3676                                            class: if current_math_output_mode == CodeOutputMode::Interpret { "output-mode-btn active" } else { "output-mode-btn" },
3677                                            onclick: move |_| math_output_mode.set(CodeOutputMode::Interpret),
3678                                            "Output"
3679                                        }
3680                                        button {
3681                                            class: if current_math_output_mode == CodeOutputMode::Rust { "output-mode-btn active" } else { "output-mode-btn" },
3682                                            onclick: move |_| math_output_mode.set(CodeOutputMode::Rust),
3683                                            "Rust"
3684                                        }
3685                                    }
3686                                }
3687                                div { class: "panel-content",
3688                                    if current_math_output_mode == CodeOutputMode::Interpret {
3689                                        ReplOutput {
3690                                            lines: math_output.read().clone(),
3691                                            on_clear: move |_| {
3692                                                math_output.write().clear();
3693                                                math_repl.set(Repl::new());  // Reset kernel state too
3694                                            },
3695                                        }
3696                                    } else {
3697                                        {
3698                                            let rust_code = generated_math_rust.read().clone();
3699                                            rsx! {
3700                                                if rust_code.is_empty() {
3701                                                    div { class: "interpreter-empty",
3702                                                        "Compile to see generated Rust"
3703                                                    }
3704                                                } else {
3705                                                    CodeView {
3706                                                        code: rust_code,
3707                                                        language: Language::Rust,
3708                                                    }
3709                                                }
3710                                            }
3711                                        }
3712                                    }
3713                                }
3714                            },
3715                            StudioMode::Hardware => rsx! {
3716                                div {
3717                                    class: "panel-header",
3718                                    onclick: move |_| {
3719                                        let v = *output_expanded.read();
3720                                        output_expanded.set(!v);
3721                                    },
3722                                    span { if hw_is_regalloc { "Allocation" } else if hw_is_pigeonhole { "Pigeonhole" } else { "SVA" } }
3723                                    if !hw_is_regalloc && !hw_is_pigeonhole {
3724                                        div {
3725                                            class: "output-mode-toggle",
3726                                            onclick: move |evt| evt.stop_propagation(),
3727                                            button {
3728                                                class: if current_hw_output_mode == CodeOutputMode::Interpret { "output-mode-btn active" } else { "output-mode-btn" },
3729                                                onclick: move |_| hw_output_mode.set(CodeOutputMode::Interpret),
3730                                                "SVA"
3731                                            }
3732                                            button {
3733                                                class: if current_hw_output_mode == CodeOutputMode::Rust { "output-mode-btn active" } else { "output-mode-btn" },
3734                                                onclick: move |_| hw_output_mode.set(CodeOutputMode::Rust),
3735                                                "Rust"
3736                                            }
3737                                        }
3738                                    }
3739                                }
3740                                div { class: "panel-content",
3741                                    if hw_is_regalloc {
3742                                        {
3743                                            let report = crate::ui::pages::register_alloc_viz::parse_register_spec(&hw_input.read())
3744                                                .map(|s| crate::ui::pages::register_alloc_viz::allocation_report(&s))
3745                                                .unwrap_or_default();
3746                                            rsx! {
3747                                                div { class: "interpreter-output",
3748                                                    div { style: "font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: rgba(255,255,255,0.45); margin: 4px 0;", "Certified register allocation" }
3749                                                    pre { style: "font-family: ui-monospace, monospace; font-size: 13px; white-space: pre-wrap; word-break: break-word; color: #e5e7eb; background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08); border-radius: 8px; padding: 10px; margin: 0;", "{report}" }
3750                                                }
3751                                            }
3752                                        }
3753                                    } else if hw_is_pigeonhole {
3754                                        {
3755                                            let report = crate::ui::pages::pigeonhole_viz::parse_pigeonhole_spec(&hw_input.read())
3756                                                .map(|s| crate::ui::pages::pigeonhole_viz::report(&s))
3757                                                .unwrap_or_default();
3758                                            rsx! {
3759                                                div { class: "interpreter-output",
3760                                                    div { style: "font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: rgba(255,255,255,0.45); margin: 4px 0;", "Certified pigeonhole refutation" }
3761                                                    pre { style: "font-family: ui-monospace, monospace; font-size: 13px; white-space: pre-wrap; word-break: break-word; color: #e5e7eb; background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08); border-radius: 8px; padding: 10px; margin: 0;", "{report}" }
3762                                                }
3763                                            }
3764                                        }
3765                                    } else if current_hw_output_mode == CodeOutputMode::Interpret {
3766                                        {
3767                                            let sva = hw_sva.read().clone();
3768                                            let psl = hw_psl.read().clone();
3769                                            let proof = hw_proof.read().clone();
3770                                            let proof_ok = *hw_proof_ok.read();
3771                                            let err = hw_error.read().clone();
3772                                            rsx! {
3773                                                div { class: "interpreter-output",
3774                                                    if let Some(e) = err {
3775                                                        div { class: "interpreter-error", "{e}" }
3776                                                    } else if sva.is_empty() {
3777                                                        div { class: "interpreter-empty",
3778                                                            "Execute to analyze \u{2014} synthesize an assertion, design a signal plan, or model-check Verilog"
3779                                                        }
3780                                                    } else {
3781                                                        div { style: "font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: rgba(255,255,255,0.45); margin: 4px 0;", "SystemVerilog Assertion" }
3782                                                        pre { style: "font-family: ui-monospace, monospace; font-size: 13px; white-space: pre-wrap; word-break: break-word; color: #e5e7eb; background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08); border-radius: 8px; padding: 10px; margin: 0 0 12px;", "{sva}" }
3783                                                        if !proof.is_empty() {
3784                                                            div {
3785                                                                style: {
3786                                                                    let (border, bg, fg) = match proof_ok {
3787                                                                        Some(true) => ("rgba(74,222,128,0.4)", "rgba(74,222,128,0.08)", "#4ade80"),
3788                                                                        Some(false) => ("rgba(248,113,113,0.4)", "rgba(248,113,113,0.08)", "#f87171"),
3789                                                                        None => ("rgba(255,255,255,0.12)", "rgba(255,255,255,0.03)", "#d0d0d0"),
3790                                                                    };
3791                                                                    format!("font-size: 13px; line-height: 1.5; color: {fg}; background: {bg}; border: 1px solid {border}; border-radius: 8px; padding: 10px; margin: 0 0 12px;")
3792                                                                },
3793                                                                "{proof}"
3794                                                            }
3795                                                        }
3796                                                        if !psl.is_empty() {
3797                                                            div { style: "font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: rgba(255,255,255,0.45); margin: 4px 0;", "PSL" }
3798                                                            pre { style: "font-family: ui-monospace, monospace; font-size: 13px; white-space: pre-wrap; word-break: break-word; color: #e5e7eb; background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08); border-radius: 8px; padding: 10px; margin: 0;", "{psl}" }
3799                                                        }
3800                                                    }
3801                                                }
3802                                            }
3803                                        }
3804                                    } else {
3805                                        {
3806                                            let rust_code = generated_hw_rust.read().clone();
3807                                            rsx! {
3808                                                if rust_code.is_empty() {
3809                                                    div { class: "interpreter-empty",
3810                                                        "Compile to see the Rust runtime monitor"
3811                                                    }
3812                                                } else {
3813                                                    CodeView {
3814                                                        code: rust_code,
3815                                                        language: Language::Rust,
3816                                                    }
3817                                                }
3818                                            }
3819                                        }
3820                                    }
3821                                }
3822                            },
3823                        }
3824                    }
3825
3826                    // Right resizer and Panel 3 (only shown when there's content)
3827                    if show_panel3 {
3828                        div {
3829                            class: if resizing.read().is_some() { "panel-resizer active" } else { "panel-resizer" },
3830                            onmousedown: move |_| resizing.set(Some("right")),
3831                        }
3832
3833                        aside {
3834                            class: "{panel3_class}",
3835                            style: "width: {right_w}%;",
3836
3837                            match current_mode {
3838                                StudioMode::Logic => rsx! {
3839                                    div { class: "panel-header",
3840                                        span { "Syntax Tree" }
3841                                    }
3842                                    div { class: "panel-content",
3843                                        AstTree {
3844                                            ast: current_result.ast.clone(),
3845                                        }
3846                                    }
3847                                },
3848                                StudioMode::Code => rsx! {
3849                                    div { class: "panel-header",
3850                                        span { "Console" }
3851                                    }
3852                                    div { class: "panel-content",
3853                                        div { class: "interpreter-output",
3854                                            {
3855                                                let result = interpreter_result.read();
3856                                                rsx! {
3857                                                    if let Some(ref err) = result.error {
3858                                                        div { class: "interpreter-error", "{err}" }
3859                                                    }
3860                                                }
3861                                            }
3862                                        }
3863                                    }
3864                                },
3865                                StudioMode::Math => rsx! {
3866                                    div { class: "panel-header",
3867                                        span { "Context" }
3868                                    }
3869                                    div { class: "panel-content",
3870                                        ContextView {
3871                                            definitions: definitions.clone(),
3872                                            inductives: inductives.clone(),
3873                                        }
3874                                    }
3875                                },
3876                                StudioMode::Hardware => {
3877                                    let ce = hw_counterexample.read().clone();
3878                                    let sigs = hw_signals.read().clone();
3879                                    if let Some(pspec) = crate::ui::pages::pigeonhole_viz::parse_pigeonhole_spec(&hw_input.read()) {
3880                                        // Pigeonhole easter egg: n pigeons fly into n-1 holes, one left
3881                                        // out — solved live (matching Hall witness + certified symmetry
3882                                        // breaking) by our prover in the browser, no Z3.
3883                                        let (svg, verdict) = crate::ui::pages::pigeonhole_viz::render(&pspec);
3884                                        rsx! {
3885                                            div { class: "panel-header", span { "Pigeonhole" } }
3886                                            div { class: "panel-content",
3887                                                div { style: "padding: 8px; overflow-x: auto;",
3888                                                    div { dangerous_inner_html: "{svg}" }
3889                                                    div {
3890                                                        style: "margin-top:4px;font-size:11px;line-height:1.4;color:rgba(255,255,255,0.45);",
3891                                                        "Each pigeon flies toward a hole; with one more pigeon than holes, one is always left out. Maximum bipartite matching proves it impossible in polynomial time (the re-verified Hall witness), and our prover emits a certified symmetry-breaking refutation — while every resolution-based solver (Kissat, CaDiCaL, Z3) provably needs exponentially many steps (Haken 1985)."
3892                                                    }
3893                                                    div {
3894                                                        style: "margin-top:8px;padding:8px;border-radius:6px;font-size:12px;line-height:1.45;background:rgba(224,108,117,0.15);color:#f3c0c4;",
3895                                                        "{verdict}"
3896                                                    }
3897                                                }
3898                                            }
3899                                        }
3900                                    } else if let Some(spec) = crate::ui::pages::register_alloc_viz::parse_register_spec(&hw_input.read()) {
3901                                        // Register-allocation easter egg: live-range timeline coloured
3902                                        // by the certified linear-scan allocation (spill clique in red).
3903                                        let (svg, verdict) = crate::ui::pages::register_alloc_viz::render(&spec);
3904                                        let ok = verdict.starts_with('\u{2713}');
3905                                        rsx! {
3906                                            div { class: "panel-header", span { "Register Allocation" } }
3907                                            div { class: "panel-content",
3908                                                div { style: "padding: 8px; overflow-x: auto;",
3909                                                    div { dangerous_inner_html: "{svg}" }
3910                                                    div {
3911                                                        style: "margin-top:4px;font-size:11px;line-height:1.4;color:rgba(255,255,255,0.45);",
3912                                                        "The sweep line is the program counter; each register lane lights up while it holds a value and goes dark when it's freed. The pressure histogram counts how many values are live at each instruction — bars above the dashed budget line turn red, the exact points where spilling is forced and a value drops to the MEM lane. The interference graph is what allocation really colours: an edge joins two values whose lifetimes overlap, nodes are coloured by their register, and a red clique is a set that mutually conflicts and provably cannot share."
3913                                                    }
3914                                                    div {
3915                                                        style: if ok {
3916                                                            "margin-top:8px;padding:8px;border-radius:6px;font-size:12px;line-height:1.45;background:rgba(152,195,121,0.15);color:#cdebc5;"
3917                                                        } else {
3918                                                            "margin-top:8px;padding:8px;border-radius:6px;font-size:12px;line-height:1.45;background:rgba(224,108,117,0.15);color:#f3c0c4;"
3919                                                        },
3920                                                        "{verdict}"
3921                                                    }
3922                                                }
3923                                            }
3924                                        }
3925                                    } else if !ce.is_empty() {
3926                                        // A counterexample exists — render it as a waveform.
3927                                        let wf = logicaffeine_compile::codegen_sva::hw_pipeline::counterexample_waveform(&ce);
3928                                        let traffic = traffic_svg(&wf);
3929                                        let osc = waveform_svg(&wf);
3930                                        let violated = *hw_proof_ok.read() == Some(false);
3931                                        let header = match (traffic.is_some(), violated) {
3932                                            (true, true) => "Intersection \u{2014} Conflict Found",
3933                                            (true, false) => "Intersection \u{2014} Proven Safe (witness run)",
3934                                            (false, true) => "Counterexample Waveform",
3935                                            (false, false) => "Witness Waveform",
3936                                        };
3937                                        rsx! {
3938                                            div { class: "panel-header", span { "{header}" } }
3939                                            div { class: "panel-content",
3940                                                div { style: "padding: 8px; overflow-x: auto;",
3941                                                    if let Some(t) = traffic.as_ref() {
3942                                                        div { dangerous_inner_html: "{t}" }
3943                                                    }
3944                                                    div { dangerous_inner_html: "{osc}" }
3945                                                }
3946                                            }
3947                                        }
3948                                    } else {
3949                                        let svg = kg_svg(&hw_kg.read());
3950                                        rsx! {
3951                                            div { class: "panel-header", span { "Knowledge Graph" } }
3952                                            div { class: "panel-content",
3953                                                if !svg.is_empty() {
3954                                                    div { style: "padding: 6px;",
3955                                                        div { dangerous_inner_html: "{svg}" }
3956                                                        div { style: "display:flex; gap:12px; flex-wrap:wrap; justify-content:center; margin-top:4px; font-size:10px;",
3957                                                            span { style: "color:#60a5fa;", "● input" }
3958                                                            span { style: "color:#4ade80;", "● output" }
3959                                                            span { style: "color:#a78bfa;", "● internal" }
3960                                                            span { style: "color:#fbbf24;", "● clock" }
3961                                                        }
3962                                                    }
3963                                                } else if !sigs.is_empty() {
3964                                                    div { style: "padding: 8px;",
3965                                                        div { style: "font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: rgba(255,255,255,0.45); margin-bottom: 6px;", "Signals" }
3966                                                        for s in sigs.iter() {
3967                                                            div { style: "font-family: ui-monospace, monospace; font-size: 13px; color: #e5e7eb; padding: 2px 0;", "{s}" }
3968                                                        }
3969                                                    }
3970                                                } else {
3971                                                    div { class: "interpreter-empty",
3972                                                        "Execute to extract the hardware knowledge graph"
3973                                                    }
3974                                                }
3975                                            }
3976                                        }
3977                                    }
3978                                },
3979                            }
3980                        }
3981                    }
3982                }
3983            }
3984
3985            // Code-mode debugger — bottom-docked, additive (hidden unless debugging).
3986            if debugging() && current_mode == StudioMode::Code {
3987                DebugDrawer {
3988                    source: code_input.read().clone(),
3989                    on_close: move |_| debugging.set(false),
3990                }
3991            }
3992        }
3993    }
3994}