diff --git a/.cursor/rules/accessibility-and-visual-qa.mdc b/.cursor/rules/accessibility-and-visual-qa.mdc new file mode 100644 index 0000000..dd9b95d --- /dev/null +++ b/.cursor/rules/accessibility-and-visual-qa.mdc @@ -0,0 +1,22 @@ +--- +description: Accessibility and visual QA checklist for product demo +alwaysApply: true +--- + +# Accessibility and Visual QA Rules + +- Контраст должен быть читаемым. +- Фокус-состояния для кнопок обязательны. +- Кнопки должны иметь понятные labels. +- Не использовать только цвет для передачи смысла. +- Текст должен быть читаемым на проекторе. +- Проверить визуально: + - 1920×1080 + - 1440×900 + - 390×844 +- Проверить: + - нет horizontal scroll; + - hero понятен; + - CTA виден; + - demo section понятен; + - mobile layout не развален. diff --git a/.cursor/rules/react-design-system.mdc b/.cursor/rules/react-design-system.mdc new file mode 100644 index 0000000..b196146 --- /dev/null +++ b/.cursor/rules/react-design-system.mdc @@ -0,0 +1,24 @@ +--- +description: React design system and component boundaries for sorter demo +globs: src/**/*.{tsx,ts,css} +alwaysApply: false +--- + +# React Design System Rules + +- Не ломать domain logic. +- UI-компоненты должны быть небольшими и понятными. +- Переиспользовать данные scenarios / criteria / demoSteps. +- Не добавлять тяжёлые UI-библиотеки. +- Не добавлять 3D. +- Не добавлять backend. +- Компоненты: + - HeroSection + - ProductDemoSection + - StorylineStepper + - ScenarioCards + - CriteriaCards + - EngineeringDetails +- Старые инженерные панели можно использовать внутри EngineeringDetails. +- Стили централизовать в styles.css или небольших CSS-блоках. +- Не делать хаотичный refactor всего проекта. diff --git a/.cursor/rules/responsive-product-demo.mdc b/.cursor/rules/responsive-product-demo.mdc new file mode 100644 index 0000000..bf45dc7 --- /dev/null +++ b/.cursor/rules/responsive-product-demo.mdc @@ -0,0 +1,28 @@ +--- +description: Responsive breakpoints and layout rules for product demo +alwaysApply: true +--- + +# Responsive Product Demo Rules + +Breakpoints: +- desktop: >= 1200px +- laptop/tablet landscape: 900–1199px +- tablet: 640–899px +- mobile: < 640px + +Требования: +- body/html/#root не должны создавать horizontal scroll. +- Проверять overflow-x. +- Все секции используют width: min(100%, ...), max-width, padding через clamp(). +- На mobile: + - одна колонка; + - крупные кнопки; + - короткие тексты; + - инженерные детали свернуты; + - сцена масштабируется; + - сценарии идут карточками. +- На desktop: + - Hero + demo в понятных секциях; + - 2 колонки только там, где хватает ширины; + - без внутреннего хаотичного скролла. diff --git a/.cursor/rules/ui-ux-pro-max.mdc b/.cursor/rules/ui-ux-pro-max.mdc new file mode 100644 index 0000000..16629a8 --- /dev/null +++ b/.cursor/rules/ui-ux-pro-max.mdc @@ -0,0 +1,33 @@ +--- +description: UI/UX Pro Max — product demo page, not debug dashboard +alwaysApply: true +--- + +# UI/UX Pro Max Rules + +Ты обязан проектировать интерфейс как demo product page, а не debug dashboard. + +Обязательные принципы: +- Пользователь должен понять смысл проекта за 10 секунд. +- Первый экран должен объяснять: + 1. Что это за система. + 2. Что она делает. + 3. Как запустить демо. + 4. Какой результат будет показан. +- Главный интерфейс должен вести пользователя по цепочке: + Detection → Classification → Command → Routing. +- Не показывать все инженерные панели сразу. +- Инженерные детали должны быть ниже или в отдельном раскрываемом блоке. +- Не должно быть горизонтального скролла. +- Mobile-first адаптив обязателен. +- Основной текст не меньше 16px. +- Кнопки не меньше 44px по высоте. +- На mobile всё в одну колонку. +- На desktop допустима сетка 2 колонки. +- Визуальная иерархия: + Hero → Demo → Result → Scenarios → Criteria → Engineering Details. +- Запрещены мелкие debug-панели на первом экране. +- Запрещены узкие внутренние scroll-панели для сценариев. +- Запрещены fixed width больше viewport. +- Использовать min-width: 0 для grid/flex children. +- SVG/сцена должны масштабироваться через width: 100%, max-width: 100%. diff --git a/README.md b/README.md index 00d267a..f9d1497 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,12 @@ - маршрутизация в зоны B/C/D; - отработка аварийных ситуаций (Jam, Emergency Stop). -UI переработан под **Product Demo Landing Page**, который: -- работает без горизонтального скролла на любых экранах; -- адаптирован для Mobile, Tablet и Desktop; -- включает Storyline (Detection → Classification → Command → Routing); -- предлагает список Jury Scenarios и OZON Criteria; -- прячет сложный Engineering Dashboard в сворачиваемый аккордеон. +UI — **Product Demo Landing Page**: +- без горизонтального скролла на desktop / laptop / mobile; +- mobile-first адаптив (одна колонка < 640px); +- storyline Detection → Classification → Decision → Command → Routing; +- карточки сценариев и критериев OZON; +- Engineering Details свёрнуты по умолчанию. ## Стек @@ -35,13 +35,46 @@ https://www.arhipovdan.ru/ http://127.0.0.1:3100/ ``` -## Структура Demo-страницы +## Как устроен новый UI -1. **Hero Section:** краткая суть проекта и цепочки действий. -2. **Product Demo Section:** живая сцена (`SorterScene`) и карточка текущего результата. -3. **Scenarios Cards:** карточки для проверки нестандартных товаров (негабарит, шар, затор). -4. **Criteria Cards:** чек-лист покрытия требований OZON. -5. **Engineering Details:** подробные отладочные панели (Event Log, PID, метрики). +1. **Hero** — что это за система, CTA «Запустить демо», цепочка Detection → Routing. +2. **Product Demo** — упрощённая сцена (`SorterScene variant="simple"`) + карточка результата + Start / Next / Reset. +3. **Storyline Stepper** — текущий этап цикла. +4. **Scenario Cards** — jury-кейсы карточками (кнопка «Показать»). +5. **Criteria Cards** — покрытие критериев OZON со ссылкой на сценарий. +6. **Engineering Details** — полные техпанели (state machine, sensors, PID, timeline, event log, criteria). + +## Как запустить демо + +1. Откройте сайт. +2. Нажмите **Запустить демо** в Hero или **Start demo** в блоке демо. +3. Нажимайте **Next step**, чтобы пройти цикл товара. +4. Выберите сценарий в карточках ниже (негабарит, круглый объект, jam и т.д.). +5. Для экспертов откройте **Инженерный режим** / **Engineering Details**. + +## Где Engineering Details + +Внизу страницы, секция **Engineering Details**. По умолчанию свёрнута. Кнопки «Инженерный режим» в header / hero / demo раскрывают блок и скроллят к нему. Внутри — полная сцена (`variant="full"`) и все инженерные панели. + +## Как проверить mobile + +1. Откройте DevTools → device toolbar. +2. Выберите `390×844` (или iPhone 12/13). +3. Проверьте: + - одна колонка; + - кнопки ≥ 44px; + - сцена масштабируется (`width: 100%`); + - нет horizontal scroll. + +## Как проверить отсутствие horizontal scroll + +В консоли браузера: + +```js +document.documentElement.scrollWidth <= document.documentElement.clientWidth +``` + +Должно вернуть `true` на ширинах 1920, 1440 и 390. ## Запуск локально @@ -116,7 +149,7 @@ State machine управляет циклом: ## Simplified PID -PID-панель показывает упрощенную имитацию control loop: target speed, actual speed, error, correction и mini graph последних тиков скорости. +PID-панель (в Engineering Details) показывает упрощенную имитацию control loop: target speed, actual speed, error, correction и mini graph последних тиков скорости. В normal flow actual speed приближается к target. В `jam` и `emergency_stop` target становится 0, actual speed визуально падает к 0. @@ -131,11 +164,21 @@ npm run test ## Документация - `docs/ARCHITECTURE.md` — модули и поток данных. -- `docs/DEMO_SCRIPT.md` — 3-5 минутный сценарий защиты. +- `docs/DEMO_SCRIPT.md` — сценарий защиты. - `docs/SCENARIOS.md` — ожидаемые результаты сценариев. -- `docs/JURY_QA.md` — короткие ответы на вопросы жюри. +- `docs/JURY_QA.md` — ответы на вопросы жюри. +- `docs/UI_UX_REDESIGN_AUDIT.md` — план редизайна UI. - `docs/SUBMISSION_CHECKLIST.md` — checklist перед сдачей. +## Cursor rules + +Локальные UI/UX rules в `.cursor/rules/`: + +- `ui-ux-pro-max.mdc` +- `responsive-product-demo.mdc` +- `react-design-system.mdc` +- `accessibility-and-visual-qa.mdc` + ## Ограничения MVP - Физика движения дискретная, без динамической модели массы/трения. diff --git a/docs/DEMO_SCRIPT.md b/docs/DEMO_SCRIPT.md index 22c16eb..0045271 100644 --- a/docs/DEMO_SCRIPT.md +++ b/docs/DEMO_SCRIPT.md @@ -1,79 +1,97 @@ # Demo Script -Open https://arhipovdan.ru/ and switch Header to `Guided Demo`. +Open https://arhipovdan.ru/ (или http://127.0.0.1:3100/). + +Страница — product demo: Hero → Demo → Storyline → Scenarios → Criteria → Engineering Details. ## 30-Second Pitch -"We built an engineering simulation of the OZON sorting cell. It models the full loop: detection -> classification -> command -> routing. We cover happy paths to B/C/D, fault handling, and timing. The Guided Demo view shows the live proof." +«Мы сделали инженерную симуляцию сортировочной ячейки OZON. Полный цикл: detection → classification → command → routing. Показываем happy path B/C/D, fault handling и timing. Главный экран — product demo, инженерные панели ниже.» -## 3-Minute Guided Demo +## 3-Minute Demo -1. Open **Guided Demo**. -2. Click **Start Guided Demo**. -3. Point to the **Current Proof Card** and the **Scene**. - - "Watch how detection leads to classification and routing." -4. Click **Next** to show `Oversized item to C`. - - "Dimensions fail first, priority C." -5. Click **Next** to show `Round object to D`. - - "Dimensions pass, but roundness triggers D." -6. Click **Next** through `Boundary`, `Low confidence`, `Jam`, `Emergency stop`. - - On faults: click "Apply fault scenario" to confirm. - - "The line stops safely on emergency." +1. На первом экране укажите Hero: что это за система и цепочку Detection → Classification → Command → Routing. +2. Нажмите **Запустить демо**. +3. В блоке **Главное демо** покажите упрощённую сцену и карточку результата: + - товар; + - категория B/C/D; + - причина решения; + - команда `ROUTE_TO_*`; + - целевая зона. +4. Нажимайте **Next step**, следите за **Этапами цикла**. +5. В **Сценариях** нажмите **Показать** на «Негабарит» — приоритет C. +6. Затем «Круглый объект» — зона D. +7. Затем «Застревание» или «Аварийная остановка» — FAULT / EMERGENCY_STOP, Reset. -## 5-Minute Guided Demo +## 5-Minute Demo -Use all 10 demo steps in Guided Demo View: +Пройдите сценарии по карточкам: -1. System overview. -2. Normal item to B. -3. Oversized item to C. -4. Round object to D. -5. Boundary dimensions. -6. Low confidence fallback. -7. Close items queue. -8. Jam / fault handling. -9. Emergency stop. -10. Performance and synchronization. +1. Обычный товар — B/C/D поток. +2. Негабарит — C. +3. Круглый объект — D. +4. Пограничные размеры — строгие min/max. +5. Низкая уверенность CV — fallback. +6. Очередь товаров — queue/spacing. +7. Застревание — FAULT. +8. Аварийная остановка — EMERGENCY_STOP. -For each step: +Для каждого: -1. Read the **Demo Narration Card**. -2. Use **Next** or the **Primary Action Button**. -3. Point to the highlighted values in the **Proof Card**. -4. Read the **Presenter Phrase** from the screen. +1. Нажмите **Показать**. +2. **Start demo** / **Next step**. +3. Укажите категорию, причину и маршрут на карточке результата. +4. Покажите активный шаг в Storyline Stepper. -## What to say on each step +## Engineering Details -Use the `presenterPhrase` text shown in the **Demo Narration Card**. It is specifically written to be short and clear for the jury. +Если жюри просит PID, event log, sensors или полный layout: -## What to do if demo gets stuck +1. Нажмите **Инженерный режим** (header / hero / demo). +2. Откроется секция **Engineering Details** с полными панелями. +3. Полная сцена — `SorterScene variant="full"`. -- Click **Reset Demo**. -- If still stuck, switch back to **Engineering Mode** and use manual scenario selection. -- Proceed manually. +## Что делать, если демо зависло -## Fallback If Domain Does Not Open +1. **Reset** в header или в блоке демо. +2. Выберите сценарий заново карточкой **Показать**. +3. При необходимости откройте Engineering Details и смените сценарий там. -1. SSH to server. -2. Check local frontend: +## Fallback, если домен не открывается + +1. SSH на сервер. +2. Проверьте локальный frontend: ```bash curl -I http://127.0.0.1:3100/ ``` -3. Check Docker project: +3. Проверьте Docker: ```bash docker compose -p owl -f /opt/arhipovdan/app/docker-compose.server.yml ps ``` -4. If screenshots/video are prepared later, show them while explaining the same demo steps. +## Mobile / no horizontal scroll -## What Each Screen Proves +Перед защитой проверьте: -- SVG scene proves physical route and actuator command. -- Classification panel proves why category B/C/D was selected. -- Timeline proves synchronization and step-by-step behavior. -- PID panel proves conveyor control-loop behavior. -- Event Log proves traceability. -- Criteria panel proves coverage of OZON evaluation points. +- 1920×1080 — hero + demo читаемы, CTA видны; +- 1440×900 — нет debug-dashboard на первом экране; +- 390×844 — одна колонка, крупные кнопки, сцена не вылезает. + +В консоли: + +```js +document.documentElement.scrollWidth <= document.documentElement.clientWidth +``` + +## What Each Block Proves + +- Hero — смысл проекта за 10 секунд. +- Product Demo scene — физический маршрут и команда. +- Proof card — почему выбрана категория B/C/D. +- Storyline — текущий этап цикла. +- Scenario cards — jury test cases без узкого скролл-списка. +- Criteria cards — покрытие критериев OZON. +- Engineering Details — timeline, PID, event log, sensors. diff --git a/docs/JURY_QA.md b/docs/JURY_QA.md index 32b51b3..fbea825 100644 --- a/docs/JURY_QA.md +++ b/docs/JURY_QA.md @@ -10,23 +10,23 @@ The animation is driven by a deterministic state machine, classifier, sensor sta ## 3. How Do You Prove The Route Was Correct? -The decision tree shows thresholds and PASS/FAIL, the scene shows the selected command and route arrow, and Event Log records state, command/category and item id. +The proof card shows category, reason and `ROUTE_TO_*`. The scene highlights the selected route arrow and zone B/C/D. Event Log in Engineering Details records state, command and item id. ## 4. Where Are OZON Rules Reflected? -The rules are explicit in classification limits: min/max dimensions, roundness threshold and C priority. Criteria Coverage maps these rules to scenarios and docs. +The rules are explicit in classification limits: min/max dimensions, roundness threshold and C priority. Criteria Cards map these rules to scenarios. Full checklist is in Engineering Details. ## 5. How Does The Operator Safely Stop The System? -The `emergency_stop` scenario enters `EMERGENCY_STOP`, stops conveyor target speed and requires Reset. In Presentation Mode, Safe Demo requires confirmation before fault scenarios. +Open scenario **Аварийная остановка**. The system enters `EMERGENCY_STOP`, stops conveyor target speed and requires Reset. ## 6. What Happens On Sensor Failure Or Low Confidence? -Low confidence is logged as a warning. The system still applies deterministic dimensions/roundness rules as a fallback and shows this in the panel. +Low confidence is logged as a warning. The system still applies deterministic dimensions/roundness rules as a fallback and shows this on the proof card. ## 7. Why 2D, Not 3D? -2D is enough for MVP validation: it shows geometry, timing, sensor positions, routes and state transitions without spending effort on heavy rendering. +2D is enough for MVP validation: it shows geometry, timing, sensor positions, routes and state transitions without heavy rendering. ## 8. What Is Needed For A Physical Prototype? @@ -38,7 +38,7 @@ The MVP uses pseudo-CV: bbox, dimensions, confidence and latency are derived fro ## 10. How Is Classification Correctness Proven? -The decision tree shows PASS/FAIL for dimensions and roundness, actual values, thresholds and final category. Tests cover key boundary cases. +The proof card shows dimensions, roundness and reason. Tests cover key boundary cases. Engineering Details has the full decision evidence and criteria checklist. ## 11. How Are Dimensions And Circular Section Handled? @@ -50,7 +50,7 @@ Oversized or undersized items are operationally unsafe for the main line and mus ## 13. How Is Synchronization Shown? -Cycle timeline shows state order, simulated timestamps, durations and status: done, active, pending or skipped. +Storyline Stepper shows the active stage. In Engineering Details, Cycle Timeline shows state order, timestamps, durations and status: done, active, pending or skipped. ## 14. How Does The Actuator Part Work? @@ -58,16 +58,24 @@ The stop-gate fixes the item. B opens the gate, C extends pusher C, D extends pu ## 15. What Happens On Jam? -The system enters FAULT, conveyor target speed becomes 0, actual speed decays toward 0, and Reset is required. +Open scenario **Застревание**. The system enters FAULT, conveyor target speed becomes 0, actual speed decays toward 0, and Reset is required. ## 16. Why is the interface a Product Demo Page now? -To avoid cognitive overload during the pitch. The jury needs to understand the decision and see the route clearly without distraction from raw JSON logs or PID graphs, especially on laptop projectors or mobile devices. +To avoid cognitive overload during the pitch. The jury needs to understand the decision and see the route clearly without distraction from raw logs or PID graphs on the first screen. ## 17. How does the Demo prove engineering realism? -Even in the Product Demo Section, the scene and proof card are driven by the real underlying state machine, classifier, and metric engine—not a pre-rendered video. +Even in the Product Demo Section, the scene and proof card are driven by the real state machine, classifier and metrics — not a pre-rendered video. ## 18. Where to see the full event log and PID? -Scroll down and open the "Engineering Details (Engineering Mode)" accordion. +Scroll to **Engineering Details** or click **Инженерный режим**. Open the section to see state machine, sensors, PID, timeline, event log and full criteria checklist. + +## 19. How to check mobile and no horizontal scroll? + +Use widths 1920×1080, 1440×900 and 390×844. On mobile the layout is one column, buttons are large, the scene scales with `width: 100%`. In console: + +```js +document.documentElement.scrollWidth <= document.documentElement.clientWidth +``` diff --git a/docs/SUBMISSION_CHECKLIST.md b/docs/SUBMISSION_CHECKLIST.md index e4e8581..f96d05e 100644 --- a/docs/SUBMISSION_CHECKLIST.md +++ b/docs/SUBMISSION_CHECKLIST.md @@ -8,16 +8,18 @@ Before final submission: - [ ] `https://arhipovdan.ru/` opens. - [ ] `https://www.arhipovdan.ru/` opens. - [ ] `https://ai-shorts.ru/` still works and was not affected. -- [ ] Presentation Mode opens. -- [ ] Demo steps checked. -- [ ] Safe Demo confirmation checked for jam/emergency. -- [ ] App is responsive on Desktop (1920x1080), Laptop (1440x900) and Mobile (390x844). -- [ ] NO horizontal scroll on body/app container. -- [ ] Product Demo layout shows step, scene, and result. -- [ ] Scenarios are displayed as cards. -- [ ] Engineering Details accordion works and shows all technical panels. -- [ ] Engineering Mode still works. +- [ ] Product Demo page opens (Hero first, not engineering dashboard). +- [ ] Start demo / Next step / Reset work. +- [ ] Storyline Stepper updates with machine state. +- [ ] Scenario cards open cases with **Показать**. +- [ ] Criteria cards show OZON coverage and linked scenarios. +- [ ] Engineering Details is collapsed by default and opens from CTA. +- [ ] Engineering Details shows full panels (state machine, sensors, PID, timeline, event log, criteria). +- [ ] App is responsive on Desktop (1920×1080), Laptop (1440×900) and Mobile (390×844). +- [ ] NO horizontal scroll on body / `#root` / product page. +- [ ] Mobile: one column, buttons ≥ 44px, scene scales. - [ ] Scenarios checked: normal_flow, oversized_item, round_object, boundary_dimensions, close_items, low_confidence, jam, emergency_stop. -- [ ] Docs present: README, ARCHITECTURE, DEMO_SCRIPT, SCENARIOS, JURY_QA, SUBMISSION_CHECKLIST. -- [ ] Branch pushed to `origin/dan_branch`. -- [ ] Nginx backup path from deploy is known if infrastructure rollback is needed. +- [ ] Docs present: README, ARCHITECTURE, DEMO_SCRIPT, SCENARIOS, JURY_QA, UI_UX_REDESIGN_AUDIT, SUBMISSION_CHECKLIST. +- [ ] Cursor rules present in `.cursor/rules/`. +- [ ] Branch ready for manual push to `origin/dan_branch`. +- [ ] Nginx / SSL / neighboring projects were not modified. diff --git a/docs/UI_UX_REDESIGN_AUDIT.md b/docs/UI_UX_REDESIGN_AUDIT.md new file mode 100644 index 0000000..2f81b9e --- /dev/null +++ b/docs/UI_UX_REDESIGN_AUDIT.md @@ -0,0 +1,58 @@ +# UI/UX Redesign Audit + +## 1. Что сейчас плохо + +- Перегруженность: первый экран похож на engineering debug-dashboard. +- Horizontal scroll из-за `min-width: 1180px` и жёстких grid-колонок. +- Слабая адаптивность: mobile не в одну колонку, мелкие tap targets. +- Debug-dashboard вместо product demo: много панелей сразу. +- Мелкий текст (11–13px) и плотная типографика. +- Непонятный первый экран: не ясно, что это за система и как запустить демо. + +## 2. Цель редизайна + +Сделать чистый, адаптивный product demo для защиты хакатона: за 10 секунд понятно, что это, как запустить и какой результат показать. Инженерные панели сохранить, но спрятать ниже. + +## 3. Новая информационная архитектура + +1. Hero — смысл, CTA, цепочка Detection → Classification → Command → Routing +2. Product Demo — сцена + карточка результата + управление +3. Storyline Stepper — текущий этап цикла +4. Scenario Cards — jury-сценарии карточками +5. Criteria Cards — покрытие критериев OZON +6. Engineering Details — сворачиваемые техпанели + +## 4. Компоненты: создать / изменить + +| Компонент | Действие | +|-----------|----------| +| HeroSection | создать / доработать | +| ProductDemoSection | создать / доработать | +| StorylineStepper | создать / доработать | +| ScenarioCards | создать / доработать | +| CriteriaCards | создать / доработать | +| EngineeringDetails | создать / доработать | +| Header | упростить до product-nav | +| App | собрать product page | +| SorterScene | добавить `variant: full \| simple` | +| styles.css | responsive redesign | + +## 5. В Engineering Details + +StateMachinePanel, TimelinePanel, PidPanel, EventLog, MetricsPanel, SensorPanel, ScenarioPanel, OzonCriteriaPanel, SorterScene `variant="full"`. + +## 6. Риски + +- Не сломать domain logic / tests. +- Не потерять инженерные функции для жюри. +- Не затронуть nginx / соседние проекты. +- Сохранить обработчики start / step / reset / scenario select. + +## 7. Definition of Done + +- Нет horizontal scroll на 1920 / 1440 / 390. +- Hero понятен, CTA видны. +- Demo ведёт по цепочке Detection → Routing. +- Engineering Details свёрнуты по умолчанию. +- `npm run build` и `npm run test` проходят. +- Docker `owl` отдаёт страницу на `:3100` и доменах. diff --git a/src/App.tsx b/src/App.tsx index 3d16130..493a181 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,24 +1,30 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import Header from './components/Header'; import HeroSection from './components/HeroSection'; import ProductDemoSection from './components/ProductDemoSection'; +import StorylineStepper from './components/StorylineStepper'; import ScenarioCards from './components/ScenarioCards'; import CriteriaCards from './components/CriteriaCards'; -import EngineeringDetails from './components/EngineeringDetails'; +import EngineeringDetails, { type EngineeringDetailsHandle } from './components/EngineeringDetails'; import { DEMO_STEPS } from './data/demoSteps'; import { SCENARIOS } from './data/scenarios'; -import { createSimulation, setRunning, stepSimulationToNextState } from './domain/simulation'; +import { createSimulation, stepSimulationToNextState } from './domain/simulation'; import type { ScenarioId, SimulationState } from './domain/types'; +function scrollToId(id: string) { + document.getElementById(id)?.scrollIntoView({ behavior: 'smooth', block: 'start' }); +} + export default function App() { const [activeScenarioId, setActiveScenarioId] = useState('normal_flow'); const [demoStepIndex, setDemoStepIndex] = useState(0); - + const engineeringRef = useRef(null); + const activeScenario = useMemo( - () => SCENARIOS.find((s) => s.id === activeScenarioId) ?? SCENARIOS[0], + () => SCENARIOS.find((scenario) => scenario.id === activeScenarioId) ?? SCENARIOS[0], [activeScenarioId], ); - + const currentDemoStep = DEMO_STEPS[demoStepIndex]; const [simulation, setSimulation] = useState(() => createSimulation(activeScenario)); @@ -26,28 +32,30 @@ export default function App() { setSimulation(createSimulation(activeScenario)); }, [activeScenario]); - // We are not using automatic interval in the new product demo by default to keep it step-by-step - // but if the user wants it to run automatically, we can preserve it if needed. - // Let's use handleNext to just step through. - const handleStartDemo = () => { - document.getElementById('demo')?.scrollIntoView({ behavior: 'smooth' }); - if (!simulation.currentItem && demoStepIndex === 0) { - setSimulation(stepSimulationToNextState(simulation)); - } + scrollToId('demo'); + setSimulation((current) => { + if (current.machineState === 'IDLE' && !current.currentItem) { + return stepSimulationToNextState(current); + } + return current; + }); }; const handleNext = () => { - // If simulation is finished or errored, we might need to reset or move to next scenario - const isFinished = simulation.machineState === 'RETURN_HOME' || simulation.machineState === 'FAULT' || simulation.machineState === 'EMERGENCY_STOP'; - - if (isFinished) { + const finished = + simulation.machineState === 'RETURN_HOME' || + simulation.machineState === 'FAULT' || + simulation.machineState === 'EMERGENCY_STOP'; + + if (finished) { const nextIndex = (demoStepIndex + 1) % DEMO_STEPS.length; setDemoStepIndex(nextIndex); setActiveScenarioId(DEMO_STEPS[nextIndex].scenarioId); - } else { - setSimulation((current) => stepSimulationToNextState(current)); + return; } + + setSimulation((current) => stepSimulationToNextState(current)); }; const handleReset = () => { @@ -56,39 +64,66 @@ export default function App() { const handleScenarioChange = (scenarioId: ScenarioId) => { setActiveScenarioId(scenarioId); - // Find first step that matches this scenario - const stepIdx = DEMO_STEPS.findIndex(s => s.scenarioId === scenarioId); - if (stepIdx >= 0) setDemoStepIndex(stepIdx); - document.getElementById('demo')?.scrollIntoView({ behavior: 'smooth' }); + const stepIdx = DEMO_STEPS.findIndex((step) => step.scenarioId === scenarioId); + if (stepIdx >= 0) { + setDemoStepIndex(stepIdx); + } + scrollToId('demo'); + }; + + const handleOpenEngineering = () => { + engineeringRef.current?.open(); + }; + + const handleShowScenarios = () => { + scrollToId('scenarios'); }; return (
-
- +
scrollToId('demo')} + onOpenScenarios={handleShowScenarios} + onOpenEngineering={handleOpenEngineering} + /> +
- - - - - + + + + - - - - + + + +
- +
-

OZON Tech Hackathon 2026. Sorter Simulation.

+

OZON Tech Hackathon 2026 · Sorter Simulation · Product Demo

); diff --git a/src/components/CriteriaCards.tsx b/src/components/CriteriaCards.tsx index 3fb3f72..9b5f4d7 100644 --- a/src/components/CriteriaCards.tsx +++ b/src/components/CriteriaCards.tsx @@ -1,27 +1,126 @@ -import { OZON_CRITERIA } from '../data/criteria'; +import type { ScenarioId } from '../domain/types'; -export default function CriteriaCards() { +interface CriteriaCard { + id: string; + title: string; + evidence: string; + status: 'covered' | 'demo'; + linkedScenario: ScenarioId; + linkedLabel: string; +} + +const PRODUCT_CRITERIA: CriteriaCard[] = [ + { + id: 'classification', + title: 'Классификация', + evidence: 'Rule-based дерево решений выдаёт категорию B/C/D для каждого SKU.', + status: 'covered', + linkedScenario: 'normal_flow', + linkedLabel: 'Обычный товар', + }, + { + id: 'dimensions', + title: 'Габариты', + evidence: 'Проверка min/max размеров до анализа формы.', + status: 'covered', + linkedScenario: 'oversized_item', + linkedLabel: 'Негабарит', + }, + { + id: 'round-section', + title: 'Круг в сечении', + evidence: 'Roundness ≥ 0.8 направляет товар в зону D.', + status: 'covered', + linkedScenario: 'round_object', + linkedLabel: 'Круглый объект', + }, + { + id: 'rule-priority', + title: 'Приоритет правил', + evidence: 'Сначала габариты (C), затем форма (D), иначе основной поток B.', + status: 'covered', + linkedScenario: 'boundary_dimensions', + linkedLabel: 'Пограничные размеры', + }, + { + id: 'routing', + title: 'Маршрутизация B/C/D', + evidence: 'Категория превращается в команду ROUTE_TO_* и физический маршрут.', + status: 'covered', + linkedScenario: 'normal_flow', + linkedLabel: 'Обычный товар', + }, + { + id: 'cycle-time', + title: 'Cycle time', + evidence: 'Метрики показывают длительность цикла и throughput.', + status: 'covered', + linkedScenario: 'close_items', + linkedLabel: 'Очередь товаров', + }, + { + id: 'faults', + title: 'Нештатные ситуации', + evidence: 'Jam переводит систему в FAULT и останавливает конвейер.', + status: 'demo', + linkedScenario: 'jam', + linkedLabel: 'Застревание', + }, + { + id: 'safety', + title: 'Безопасность', + evidence: 'Emergency stop замораживает линию до Reset.', + status: 'demo', + linkedScenario: 'emergency_stop', + linkedLabel: 'Аварийная остановка', + }, + { + id: 'reproducibility', + title: 'Воспроизводимость', + evidence: 'Сценарии, тесты и docs позволяют повторить демо жюри.', + status: 'covered', + linkedScenario: 'low_confidence', + linkedLabel: 'Низкая уверенность CV', + }, +]; + +interface Props { + onLinkedScenario?: (id: ScenarioId) => void; +} + +export default function CriteriaCards({ onLinkedScenario }: Props) { return ( -
+
-

Покрытие критериев OZON

-

Каждый инженерный критерий хакатона покрывается симуляцией и тестами.

+

Критерии OZON

+

Каждый критерий хакатона закрыт сценарием, симуляцией и тестами.

- +
- {OZON_CRITERIA.map((criterion) => ( -
+ {PRODUCT_CRITERIA.map((criterion) => ( +

{criterion.title}

- - {criterion.status} + + {criterion.status === 'covered' ? 'Covered' : 'Demo'}

{criterion.evidence}

- Docs: {criterion.relatedDoc} + Сценарий: + {onLinkedScenario ? ( + + ) : ( + {criterion.linkedLabel} + )}
-
+ ))}
diff --git a/src/components/CurrentProofCard.tsx b/src/components/CurrentProofCard.tsx index e692131..76e1979 100644 --- a/src/components/CurrentProofCard.tsx +++ b/src/components/CurrentProofCard.tsx @@ -1,4 +1,3 @@ -import { DIMENSION_LIMITS } from '../domain/classifier'; import type { SimulationState } from '../domain/types'; export default function CurrentProofCard({ simulation }: { simulation: SimulationState }) { @@ -8,14 +7,19 @@ export default function CurrentProofCard({ simulation }: { simulation: Simulatio const dimensions = item?.dimensionsMm; const lowConfidence = Boolean(item && item.confidence < 0.65); const stopped = simulation.machineState === 'FAULT' || simulation.machineState === 'EMERGENCY_STOP'; + const command = simulation.machineState.startsWith('ROUTE_TO_') + ? simulation.machineState + : result + ? `ROUTE_TO_${result.category}` + : 'WAITING'; if (!current) { return (
-

Awaiting cycle

-

Ready to demonstrate sorting cycle

-

Нажмите «Запустить демо» для начала или переключите сценарий ниже.

+

Ожидание цикла

+

Готово к демонстрации

+

Нажмите Start demo — товар пройдёт Detection → Classification → Command → Routing.

); @@ -25,33 +29,33 @@ export default function CurrentProofCard({ simulation }: { simulation: Simulatio

Результат классификации

-

{item?.name} (ID: {item?.id})

+

{item?.name}

- +
Категория - {result?.category || '-'} + {result?.category ?? '—'}
- Маршрут - {result?.category ? `Зона ${result.category}` : '-'} + Зона + {result?.category ? `Зона ${result.category}` : '—'}
-
+
Команда - {simulation.machineState.startsWith('ROUTE_TO_') ? simulation.machineState : 'WAITING'} + {command}
- Габариты: + Габариты - {dimensions?.width} x {dimensions?.depth} x {dimensions?.height} мм + {dimensions?.width} × {dimensions?.depth} × {dimensions?.height} мм
- Roundness: + Roundness {item?.roundness.toFixed(2)} @@ -59,12 +63,15 @@ export default function CurrentProofCard({ simulation }: { simulation: Simulatio
- Почему такое решение: {result?.reason} - {lowConfidence &&

Низкая уверенность CV — использован rule-based fallback

} - {simulation.machineState === 'FAULT' &&

FAULT: Конвейер остановлен

} - {simulation.machineState === 'EMERGENCY_STOP' &&

EMERGENCY_STOP: Аварийная остановка

} - {simulation.metrics.queueLength > 1 &&

Внимание: очередь товаров

} + Причина решения +

{result?.reason}

+ {lowConfidence ?

Низкая уверенность CV — rule-based fallback

: null} + {simulation.machineState === 'FAULT' ?

FAULT: конвейер остановлен

: null} + {simulation.machineState === 'EMERGENCY_STOP' ? ( +

EMERGENCY_STOP: аварийная остановка

+ ) : null} + {simulation.metrics.queueLength > 1 ?

Очередь товаров

: null}
); -} \ No newline at end of file +} diff --git a/src/components/EngineeringDetails.tsx b/src/components/EngineeringDetails.tsx index d56a6bf..e4af842 100644 --- a/src/components/EngineeringDetails.tsx +++ b/src/components/EngineeringDetails.tsx @@ -1,36 +1,107 @@ -import { useState } from 'react'; +import { forwardRef, useImperativeHandle, useState } from 'react'; import StateMachinePanel from './StateMachinePanel'; import TimelinePanel from './TimelinePanel'; import PidPanel from './PidPanel'; import EventLog from './EventLog'; import MetricsPanel from './MetricsPanel'; import SensorPanel from './SensorPanel'; -import type { SimulationState } from '../domain/types'; +import ScenarioPanel from './ScenarioPanel'; +import OzonCriteriaPanel from './OzonCriteriaPanel'; +import SorterScene from './SorterScene'; +import { DEMO_STEPS } from '../data/demoSteps'; +import { SCENARIOS } from '../data/scenarios'; +import type { ScenarioId, SimulationState } from '../domain/types'; -export default function EngineeringDetails({ simulation }: { simulation: SimulationState }) { - const [isOpen, setIsOpen] = useState(false); - - return ( -
-
setIsOpen(!isOpen)}> -

Инженерные детали (Engineering Mode)

- -
- - {isOpen && ( -
-
-
-
-
-
-
-
-
-
- )} -
- ); +export interface EngineeringDetailsHandle { + open: () => void; } + +interface Props { + simulation: SimulationState; + onScenarioChange: (id: ScenarioId) => void; +} + +const EngineeringDetails = forwardRef( + function EngineeringDetails({ simulation, onScenarioChange }, ref) { + const [isOpen, setIsOpen] = useState(false); + const currentStep = + DEMO_STEPS.find((step) => step.scenarioId === simulation.scenario.id) ?? DEMO_STEPS[0]; + + useImperativeHandle(ref, () => ({ + open: () => { + setIsOpen(true); + requestAnimationFrame(() => { + document.getElementById('engineering')?.scrollIntoView({ behavior: 'smooth' }); + }); + }, + })); + + return ( +
+
+
+

For experts

+

Engineering Details

+

+ Полные технические панели: state machine, sensors, PID, timeline, event log и критерии. +

+
+ +
+ + {isOpen ? ( +
+
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ ) : null} +
+ ); + }, +); + +export default EngineeringDetails; diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 77d9758..2fe56c2 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -2,30 +2,49 @@ import type { SimulationState } from '../domain/types'; interface HeaderProps { simulation: SimulationState; + onReset: () => void; + onOpenDemo: () => void; + onOpenScenarios: () => void; + onOpenEngineering: () => void; } -export default function Header({ simulation }: HeaderProps) { +export default function Header({ + simulation, + onReset, + onOpenDemo, + onOpenScenarios, + onOpenEngineering, +}: HeaderProps) { return (
- O +
- OZON Tech - Sorter Simulation + OZON Tech Sorter + Product Demo
- - - -
-
+ + + +
+
); diff --git a/src/components/HeroSection.tsx b/src/components/HeroSection.tsx index d26db98..236b00f 100644 --- a/src/components/HeroSection.tsx +++ b/src/components/HeroSection.tsx @@ -1,14 +1,27 @@ -export default function HeroSection({ onStartDemo }: { onStartDemo: () => void }) { +interface HeroSectionProps { + onStartDemo: () => void; + onShowScenarios: () => void; + onOpenEngineering: () => void; +} + +export default function HeroSection({ + onStartDemo, + onShowScenarios, + onOpenEngineering, +}: HeroSectionProps) { return ( -
+
-

Интеллектуальная система сортировки товаров

+

OZON Tech · Product Demo

+

+ Интеллектуальная система сортировки товаров +

- Симуляция показывает полный цикл: обнаружение товара, классификация по правилам, - управляющая команда и маршрутизация в зоны B, C, D. + Симуляция показывает полный цикл: обнаружение товара, классификация по правилам, + управляющая команда и маршрутизация в зоны B/C/D.

- -
+ +
Computer Vision Rule-based classification Actuator routing @@ -17,34 +30,43 @@ export default function HeroSection({ onStartDemo }: { onStartDemo: () => void }
- - Показать сценарии - Инженерный режим + + +
- -
+ +
); diff --git a/src/components/ProductDemoSection.tsx b/src/components/ProductDemoSection.tsx index ee3447f..dc6c80a 100644 --- a/src/components/ProductDemoSection.tsx +++ b/src/components/ProductDemoSection.tsx @@ -1,40 +1,97 @@ import SorterScene from './SorterScene'; import CurrentProofCard from './CurrentProofCard'; -import StorylineStepper from './StorylineStepper'; import type { SimulationState } from '../domain/types'; interface Props { simulation: SimulationState; + demoStepTitle: string; + onStartDemo: () => void; onNext: () => void; onReset: () => void; - demoStepTitle: string; + onOpenEngineering: () => void; } -export default function ProductDemoSection({ simulation, onNext, onReset, demoStepTitle }: Props) { +export default function ProductDemoSection({ + simulation, + demoStepTitle, + onStartDemo, + onNext, + onReset, + onOpenEngineering, +}: Props) { + const item = simulation.currentItem; + const category = item?.classification.category; + const command = simulation.machineState.startsWith('ROUTE_TO_') + ? simulation.machineState + : item?.classification + ? `ROUTE_TO_${item.classification.category}` + : '—'; + return ( -
+
-

Live Demo

+
+

Live product demo

+

Главное демо

+

+ Слева — упрощённая сцена конвейера. Справа — товар, категория, причина решения и команда маршрутизации. +

+
- Текущий шаг: {demoStepTitle} - + Шаг: + {demoStepTitle} +
- - - +
-
- -
- -
- -
- - +
+

+ Товар проходит камеру, классификатор и gate, затем уходит в зону B, C или D. +

+
+
+ +
); diff --git a/src/components/ScenarioCards.tsx b/src/components/ScenarioCards.tsx index e4f765b..6f56d08 100644 --- a/src/components/ScenarioCards.tsx +++ b/src/components/ScenarioCards.tsx @@ -6,39 +6,97 @@ interface Props { onScenarioChange: (id: ScenarioId) => void; } +const SCENARIO_UI: Record< + ScenarioId, + { title: string; summary: string; badge: string; badgeTone: 'b' | 'c' | 'd' | 'mixed' | 'fault' } +> = { + normal_flow: { + title: 'Обычный товар', + summary: 'Стандартный поток с маршрутизацией в B, C и D.', + badge: 'B / C / D', + badgeTone: 'mixed', + }, + oversized_item: { + title: 'Негабарит', + summary: 'Габариты превышают лимит — приоритет зоны C.', + badge: 'Зона C', + badgeTone: 'c', + }, + round_object: { + title: 'Круглый объект', + summary: 'Габариты проходят, но круглое сечение ведёт в D.', + badge: 'Зона D', + badgeTone: 'd', + }, + boundary_dimensions: { + title: 'Пограничные размеры', + summary: 'Проверка строгих min/max границ габаритов.', + badge: 'B / C', + badgeTone: 'mixed', + }, + low_confidence: { + title: 'Низкая уверенность CV', + summary: 'Предупреждение CV и rule-based fallback.', + badge: 'Fallback', + badgeTone: 'b', + }, + close_items: { + title: 'Очередь товаров', + summary: 'Близкие товары обрабатываются по одному циклу.', + badge: 'Queue', + badgeTone: 'mixed', + }, + jam: { + title: 'Застревание', + summary: 'Jam у gate переводит линию в FAULT.', + badge: 'FAULT', + badgeTone: 'fault', + }, + emergency_stop: { + title: 'Аварийная остановка', + summary: 'EMERGENCY_STOP останавливает движение до Reset.', + badge: 'E-STOP', + badgeTone: 'fault', + }, +}; + export default function ScenarioCards({ scenarios, activeScenarioId, onScenarioChange }: Props) { return ( -
+
-

Сценарии тестирования (Jury Test Cases)

-

Выберите сценарий для проверки поведения системы в разных условиях.

+

Сценарии

+

Выберите кейс жюри: обычный поток, негабарит, круглое сечение, очередь или авария.

- +
- {scenarios.map((scenario) => ( -
onScenarioChange(scenario.id)} - > -
-

{scenario.name}

- {scenario.id === activeScenarioId && Выбран} -
-

{scenario.description}

-
-
- Цель: - {scenario.goal} + {scenarios.map((scenario) => { + const ui = SCENARIO_UI[scenario.id]; + const active = scenario.id === activeScenarioId; + + return ( +
+
+

{ui.title}

+ {ui.badge}
-
- Результат: - {scenario.expectedCategorySummary} -
-
- -
- ))} +

{ui.summary}

+

+ Ожидаемый результат + {scenario.expectedCategorySummary} +

+ + + ); + })}
); diff --git a/src/components/SorterScene.tsx b/src/components/SorterScene.tsx index 88dba2f..0d55a2e 100644 --- a/src/components/SorterScene.tsx +++ b/src/components/SorterScene.tsx @@ -64,106 +64,172 @@ function LegendItem({ color, label }: { color: string; label: string }) { ); } -export default function SorterScene({ simulation }: { simulation: SimulationState }) { +interface SorterSceneProps { + simulation: SimulationState; + variant?: 'full' | 'simple'; +} + +export default function SorterScene({ simulation, variant = 'full' }: SorterSceneProps) { const current = simulation.currentItem; const category = current?.classification.category; const color = category ? routeColors[category] : '#38bdf8'; const position = itemPosition(simulation); const detecting = simulation.machineState === 'DETECTING'; + const classifying = simulation.machineState === 'CLASSIFYING' || simulation.machineState === 'WAITING_AT_GATE'; const routeVisible = simulation.machineState.startsWith('ROUTE_TO_'); const stopped = simulation.machineState === 'FAULT' || simulation.machineState === 'EMERGENCY_STOP'; const itemWidth = current ? Math.max(24, Math.min(74, current.item.dimensionsMm.width / 6)) : 56; const itemHeight = current ? Math.max(18, Math.min(58, current.item.dimensionsMm.depth / 5.5)) : 48; + const isSimple = variant === 'simple'; return ( -
-
-
-

Work zone 6000 x 10000 mm / conveyor 500 mm

-

Engineering layout, sensors and routing commands

+
+ {!isSimple ? ( +
+
+

Work zone 6000 x 10000 mm / conveyor 500 mm

+

Engineering layout, sensors and routing commands

+
+
{simulation.machineState}
-
{simulation.machineState}
-
+ ) : ( +
+

Конвейер · камера · classifier · gate · зоны B/C/D

+
{simulation.machineState}
+
+ )} + - + - - + + {(['B', 'C', 'D'] as Category[]).map((route) => ( - + ))} - - Scaled plan: 6000 mm x 10000 mm work cell + {!isSimple ? ( + + ) : null} - - - + {!isSimple ? ( + <> + Scaled plan: 6000 mm x 10000 mm work cell + + + + + ) : null} - A feed zone + {isSimple ? 'A' : 'A feed zone'} - + - B main sorter + + {isSimple ? 'B' : 'B main sorter'} + - + - D roll-cage 1200 x 800 x 800 mm + + {isSimple ? 'D' : 'D roll-cage 1200 x 800 x 800 mm'} + - + - C roll-cage 1200 x 800 x 800 mm + + {isSimple ? 'C' : 'C roll-cage 1200 x 800 x 800 mm'} + - {Array.from({ length: 18 }).map((_, index) => ( - - ))} + {!isSimple + ? Array.from({ length: 18 }).map((_, index) => ( + + )) + : null} - Camera / bbox - - - - - Laser height - - - - - Ultrasonic gate + {isSimple ? 'Camera' : 'Camera / bbox'} + {!isSimple ? ( + <> + + + + Laser height + + + + + Ultrasonic gate + + + ) : ( + + + + Classifier + + )} + - Stop-gate {simulation.gate.open ? 'open' : 'closed'} + {isSimple ? 'Gate' : `Stop-gate ${simulation.gate.open ? 'open' : 'closed'}`} - - - Pusher C command - - - - Pusher D command - + {!isSimple ? ( + <> + + + Pusher C command + + + + Pusher D command + + + ) : null} + + + + - - - {routeVisible && category ? ( @@ -184,11 +250,22 @@ export default function SorterScene({ simulation }: { simulation: SimulationStat stroke="#ffffff" strokeWidth="1.4" /> - {current.item.id} + + {isSimple ? current.item.name : current.item.id} + {detecting ? ( - - bbox {current.item.dimensionsMm.width} x {current.item.dimensionsMm.depth} mm + + {!isSimple ? ( + + bbox {current.item.dimensionsMm.width} x {current.item.dimensionsMm.depth} mm + + ) : null} ) : null} @@ -202,13 +279,23 @@ export default function SorterScene({ simulation }: { simulation: SimulationStat ) : null} - - - - - - - + {!isSimple ? ( + + + + + + + + ) : ( + + + + + + + + )}
); diff --git a/src/components/StorylineStepper.tsx b/src/components/StorylineStepper.tsx index 649a7d4..4546e82 100644 --- a/src/components/StorylineStepper.tsx +++ b/src/components/StorylineStepper.tsx @@ -1,65 +1,76 @@ -import type { SimulationState } from '../domain/types'; +import type { MachineState, SimulationState } from '../domain/types'; + +type StepKey = 'detection' | 'classification' | 'decision' | 'command' | 'routing'; +type StepStatus = 'active' | 'completed' | 'pending'; + +const STEPS: Array<{ key: StepKey; number: number; label: string; description: string }> = [ + { key: 'detection', number: 1, label: 'Detection', description: 'Камера и датчики снимают габариты' }, + { key: 'classification', number: 2, label: 'Classification', description: 'Правила проверяют размеры и форму' }, + { key: 'decision', number: 3, label: 'Decision', description: 'Система выбирает категорию B/C/D' }, + { key: 'command', number: 4, label: 'Command', description: 'Формируется команда ROUTE_TO_*' }, + { key: 'routing', number: 5, label: 'Routing', description: 'Товар уходит в целевую зону' }, +]; + +function activeStep(state: MachineState): StepKey | null { + if (state === 'IDLE') return null; + if (state === 'MOVING_TO_CAMERA' || state === 'DETECTING') return 'detection'; + if (state === 'MOVING_TO_GATE' || state === 'WAITING_AT_GATE') return 'classification'; + if (state === 'CLASSIFYING') return 'decision'; + if (state.startsWith('ROUTE_TO_')) return 'command'; + if (state === 'RETURN_HOME') return 'routing'; + if (state === 'FAULT' || state === 'EMERGENCY_STOP') return 'routing'; + return null; +} + +function stepStatus(key: StepKey, current: StepKey | null, machineState: MachineState): StepStatus { + if (machineState === 'IDLE') return 'pending'; + + const order: StepKey[] = ['detection', 'classification', 'decision', 'command', 'routing']; + const currentIndex = current ? order.indexOf(current) : -1; + const keyIndex = order.indexOf(key); + + if (machineState === 'RETURN_HOME') { + return keyIndex < order.length - 1 ? 'completed' : 'active'; + } + + if (machineState === 'FAULT' || machineState === 'EMERGENCY_STOP') { + return keyIndex <= currentIndex ? 'active' : 'pending'; + } + + if (keyIndex < currentIndex) return 'completed'; + if (keyIndex === currentIndex) return 'active'; + return 'pending'; +} export default function StorylineStepper({ simulation }: { simulation: SimulationState }) { - const state = simulation.machineState; - - const isDetection = state === 'MOVING_TO_CAMERA' || state === 'DETECTING'; - const isClassification = state === 'MOVING_TO_GATE' || state === 'WAITING_AT_GATE' || state === 'CLASSIFYING'; - const isCommand = state.startsWith('ROUTE_TO_'); - const isRouting = state === 'RETURN_HOME'; - - const item = simulation.currentItem; - - return ( -
- - - - - -
- ); -} - -function Step({ label, desc, active, done }: { label: string, desc: string, active: boolean, done: boolean }) { - let statusClass = 'step-pending'; - if (active) statusClass = 'step-active'; - else if (done) statusClass = 'step-done'; + const current = activeStep(simulation.machineState); return ( -
-
-
+
+
+

Этапы цикла

+

Текущий этап системы в цепочке Detection → Classification → Decision → Command → Routing.

-
- {label} - {desc} -
-
+ +
    + {STEPS.map((step) => { + const status = stepStatus(step.key, current, simulation.machineState); + return ( +
  1. + +
    + {step.label} + {step.description} + + {status === 'active' ? 'Сейчас' : status === 'completed' ? 'Готово' : 'Ожидание'} + +
    +
  2. + ); + })} +
+
); } diff --git a/src/styles.css b/src/styles.css index 4912e0a..379216e 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1,3 +1,7 @@ +/* ========================================================= + OZON Tech Sorter — Product Demo UI + ========================================================= */ + :root { color-scheme: dark; --bg: #050910; @@ -7,154 +11,908 @@ --muted: #91a4b8; --text: #e5f2ff; --cyan: #38bdf8; + --cyan-hover: #7dd3fc; --green: #4ade80; --orange: #f59e0b; --purple: #c084fc; --red: #fb3d4e; + --section-max: 1200px; + --radius: 16px; + --tap: 44px; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } -* { +*, +*::before, +*::after { box-sizing: border-box; } +html, +body, +#root { + width: 100%; + max-width: 100%; + min-width: 0; + overflow-x: hidden; +} + +html { + scroll-behavior: smooth; +} + body { margin: 0; - min-width: 1180px; min-height: 100vh; background: - radial-gradient(circle at 20% 10%, rgba(56, 189, 248, 0.13), transparent 26%), - radial-gradient(circle at 86% 30%, rgba(192, 132, 252, 0.12), transparent 26%), - linear-gradient(135deg, #03060b 0%, #07111d 54%, #050910 100%); + radial-gradient(circle at 18% 8%, rgba(56, 189, 248, 0.14), transparent 28%), + radial-gradient(circle at 88% 18%, rgba(192, 132, 252, 0.1), transparent 24%), + linear-gradient(160deg, #03060b 0%, #07111d 52%, #050910 100%); color: var(--text); + font-size: 16px; + line-height: 1.5; } -button { - border: 1px solid #2a4a66; - border-radius: 10px; - background: #0e1b2b; - color: var(--text); - cursor: pointer; - font-weight: 700; - letter-spacing: 0.01em; - transition: transform 160ms ease, border-color 160ms ease, background 160ms ease; -} - -button:hover, -button:focus-visible { - border-color: var(--cyan); - background: #11273b; - outline: none; - transform: translateY(-1px); -} - -.app-shell { - min-height: 100vh; - padding: 18px; -} - -.header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 20px; - padding: 16px 18px; - border: 1px solid var(--line); - border-radius: 22px; - background: rgba(7, 14, 23, 0.9); - box-shadow: 0 18px 80px rgba(0, 0, 0, 0.35); +img, +svg, +canvas, +video { + max-width: 100%; + height: auto; } h1, h2, -p { +h3, +p, +ol, +ul { margin: 0; } -h1 { - font-size: clamp(28px, 3vw, 42px); - letter-spacing: -0.045em; +button, +a { + font: inherit; } -h2 { - font-size: 18px; - letter-spacing: -0.02em; +button { + min-height: var(--tap); + border: 1px solid #2a4a66; + border-radius: 12px; + background: #0e1b2b; + color: var(--text); + cursor: pointer; + font-weight: 700; + transition: transform 160ms ease, border-color 160ms ease, background 160ms ease; } -.eyebrow { +button:hover { + border-color: var(--cyan); + background: #11273b; + transform: translateY(-1px); +} + +button:focus-visible, +.nav-link:focus-visible, +.criteria-link:focus-visible { + outline: 2px solid var(--cyan); + outline-offset: 2px; +} + +button:disabled { + cursor: not-allowed; + opacity: 0.45; + transform: none; +} + +.btn-primary, +.btn-secondary { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: var(--tap); + padding: 0 22px; + border-radius: 12px; + font-size: 16px; + font-weight: 700; + text-decoration: none; + text-align: center; +} + +.btn-primary { + border: none; + background: linear-gradient(135deg, #38bdf8, #0ea5e9); + color: #041018; +} + +.btn-primary:hover { + background: linear-gradient(135deg, #7dd3fc, #38bdf8); + color: #041018; +} + +.btn-secondary { + background: #0e1b2b; + color: var(--text); + border: 1px solid #2a4a66; +} + +.category-B { color: var(--green); } +.category-C { color: var(--orange); } +.category-D { color: var(--purple); } +.fail-text { color: var(--red); } +.warning-note { color: #facc15; font-weight: 700; } +.error-note { color: var(--red); font-weight: 700; } + +.eyebrow, +.section-eyebrow { color: var(--cyan); - font-size: 11px; + font-size: 12px; font-weight: 800; - letter-spacing: 0.14em; + letter-spacing: 0.12em; text-transform: uppercase; } -.header-status { +/* ---------- Product page shell ---------- */ + +.product-page { display: flex; - align-items: center; - justify-content: flex-end; - flex-wrap: wrap; - gap: 10px; + flex-direction: column; + min-height: 100vh; + width: 100%; + max-width: 100%; + overflow-x: hidden; } -.status-pill, -.machine-state-chip, -.scenario-label { +.product-main { + flex: 1; + width: min(100%, var(--section-max)); + max-width: 100%; + margin: 0 auto; + padding: 0 clamp(16px, 3vw, 28px); + min-width: 0; +} + +.site-footer { + width: 100%; + padding: clamp(24px, 4vw, 40px) clamp(16px, 3vw, 28px); + border-top: 1px solid var(--line); + color: var(--muted); + text-align: center; + font-size: 14px; +} + +/* ---------- Header ---------- */ + +.site-header { + position: sticky; + top: 0; + z-index: 100; + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 12px; + width: 100%; + max-width: 100%; + padding: 12px clamp(16px, 3vw, 28px); + border-bottom: 1px solid var(--line); + background: rgba(5, 9, 16, 0.88); + backdrop-filter: blur(14px); +} + +.header-logo { + display: flex; + align-items: center; + gap: 12px; + min-width: 0; +} + +.logo-icon { + display: grid; + place-items: center; + width: 36px; + height: 36px; + border-radius: 10px; + background: linear-gradient(135deg, #38bdf8, #818cf8); + color: #041018; + font-weight: 900; +} + +.logo-text { + display: flex; + flex-direction: column; + min-width: 0; +} + +.logo-title { + font-size: 15px; + font-weight: 800; + line-height: 1.1; +} + +.logo-subtitle { + color: var(--muted); + font-size: 12px; +} + +.header-nav { + display: flex; + flex-wrap: wrap; + gap: 8px; + min-width: 0; +} + +.nav-link { + min-height: var(--tap); + padding: 0 14px; + border: 1px solid transparent; + border-radius: 999px; + background: transparent; + color: var(--text); + font-size: 14px; + font-weight: 700; +} + +.nav-link:hover, +.nav-reset:hover { + border-color: var(--line); + background: rgba(56, 189, 248, 0.08); + color: var(--cyan); +} + +.header-status-compact { display: inline-flex; align-items: center; gap: 8px; - min-height: 38px; - padding: 8px 12px; - border: 1px solid #284762; + min-height: 36px; + padding: 0 12px; + border: 1px solid var(--line); border-radius: 999px; background: #091522; color: #c8d7e8; - font-size: 12px; - font-weight: 800; + font-size: 13px; + font-weight: 700; } -.status-pill span { +.status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green); - box-shadow: 0 0 18px var(--green); + box-shadow: 0 0 10px var(--green); } -.status-paused span { background: var(--orange); box-shadow: 0 0 18px var(--orange); } -.status-fault span, -.status-emergency_stop span { background: var(--red); box-shadow: 0 0 18px var(--red); } +.status-dot.paused { background: var(--orange); box-shadow: 0 0 10px var(--orange); } +.status-dot.fault, +.status-dot.emergency_stop { background: var(--red); box-shadow: 0 0 10px var(--red); } -.header-actions { - display: flex; - gap: 8px; -} +/* ---------- Hero ---------- */ -.header-actions button { - min-height: 38px; - padding: 0 14px; -} - -.dashboard-grid { +.hero-section { display: grid; - grid-template-columns: minmax(760px, 1fr) 390px; + grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr); + gap: clamp(24px, 4vw, 48px); + align-items: center; + padding: clamp(36px, 6vw, 72px) 0 clamp(40px, 6vw, 64px); +} + +.hero-content, +.hero-visual { + min-width: 0; +} + +.hero-eyebrow { + margin-bottom: 14px; +} + +.hero-title { + margin-bottom: 18px; + font-size: clamp(2rem, 4.4vw, 3.4rem); + line-height: 1.08; + letter-spacing: -0.03em; + font-weight: 900; +} + +.hero-subtitle { + max-width: 38rem; + margin-bottom: 24px; + color: var(--muted); + font-size: clamp(16px, 1.5vw, 18px); + line-height: 1.55; +} + +.hero-badges { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 28px; +} + +.hero-badge { + padding: 8px 12px; + border: 1px solid rgba(56, 189, 248, 0.24); + border-radius: 999px; + background: rgba(56, 189, 248, 0.1); + color: #bfe9ff; + font-size: 13px; + font-weight: 700; +} + +.hero-actions { + display: flex; + flex-wrap: wrap; + gap: 12px; +} + +.hero-chain { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 10px; + padding: clamp(18px, 3vw, 28px); + border: 1px solid var(--line); + border-radius: 24px; + background: + linear-gradient(160deg, rgba(56, 189, 248, 0.1), rgba(12, 23, 37, 0.92) 40%), + var(--panel); + box-shadow: 0 24px 60px rgba(0, 0, 0, 0.28); +} + +.chain-step { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + min-width: 0; + text-align: center; +} + +.chain-icon { + display: grid; + place-items: center; + width: 48px; + height: 48px; + border: 1px solid rgba(56, 189, 248, 0.35); + border-radius: 14px; + background: #0d1b2a; + color: var(--cyan); + font-size: 16px; + font-weight: 900; +} + +.chain-step span { + color: #c8d7e8; + font-size: 13px; + font-weight: 700; +} + +.chain-arrow { + display: none; +} + +.hero-chain-caption { + margin-top: 14px; + color: var(--muted); + font-size: 14px; + text-align: center; +} + +/* ---------- Shared sections ---------- */ + +.section-header { + margin-bottom: clamp(20px, 3vw, 32px); +} + +.section-header h2, +.demo-header h2, +.engineering-toggle h2 { + margin: 8px 0 10px; + font-size: clamp(1.5rem, 2.4vw, 2rem); + font-weight: 800; + letter-spacing: -0.02em; +} + +.section-header p, +.demo-lead, +.engineering-lead { + max-width: 42rem; + color: var(--muted); + font-size: 16px; + line-height: 1.5; +} + +/* ---------- Product demo ---------- */ + +.product-demo-section, +.storyline-section, +.scenario-cards-section, +.criteria-cards-section, +.engineering-details-section { + padding: clamp(36px, 5vw, 64px) 0; + border-top: 1px solid rgba(30, 58, 84, 0.8); +} + +.demo-header { + display: flex; + flex-wrap: wrap; + align-items: flex-end; + justify-content: space-between; + gap: 16px; + margin-bottom: 24px; +} + +.demo-status { + display: inline-flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + min-height: 44px; + padding: 10px 14px; + border: 1px solid var(--line); + border-radius: 999px; + background: var(--panel); + color: var(--muted); + font-size: 14px; +} + +.demo-status strong { + color: var(--cyan); +} + +.demo-layout { + display: grid; + grid-template-columns: minmax(0, 1.35fr) minmax(0, 0.9fr); + gap: 20px; + align-items: start; +} + +.demo-scene-column, +.demo-result-column, +.demo-scene-container, +.demo-controls, +.demo-result-summary { + min-width: 0; +} + +.demo-mobile-hint { + display: none; + margin-bottom: 12px; + color: var(--muted); + font-size: 15px; +} + +.demo-scene-container { + padding: clamp(12px, 2vw, 18px); + border: 1px solid var(--line); + border-radius: 20px; + background: var(--panel); + box-shadow: 0 18px 50px rgba(0, 0, 0, 0.22); +} + +.demo-result-column { + display: flex; + flex-direction: column; + gap: 14px; +} + +.demo-result-summary { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + +.summary-item { + display: flex; + flex-direction: column; + gap: 4px; + min-width: 0; + padding: 12px; + border: 1px solid #1a2f44; + border-radius: 12px; + background: #0d1b2a; +} + +.summary-item span { + color: var(--muted); + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.06em; +} + +.summary-item strong { + font-size: 15px; + word-break: break-word; +} + +.demo-controls { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + +/* ---------- Proof card ---------- */ + +.proof-card { + display: flex; + flex-direction: column; + gap: 16px; + padding: 20px; + border: 1px solid var(--line); + border-radius: 20px; + background: var(--panel); +} + +.proof-card-stopped { + border-color: rgba(251, 61, 78, 0.55); + box-shadow: 0 0 24px rgba(251, 61, 78, 0.12); +} + +.proof-card-empty { + min-height: 220px; + align-items: center; + justify-content: center; + text-align: center; +} + +.proof-card-empty h3 { + margin: 8px 0; + font-size: 1.25rem; +} + +.proof-card-empty p, +.proof-reason p { + color: var(--muted); +} + +.proof-card-header { + padding-bottom: 12px; + border-bottom: 1px solid var(--line); +} + +.proof-card-header h3 { + margin-top: 4px; + font-size: 1.25rem; +} + +.proof-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + +.proof-item { + display: flex; + flex-direction: column; + gap: 4px; + min-width: 0; + padding: 12px; + border: 1px solid #1a2f44; + border-radius: 12px; + background: #0d1b2a; +} + +.proof-item-wide { + grid-column: 1 / -1; +} + +.proof-label { + color: var(--muted); + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.06em; +} + +.proof-item strong { + font-size: 16px; + word-break: break-word; +} + +.proof-details { + display: flex; + flex-direction: column; + gap: 8px; + padding: 12px; + border: 1px solid #1a2f44; + border-radius: 12px; + background: #0d1b2a; + font-size: 14px; +} + +.detail-row { + display: flex; + justify-content: space-between; + gap: 12px; +} + +.detail-row span { + color: var(--muted); +} + +.proof-reason { + padding: 14px 16px; + border-left: 4px solid var(--cyan); + border-radius: 0 12px 12px 0; + background: rgba(56, 189, 248, 0.06); +} + +.proof-reason strong { + display: block; + margin-bottom: 6px; +} + +/* ---------- Storyline ---------- */ + +.storyline-stepper { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 12px; + padding: 0; + list-style: none; +} + +.story-step { + display: flex; + flex-direction: column; + gap: 12px; + min-width: 0; + padding: 16px; + border: 1px solid var(--line); + border-radius: 16px; + background: var(--panel); +} + +.step-indicator { + display: grid; + place-items: center; + width: 36px; + height: 36px; + border: 2px solid #2a4a66; + border-radius: 50%; + background: #050910; +} + +.step-number { + font-size: 14px; + font-weight: 800; +} + +.step-content { + display: flex; + flex-direction: column; + gap: 4px; + min-width: 0; +} + +.step-content strong { + font-size: 15px; +} + +.step-content span { + color: var(--muted); + font-size: 13px; + line-height: 1.4; +} + +.step-status-label { + margin-top: 4px; + font-size: 12px !important; + font-weight: 800; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.step-active { + border-color: rgba(56, 189, 248, 0.7); + background: rgba(56, 189, 248, 0.08); + box-shadow: 0 0 0 1px rgba(56, 189, 248, 0.18); +} + +.step-active .step-indicator { + border-color: var(--cyan); + color: var(--cyan); +} + +.step-active .step-status-label { + color: var(--cyan) !important; +} + +.step-done { + border-color: rgba(74, 222, 128, 0.45); +} + +.step-done .step-indicator { + border-color: var(--green); + background: rgba(74, 222, 128, 0.16); + color: var(--green); +} + +.step-done .step-status-label { + color: var(--green) !important; +} + +.step-pending { + opacity: 0.72; +} + +/* ---------- Scenario cards ---------- */ + +.scenario-grid, +.criteria-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr)); + gap: 16px; +} + +.scenario-card, +.criteria-card { + display: flex; + flex-direction: column; + gap: 12px; + min-width: 0; + padding: 18px; + border: 1px solid var(--line); + border-radius: 16px; + background: var(--panel); +} + +.scenario-card:hover, +.criteria-card:hover { + border-color: rgba(56, 189, 248, 0.45); + transform: translateY(-2px); + transition: border-color 160ms ease, transform 160ms ease; +} + +.scenario-card.active { + border-color: var(--cyan); + background: rgba(56, 189, 248, 0.06); + box-shadow: 0 0 0 1px rgba(56, 189, 248, 0.25); +} + +.scenario-card-header, +.criteria-card-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 10px; +} + +.scenario-card-header h3, +.criteria-card-header h3 { + font-size: 1.05rem; + line-height: 1.25; +} + +.scenario-desc, +.criteria-evidence { + color: var(--muted); + font-size: 15px; + line-height: 1.45; + flex: 1; +} + +.scenario-expected { + display: flex; + flex-direction: column; + gap: 4px; + padding: 12px; + border-radius: 12px; + background: #0d1b2a; + border: 1px solid #1a2f44; + font-size: 14px; +} + +.scenario-expected span { + color: #7f93a8; + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.result-badge, +.status-badge { + display: inline-flex; + align-items: center; + min-height: 28px; + padding: 4px 10px; + border-radius: 999px; + font-size: 12px; + font-weight: 800; + white-space: nowrap; +} + +.tone-b { background: rgba(74, 222, 128, 0.12); color: var(--green); } +.tone-c { background: rgba(245, 158, 11, 0.12); color: var(--orange); } +.tone-d { background: rgba(192, 132, 252, 0.12); color: var(--purple); } +.tone-mixed { background: rgba(56, 189, 248, 0.12); color: var(--cyan); } +.tone-fault { background: rgba(251, 61, 78, 0.12); color: #fda4af; } + +.status-covered, +.status-badge.status-covered { + background: rgba(74, 222, 128, 0.12); + color: var(--green); +} + +.status-demo, +.status-badge.status-demo { + background: rgba(192, 132, 252, 0.12); + color: var(--purple); +} + +.criteria-footer { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + padding-top: 12px; + border-top: 1px solid var(--line); + color: var(--muted); + font-size: 14px; +} + +.criteria-link { + min-height: 36px; + padding: 0 10px; + border: 1px solid rgba(56, 189, 248, 0.28); + border-radius: 999px; + background: rgba(56, 189, 248, 0.08); + color: var(--cyan); + font-size: 13px; + font-weight: 700; +} + +/* ---------- Engineering details ---------- */ + +.engineering-toggle { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: clamp(18px, 3vw, 24px); + border: 1px solid var(--line); + border-radius: 18px; + background: var(--panel); +} + +.engineering-content { + margin-top: 20px; +} + +.engineering-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr)); gap: 16px; margin-top: 16px; } -.panel { +.eng-panel { + min-width: 0; + padding: 14px; border: 1px solid var(--line); - border-radius: 20px; + border-radius: 16px; background: var(--panel); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04), 0 18px 60px rgba(0, 0, 0, 0.2); + overflow: hidden; } -.panel-scene { - min-height: 672px; - padding: 16px; +.eng-panel-wide { + grid-column: 1 / -1; +} + +.eng-panel .panel { + border: none; + background: transparent; + box-shadow: none; + padding: 0; +} + +/* ---------- Scene ---------- */ + +.scene-wrap { + width: 100%; + min-width: 0; } .scene-title-row, +.scene-simple-header, .panel-heading { display: flex; align-items: flex-start; @@ -163,13 +921,40 @@ h2 { margin-bottom: 12px; } +.scene-simple-header { + align-items: center; +} + .sorter-svg { + display: block; width: 100%; - height: 588px; + max-width: 100%; + height: auto; border-radius: 16px; background: #060d16; } +.machine-state-chip, +.status-pill, +.scenario-label { + display: inline-flex; + align-items: center; + gap: 8px; + min-height: 34px; + padding: 6px 12px; + border: 1px solid #284762; + border-radius: 999px; + background: #091522; + color: #c8d7e8; + font-size: 12px; + font-weight: 800; +} + +.fault-chip { + border-color: var(--red); + color: #fecdd3; +} + .zone rect { fill: rgba(14, 27, 43, 0.82); stroke-width: 2; @@ -187,11 +972,26 @@ h2 { text-anchor: middle; } +.zone-label-large { + fill: #f4fbff !important; + font-size: 34px !important; + font-weight: 900 !important; +} + .zone-a rect { stroke: #38bdf8; } .zone-b rect { stroke: #4ade80; } .zone-c rect { stroke: #f59e0b; } .zone-d rect { stroke: #c084fc; } +.zone-active rect { + fill: rgba(56, 189, 248, 0.08); + filter: drop-shadow(0 0 10px rgba(56, 189, 248, 0.25)); +} + +.zone-b.zone-active rect { fill: rgba(74, 222, 128, 0.12); } +.zone-c.zone-active rect { fill: rgba(245, 158, 11, 0.12); } +.zone-d.zone-active rect { fill: rgba(192, 132, 252, 0.12); } + .device rect, .device circle { fill: #0f2031; @@ -220,8 +1020,15 @@ h2 { stroke-linecap: round; } -.gate.open line { stroke: var(--green); transform: rotate(22deg); transform-origin: 680px 315px; } -.gate.closed line { stroke: var(--red); } +.gate.open line { + stroke: var(--green); + transform: rotate(22deg); + transform-origin: 680px 315px; +} + +.gate.closed line { + stroke: var(--red); +} .pusher rect { fill: #101d2d; @@ -234,310 +1041,6 @@ h2 { filter: drop-shadow(0 0 12px rgba(245, 158, 11, 0.55)); } -.right-rail { - display: grid; - gap: 16px; -} - -.compact-panel, -.metrics-panel, -.pid-panel, -.event-panel, -.scenario-panel { - padding: 14px; -} - -.kv-grid { - display: grid; - grid-template-columns: 120px 1fr; - gap: 9px 12px; - font-size: 13px; -} - -.kv-grid span, -.muted { - color: var(--muted); -} - -.kv-grid strong { - font-size: 13px; -} - -.category-B { color: var(--green); } -.category-C { color: var(--orange); } -.category-D { color: var(--purple); } - -.decision-tree { - display: grid; - gap: 8px; - padding-left: 20px; - color: #b8c8da; - font-size: 13px; -} - -.decision-tree li { - padding: 8px 10px; - border: 1px solid #243e57; - border-radius: 10px; - background: #0b1725; -} - -.decision-tree .pass { border-color: rgba(74, 222, 128, 0.65); } -.decision-tree .fail { border-color: rgba(245, 158, 11, 0.7); } - -.state-list { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 7px; -} - -.state { - display: flex; - align-items: center; - gap: 7px; - padding: 7px 8px; - border: 1px solid #243e57; - border-radius: 9px; - color: #95a7bc; - font-size: 11px; - font-weight: 800; -} - -.state span { - width: 7px; - height: 7px; - border-radius: 50%; - background: #33485c; -} - -.state.active { - color: #f4fbff; - border-color: var(--cyan); - background: rgba(56, 189, 248, 0.11); -} - -.state.active span { - background: var(--cyan); - box-shadow: 0 0 14px var(--cyan); -} - -.sensor-stack { - display: grid; - gap: 8px; -} - -.sensor-row { - display: flex; - justify-content: space-between; - gap: 12px; - padding: 10px; - border: 1px solid #243e57; - border-radius: 12px; - background: #0b1725; -} - -.sensor-row.active { - border-color: var(--cyan); - box-shadow: inset 0 0 0 1px rgba(56, 189, 248, 0.2); -} - -.sensor-row strong, -.sensor-row span, -.sensor-meta span { - display: block; - font-size: 12px; -} - -.sensor-row span, -.sensor-meta span { - color: var(--muted); -} - -.sensor-meta { - text-align: right; -} - -.actuator-grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 8px; - margin-top: 10px; -} - -.actuator-grid div, -.pid-bars div { - padding: 9px; - border: 1px solid #243e57; - border-radius: 12px; - background: #081422; -} - -.actuator-grid span, -.pid-bars span { - display: block; - color: var(--muted); - font-size: 11px; -} - -.actuator-grid strong, -.pid-bars strong { - font-size: 13px; -} - -.bottom-row { - display: grid; - grid-column: 1 / -1; - grid-template-columns: 1.1fr 0.75fr 1.3fr 1fr; - gap: 16px; -} - -.metric-grid { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 8px; -} - -.metric-card { - min-height: 78px; - padding: 10px; - border: 1px solid #243e57; - border-radius: 14px; - background: linear-gradient(180deg, #0b1725, #07111d); -} - -.metric-card span, -.metric-card em { - display: block; - color: var(--muted); - font-size: 11px; - font-style: normal; -} - -.metric-card strong { - display: block; - margin: 5px 0; - font-size: 22px; - letter-spacing: -0.04em; -} - -.pid-bars { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 8px; -} - -.speed-track { - height: 10px; - margin: 12px 0; - overflow: hidden; - border-radius: 999px; - background: #102034; -} - -.speed-track div { - height: 100%; - background: linear-gradient(90deg, var(--cyan), var(--green)); -} - -.event-table { - overflow: auto; - max-height: 290px; - border: 1px solid #243e57; - border-radius: 14px; -} - -.event-row { - display: grid; - grid-template-columns: 68px 92px 94px minmax(240px, 1fr) 72px 72px; - gap: 8px; - padding: 8px 10px; - border-bottom: 1px solid #172d42; - color: #c8d7e8; - font-size: 12px; -} - -.event-head { - position: sticky; - top: 0; - z-index: 1; - background: #0d1b2b; - color: var(--muted); - font-weight: 800; - text-transform: uppercase; -} - -.event-row.warning { color: #facc15; } -.event-row.error { color: #fb7185; } -.event-row.success { color: #86efac; } - -.empty-log { - padding: 16px; - color: var(--muted); -} - -.scenario-buttons { - display: grid; - gap: 8px; - max-height: 290px; - overflow: auto; -} - -.scenario-buttons button { - padding: 10px; - text-align: left; -} - -.scenario-buttons button strong, -.scenario-buttons button span { - display: block; -} - -.scenario-buttons button span { - margin-top: 4px; - color: var(--muted); - font-size: 12px; - font-weight: 500; -} - -.scenario-buttons button.active { - border-color: var(--cyan); - background: rgba(56, 189, 248, 0.12); -} - -@media (max-width: 1280px) { - body { - min-width: 0; - } - - .dashboard-grid, - .bottom-row { - grid-template-columns: 1fr; - } - - .right-rail { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } -} - - -button:disabled { - cursor: not-allowed; - opacity: 0.45; - transform: none; -} - -.sorter-svg { - height: 660px; -} - -.panel-scene { - min-height: 746px; -} - -.left-label { - text-anchor: start; -} - .conveyor { fill: #101f30; stroke: #4b647d; @@ -597,6 +1100,11 @@ button:disabled { .route-c { stroke: var(--orange); } .route-d { stroke: var(--purple); } +.route-active { + opacity: 1; + stroke-width: 4; +} + .route-command text { fill: #05101b; font-size: 13px; @@ -617,11 +1125,6 @@ button:disabled { font-weight: 800; } -.fault-chip { - border-color: var(--red); - color: #fecdd3; -} - .fault-overlay rect { fill: rgba(54, 12, 22, 0.88); stroke: var(--red); @@ -635,6 +1138,452 @@ button:disabled { text-anchor: middle; } +.left-label { + text-anchor: start; +} + +/* ---------- Engineering panels ---------- */ + +.panel { + border: 1px solid var(--line); + border-radius: 20px; + background: var(--panel); +} + +.compact-panel, +.metrics-panel, +.pid-panel, +.event-panel, +.scenario-panel, +.timeline-panel, +.criteria-panel { + padding: 4px; +} + +.kv-grid { + display: grid; + grid-template-columns: minmax(0, 120px) minmax(0, 1fr); + gap: 9px 12px; + font-size: 13px; +} + +.kv-grid span, +.muted { + color: var(--muted); +} + +.state-list { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 7px; +} + +.state { + display: flex; + align-items: center; + gap: 7px; + min-width: 0; + padding: 7px 8px; + border: 1px solid #243e57; + border-radius: 9px; + color: #95a7bc; + font-size: 11px; + font-weight: 800; +} + +.state span { + width: 7px; + height: 7px; + border-radius: 50%; + background: #33485c; + flex-shrink: 0; +} + +.state.active { + color: #f4fbff; + border-color: var(--cyan); + background: rgba(56, 189, 248, 0.11); +} + +.state.active span { + background: var(--cyan); + box-shadow: 0 0 14px var(--cyan); +} + +.sensor-stack { + display: grid; + gap: 8px; +} + +.sensor-row { + display: flex; + justify-content: space-between; + gap: 12px; + padding: 10px; + border: 1px solid #243e57; + border-radius: 12px; + background: #0b1725; +} + +.sensor-row.active { + border-color: var(--cyan); + box-shadow: inset 0 0 0 1px rgba(56, 189, 248, 0.2); +} + +.sensor-row strong, +.sensor-row span, +.sensor-meta span { + display: block; + font-size: 12px; +} + +.sensor-row span, +.sensor-meta span { + color: var(--muted); +} + +.sensor-meta { + text-align: right; +} + +.actuator-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; + margin-top: 10px; +} + +.actuator-grid div, +.pid-bars div { + padding: 9px; + border: 1px solid #243e57; + border-radius: 12px; + background: #081422; +} + +.actuator-grid span, +.pid-bars span { + display: block; + color: var(--muted); + font-size: 11px; +} + +.actuator-grid strong, +.pid-bars strong { + font-size: 13px; +} + +.metric-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; +} + +.metric-card { + min-height: 78px; + min-width: 0; + padding: 10px; + border: 1px solid #243e57; + border-radius: 14px; + background: linear-gradient(180deg, #0b1725, #07111d); +} + +.metric-card span, +.metric-card em { + display: block; + color: var(--muted); + font-size: 11px; + font-style: normal; +} + +.metric-card strong { + display: block; + margin: 5px 0; + font-size: 20px; + letter-spacing: -0.04em; +} + +.pid-bars { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; +} + +.speed-track { + height: 10px; + margin: 12px 0; + overflow: hidden; + border-radius: 999px; + background: #102034; +} + +.speed-track div { + height: 100%; + background: linear-gradient(90deg, var(--cyan), var(--green)); +} + +.pid-sparkline { + width: 100%; + height: 72px; + margin: 4px 0 10px; + border: 1px solid #243e57; + border-radius: 12px; + background: #071423; +} + +.pid-sparkline path { + stroke: #223c55; + stroke-width: 1; +} + +.pid-sparkline polyline { + fill: none; + stroke: var(--cyan); + stroke-width: 3; + stroke-linejoin: round; + stroke-linecap: round; +} + +.timeline-summary { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(90px, 1fr)); + gap: 6px; + margin-bottom: 10px; +} + +.timeline-summary div { + padding: 8px; + border: 1px solid #243e57; + border-radius: 10px; + background: #081422; +} + +.timeline-summary span, +.timeline-summary strong { + display: block; + font-size: 11px; +} + +.timeline-summary span { + color: var(--muted); +} + +.timeline-list { + display: grid; + gap: 6px; + max-height: 260px; + overflow: auto; +} + +.timeline-row { + display: grid; + grid-template-columns: 14px minmax(0, 1fr) 60px 62px 58px; + align-items: center; + gap: 7px; + padding: 7px 8px; + border: 1px solid #203950; + border-radius: 10px; + color: #b8c8da; + font-size: 11px; +} + +.timeline-row em { + color: var(--muted); + font-style: normal; + font-weight: 800; +} + +.timeline-dot { + width: 9px; + height: 9px; + border-radius: 50%; + background: #31485f; +} + +.timeline-row.done .timeline-dot { background: var(--green); } +.timeline-row.active { + border-color: var(--cyan); + background: rgba(56, 189, 248, 0.09); +} +.timeline-row.active .timeline-dot { + background: var(--cyan); + box-shadow: 0 0 12px var(--cyan); +} +.timeline-row.skipped { opacity: 0.48; } + +.event-table { + overflow: auto; + max-height: 290px; + max-width: 100%; + border: 1px solid #243e57; + border-radius: 14px; +} + +.event-row { + display: grid; + grid-template-columns: 62px 84px 116px 88px 128px minmax(160px, 1fr) 68px; + gap: 8px; + min-width: 720px; + padding: 8px 10px; + border-bottom: 1px solid #172d42; + color: #c8d7e8; + font-size: 12px; +} + +.event-head { + position: sticky; + top: 0; + z-index: 1; + background: #0d1b2b; + color: var(--muted); + font-weight: 800; + text-transform: uppercase; +} + +.event-row.warning { color: #facc15; } +.event-row.error { color: #fb7185; } +.event-row.success { color: #86efac; } + +.empty-log { + padding: 16px; + color: var(--muted); +} + +.log-filters { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-bottom: 10px; +} + +.log-filters button, +.small-button { + min-height: 32px; + padding: 4px 8px; + font-size: 11px; +} + +.log-filters button.active { + border-color: var(--cyan); + background: rgba(56, 189, 248, 0.12); +} + +.scenario-buttons { + display: grid; + gap: 8px; + max-height: 320px; + overflow: auto; +} + +.scenario-buttons button { + padding: 10px; + text-align: left; +} + +.scenario-buttons button strong, +.scenario-buttons button span, +.scenario-buttons button em { + display: block; +} + +.scenario-buttons button span, +.scenario-buttons button em { + margin-top: 4px; + color: var(--muted); + font-size: 12px; + font-weight: 500; + font-style: normal; +} + +.scenario-buttons button.active { + border-color: var(--cyan); + background: rgba(56, 189, 248, 0.12); +} + +.criteria-filters { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-bottom: 10px; +} + +.criteria-filters button { + min-height: 32px; + padding: 4px 8px; + font-size: 11px; +} + +.criteria-filters button.active { + border-color: var(--cyan); + background: rgba(56, 189, 248, 0.12); +} + +.criteria-list { + display: grid; + gap: 8px; + max-height: 320px; + overflow: auto; +} + +.criterion-card { + padding: 10px; + border: 1px solid #243e57; + border-radius: 13px; + background: #081422; +} + +.criterion-card.related { + border-color: var(--green); + background: rgba(74, 222, 128, 0.07); +} + +.criterion-card div { + display: flex; + justify-content: space-between; + gap: 8px; + margin-bottom: 6px; +} + +.criterion-card strong, +.criterion-card p, +.criterion-card em { + display: block; + font-size: 12px; +} + +.criterion-card p { + color: #c1d1e3; + line-height: 1.35; +} + +.criterion-card em { + margin-top: 6px; + color: var(--muted); + font-style: normal; +} + +.criterion-status { + white-space: nowrap; + color: var(--cyan); + font-size: 10px; + font-weight: 900; + text-transform: uppercase; +} + +.criteria-count { + padding: 5px 9px; + border: 1px solid #284762; + border-radius: 999px; + background: #071423; + color: #b9cce0; + font-size: 11px; + font-weight: 750; +} + +.status-covered { color: var(--green); } +.status-partially-covered { color: var(--orange); } +.status-demo-step-available { color: var(--purple); } + .pseudo-cv-block { margin-top: 12px; padding: 10px; @@ -652,11 +1601,7 @@ button:disabled { } .compact-kv { - grid-template-columns: 116px 1fr; -} - -.classification-panel .panel-heading { - margin-bottom: 10px; + grid-template-columns: minmax(0, 116px) minmax(0, 1fr); } .rule-stack { @@ -706,1459 +1651,151 @@ button:disabled { .verdict.fail { color: var(--orange); border-color: var(--orange); } .verdict.pending, .verdict.skipped { color: var(--muted); } -.warning-note { - margin-top: 6px; - color: #facc15 !important; -} -.bottom-row { - grid-template-columns: 1fr 1.15fr 0.82fr 1.35fr 1fr; - align-items: start; -} - -.timeline-panel { - padding: 14px; -} - -.timeline-summary { +.decision-tree { display: grid; - grid-template-columns: repeat(5, minmax(0, 1fr)); - gap: 6px; - margin-bottom: 10px; -} - -.timeline-summary div { - padding: 8px; - border: 1px solid #243e57; - border-radius: 10px; - background: #081422; -} - -.timeline-summary span, -.timeline-summary strong { - display: block; - font-size: 11px; -} - -.timeline-summary span { - color: var(--muted); -} - -.timeline-list { - display: grid; - gap: 6px; - max-height: 260px; - overflow: auto; -} - -.timeline-row { - display: grid; - grid-template-columns: 14px minmax(130px, 1fr) 60px 62px 58px; - align-items: center; - gap: 7px; - padding: 7px 8px; - border: 1px solid #203950; - border-radius: 10px; + gap: 8px; + padding-left: 20px; color: #b8c8da; - font-size: 11px; + font-size: 13px; } -.timeline-row strong { - font-size: 11px; -} - -.timeline-row em { - color: var(--muted); - font-style: normal; - font-weight: 800; -} - -.timeline-dot { - width: 9px; - height: 9px; - border-radius: 50%; - background: #31485f; -} - -.timeline-row.done .timeline-dot { background: var(--green); } -.timeline-row.active { - border-color: var(--cyan); - background: rgba(56, 189, 248, 0.09); -} -.timeline-row.active .timeline-dot { background: var(--cyan); box-shadow: 0 0 12px var(--cyan); } -.timeline-row.skipped { opacity: 0.48; } -.timeline-row.skipped .timeline-dot { background: #475569; } - -.pid-sparkline { - width: 100%; - height: 72px; - margin: 4px 0 10px; +.decision-tree li { + padding: 8px 10px; border: 1px solid #243e57; - border-radius: 12px; - background: #071423; -} - -.pid-sparkline path { - stroke: #223c55; - stroke-width: 1; -} - -.pid-sparkline polyline { - fill: none; - stroke: var(--cyan); - stroke-width: 3; - stroke-linejoin: round; - stroke-linecap: round; -} - -.log-filters { - display: flex; - flex-wrap: wrap; - gap: 6px; - margin-bottom: 10px; -} - -.log-filters button, -.small-button { - min-height: 28px; - padding: 4px 8px; - font-size: 11px; -} - -.log-filters button.active { - border-color: var(--cyan); - background: rgba(56, 189, 248, 0.12); -} - -.event-row { - grid-template-columns: 62px 84px 116px 88px 128px minmax(260px, 1fr) 68px; -} - -.scenario-buttons button em { - display: block; - margin-top: 5px; - color: #b6c6d8; - font-size: 11px; - font-style: normal; - font-weight: 650; -} - -.metric-grid { - grid-template-columns: repeat(3, minmax(0, 1fr)); -} - -@media (max-width: 1500px) { - .bottom-row { - grid-template-columns: 1fr 1fr; - } -} - - -.mode-toggle { - display: inline-flex; - gap: 4px; - padding: 4px; - border: 1px solid #284762; - border-radius: 999px; - background: #071423; -} - -.mode-toggle button, -.safe-demo-toggle { - min-height: 30px; - padding: 4px 10px; - font-size: 11px; -} - -.mode-toggle button.active, -.safe-demo-toggle.active, -.presentation-actions button.active { - border-color: var(--cyan); - background: rgba(56, 189, 248, 0.14); - color: #e8f7ff; -} - -.mode-presentation .dashboard-grid { - margin-top: 14px; -} - -.presentation-panel { - display: grid; - grid-template-columns: 90px minmax(260px, 0.9fr) minmax(420px, 1.4fr); - gap: 14px; - margin-top: 16px; - padding: 16px; - border-color: rgba(56, 189, 248, 0.55); - background: - linear-gradient(135deg, rgba(56, 189, 248, 0.1), rgba(12, 23, 37, 0.92) 32%), - var(--panel); -} - -.presentation-step-index { - display: grid; - place-items: center; - min-height: 92px; - border: 1px solid #2d5572; - border-radius: 16px; - color: var(--cyan); - font-size: 15px; - font-weight: 950; - text-align: center; -} - -.presentation-copy h2 { - margin: 4px 0 8px; - font-size: 26px; -} - -.presentation-copy p, -.presentation-proof span, -.presentation-meta span, -.presentation-panel blockquote { - color: #c6d7e8; - font-size: 13px; - line-height: 1.45; -} - -.presentation-proof { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 8px; -} - -.presentation-proof div { - padding: 10px; - border: 1px solid #284762; - border-radius: 13px; - background: #081422; -} - -.presentation-proof strong, -.presentation-proof span { - display: block; -} - -.presentation-proof strong { - margin-bottom: 5px; - color: #f1f8ff; - font-size: 12px; -} - -.presentation-panel blockquote { - grid-column: 2 / -1; - margin: 0; - padding: 10px 12px; - border-left: 3px solid var(--cyan); border-radius: 10px; - background: rgba(56, 189, 248, 0.08); - font-weight: 750; + background: #0b1725; } -.presentation-meta, -.criteria-tags, -.presentation-actions { - grid-column: 1 / -1; - display: flex; - flex-wrap: wrap; - gap: 8px; - align-items: center; -} - -.presentation-meta span, -.criteria-tags span, -.criteria-count { - padding: 5px 9px; - border: 1px solid #284762; - border-radius: 999px; - background: #071423; - color: #b9cce0; - font-size: 11px; - font-weight: 750; -} - -.criteria-tags span { - border-color: rgba(74, 222, 128, 0.38); - color: #bbf7d0; -} - -.confirm-note { - grid-column: 1 / -1; - padding: 10px 12px; - border: 1px solid rgba(245, 158, 11, 0.65); - border-radius: 12px; - background: rgba(245, 158, 11, 0.1); - color: #fde68a; - font-size: 13px; - font-weight: 800; -} - -.presentation-actions button { - min-height: 34px; - padding: 6px 12px; - font-size: 12px; -} - -.focus-highlight > .panel, -.focus-highlight.panel { - border-color: var(--cyan); - box-shadow: 0 0 0 1px rgba(56, 189, 248, 0.3), 0 0 30px rgba(56, 189, 248, 0.12); -} - -.focus-highlight > .panel::before, -.focus-highlight.panel::before { - content: "Current demo focus"; - display: inline-flex; - margin: -2px 0 8px; - padding: 4px 8px; - border-radius: 999px; - background: rgba(56, 189, 248, 0.16); - color: var(--cyan); - font-size: 10px; - font-weight: 950; - letter-spacing: 0.08em; - text-transform: uppercase; -} - -.mode-presentation .presentation-muted > .panel, -.mode-presentation .presentation-muted.panel { - opacity: 0.74; -} - -.criteria-panel { - padding: 14px; -} - -.criteria-filters { - display: flex; - flex-wrap: wrap; - gap: 6px; - margin-bottom: 10px; -} - -.criteria-filters button { - min-height: 28px; - padding: 4px 8px; - font-size: 11px; -} - -.criteria-filters button.active { - border-color: var(--cyan); - background: rgba(56, 189, 248, 0.12); -} - -.criteria-list { - display: grid; - gap: 8px; - max-height: 290px; - overflow: auto; -} - -.criterion-card { - padding: 10px; - border: 1px solid #243e57; - border-radius: 13px; - background: #081422; -} - -.criterion-card.related { - border-color: var(--green); - background: rgba(74, 222, 128, 0.07); -} - -.criterion-card div { - display: flex; - justify-content: space-between; - gap: 8px; - margin-bottom: 6px; -} - -.criterion-card strong, -.criterion-card p, -.criterion-card em { - display: block; - font-size: 12px; -} - -.criterion-card p { - color: #c1d1e3; - line-height: 1.35; -} - -.criterion-card em { - margin-top: 6px; - color: var(--muted); - font-style: normal; -} - -.criterion-status { - white-space: nowrap; - color: var(--cyan); - font-size: 10px; - font-weight: 900; - text-transform: uppercase; -} - -.status-covered { color: var(--green); } -.status-partially-covered { color: var(--orange); } -.status-demo-step-available { color: var(--purple); } - -.scenario-panel-wrap { - grid-column: auto; -} - -@media (max-width: 1500px) { - .presentation-panel { - grid-template-columns: 1fr; - } - - .presentation-panel blockquote, - .presentation-meta, - .criteria-tags, - .presentation-actions, - .confirm-note { - grid-column: 1; - } - - .presentation-proof { - grid-template-columns: 1fr; - } -} - -/* Guided Demo View */ -.guided-demo-layout { - display: flex; - flex-direction: column; - height: 100vh; - padding: 16px; - overflow: hidden; - background: var(--bg); -} - -.guided-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 14px 20px; - background: rgba(7, 14, 23, 0.95); - border: 1px solid var(--line); - border-radius: 16px; - margin-bottom: 16px; - flex-shrink: 0; -} - -.guided-header h1 { - font-size: 24px; -} - -.guided-header-center { - display: flex; - gap: 12px; -} - -.guided-header-right { - display: flex; - gap: 10px; - align-items: center; -} - -.primary-action { - background: var(--cyan); - color: #000; - border-color: var(--cyan); - font-size: 14px; - padding: 0 16px; -} - -.primary-action:hover { - background: #7dd3fc; - color: #000; -} - -.nav-actions { - display: flex; - gap: 6px; -} - -.guided-grid { - display: grid; - grid-template-columns: 1fr 420px; - gap: 16px; - flex-grow: 1; - min-height: 0; -} - -.guided-left { - display: flex; - flex-direction: column; - min-height: 0; -} - -.guided-scene-panel { - flex-grow: 1; - display: flex; - flex-direction: column; - padding: 16px; - min-height: 0; -} - -.guided-scene-panel .scene-wrap { - flex-grow: 1; - display: flex; - flex-direction: column; -} - -.guided-scene-panel svg { - flex-grow: 1; - height: auto; - max-height: 100%; -} - -.guided-metrics-strip { - display: flex; - justify-content: space-around; - gap: 12px; - margin-top: 16px; - padding: 12px; - background: rgba(0,0,0,0.2); - border-radius: 12px; - border: 1px solid var(--line); - flex-shrink: 0; -} - -.guided-metric { - text-align: center; -} - -.guided-metric span { - display: block; - font-size: 12px; - color: var(--muted); - margin-bottom: 4px; -} - -.guided-metric strong { - font-size: 18px; - color: var(--text); -} - -.guided-right { - display: flex; - flex-direction: column; - gap: 16px; - overflow-y: auto; - padding-right: 4px; -} - -/* Proof Card */ -.proof-card { - padding: 16px; - background: #081422; - border: 1px solid var(--cyan); - border-radius: 16px; - box-shadow: 0 0 20px rgba(56, 189, 248, 0.1); - flex-shrink: 0; -} - -.proof-card-stopped { - border-color: var(--red); - box-shadow: 0 0 20px rgba(251, 61, 78, 0.15); -} - -.proof-card-empty { - border-color: #243e57; - box-shadow: none; - text-align: center; - display: flex; - align-items: center; - justify-content: center; - min-height: 200px; -} - -.proof-card-empty h2 { - font-size: 20px; - margin: 12px 0; -} - -.proof-card-empty p { - color: var(--muted); - margin-bottom: 16px; -} - -.proof-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 12px; - margin-bottom: 16px; -} - -.proof-item { - background: #0d1b2a; - padding: 10px; - border-radius: 10px; - border: 1px solid #1a2f44; -} - -.proof-label { - display: block; - font-size: 11px; - color: var(--muted); - text-transform: uppercase; - letter-spacing: 0.05em; - margin-bottom: 4px; -} - -.proof-item strong { - font-size: 16px; -} - -.fail-text { color: var(--red); } -.warn-text { color: var(--orange); } - -.proof-reason { - background: #0d1b2a; - padding: 12px; - border-radius: 10px; - border: 1px solid #1a2f44; - font-size: 13px; - margin-bottom: 16px; -} - -.proof-reason .warning-note { margin-top: 8px; font-weight: bold; } -.proof-reason .error-note { margin-top: 8px; color: var(--red); font-weight: bold; } - -.evidence-chain { - display: flex; - align-items: center; - justify-content: space-between; - font-size: 11px; - font-weight: bold; - background: #050a10; - padding: 10px 14px; - border-radius: 8px; - border: 1px solid #1e3a54; -} - -.evidence-chain span { color: #475569; } -.evidence-chain .arrow { color: #334155; } -.evidence-chain .done-step { color: var(--green); } -.evidence-chain .active-step { color: var(--cyan); text-shadow: 0 0 8px rgba(56, 189, 248, 0.5); } - -/* Narration */ -.guided-narration { - padding: 16px; - flex-shrink: 0; -} - -.guided-narration .explanation { - font-size: 14px; - line-height: 1.5; - margin-bottom: 16px; -} - -.narration-details { - display: flex; - flex-direction: column; - gap: 8px; - margin-bottom: 16px; - font-size: 13px; - background: #0d1b2a; - padding: 12px; - border-radius: 10px; - border: 1px solid #1a2f44; -} - -.narration-details strong { color: var(--cyan); } - -.presenter-phrase { - margin: 0; - padding: 14px; - background: rgba(56, 189, 248, 0.1); - border-left: 4px solid var(--cyan); - border-radius: 0 10px 10px 0; - font-size: 15px; - font-weight: 600; - font-style: italic; - line-height: 1.5; -} - -.fault-confirm-btn { - margin-top: 12px; - width: 100%; - padding: 12px; - background: rgba(245, 158, 11, 0.1); - border: 1px solid var(--orange); - color: var(--orange); -} - -.fault-confirm-btn.pending { - background: var(--orange); - color: #000; -} - -/* Criteria Snapshot */ -.guided-criteria { - padding: 16px; - flex-shrink: 0; -} - -.criteria-list-mini { - display: flex; - flex-direction: column; - gap: 10px; -} - -.criterion-mini { - background: #0d1b2a; - border: 1px solid #1a2f44; - padding: 12px; - border-radius: 10px; -} - -.criterion-mini-header { - display: flex; - justify-content: space-between; - margin-bottom: 6px; -} - -.criterion-mini-header strong { font-size: 13px; } -.criterion-mini p { font-size: 12px; color: var(--muted); margin: 0; line-height: 1.4; } - - -/* ====== NEW PRODUCT DEMO STYLES ====== */ - -.product-page { - display: flex; - flex-direction: column; - min-height: 100vh; - width: 100%; -} - -.product-main { - flex: 1; - width: 100%; - max-width: 1440px; - margin: 0 auto; - padding: 0 20px; -} - -/* Header */ -.site-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 16px 24px; - background: rgba(7, 14, 23, 0.9); - border-bottom: 1px solid var(--line); - position: sticky; - top: 0; - z-index: 100; - backdrop-filter: blur(12px); -} - -.header-logo { - display: flex; - align-items: center; - gap: 12px; -} - -.logo-icon { - display: flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - background: var(--cyan); - color: #000; - font-weight: 900; - border-radius: 8px; -} - -.logo-text { - display: flex; - flex-direction: column; -} - -.logo-title { - font-weight: 800; - font-size: 16px; - line-height: 1; -} - -.logo-subtitle { - font-size: 11px; - color: var(--muted); -} - -.header-nav { - display: flex; - gap: 24px; -} - -.nav-link { - color: var(--text); - text-decoration: none; - font-size: 14px; - font-weight: 600; - transition: color 0.2s; -} - -.nav-link:hover { - color: var(--cyan); -} - -.header-status-compact { - display: flex; - align-items: center; - gap: 8px; - font-size: 13px; - font-weight: 600; -} - -.status-dot { - width: 8px; - height: 8px; - border-radius: 50%; - background: var(--green); - box-shadow: 0 0 10px var(--green); -} -.status-dot.paused { background: var(--orange); box-shadow: 0 0 10px var(--orange); } -.status-dot.fault, .status-dot.emergency_stop { background: var(--red); box-shadow: 0 0 10px var(--red); } - -/* Hero */ -.hero-section { - padding: 60px 0 80px; - display: flex; - gap: 40px; - align-items: center; -} - -.hero-content { - flex: 1; -} - -.hero-title { - font-size: clamp(36px, 4vw, 56px); - line-height: 1.1; - letter-spacing: -0.02em; - margin: 0 0 20px; - font-weight: 900; -} - -.hero-subtitle { - font-size: 18px; - color: var(--muted); - margin: 0 0 32px; - max-width: 600px; -} - -.hero-badges { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin-bottom: 40px; -} - -.hero-badge { - padding: 6px 12px; - background: rgba(56, 189, 248, 0.1); - color: var(--cyan); - border: 1px solid rgba(56, 189, 248, 0.2); - border-radius: 999px; - font-size: 12px; - font-weight: 600; -} - -.hero-actions { - display: flex; - gap: 16px; - flex-wrap: wrap; -} - -.btn-primary { - background: var(--cyan); - color: #000; - border: none; - padding: 14px 28px; - border-radius: 12px; - font-size: 16px; - font-weight: 700; - cursor: pointer; - transition: all 0.2s; - text-decoration: none; - display: inline-flex; - align-items: center; - justify-content: center; -} - -.btn-primary:hover { - background: var(--cyan-hover); - transform: translateY(-2px); -} - -.btn-secondary { - background: #0e1b2b; - color: var(--text); - border: 1px solid #2a4a66; - padding: 14px 28px; - border-radius: 12px; - font-size: 16px; - font-weight: 600; - cursor: pointer; - transition: all 0.2s; - text-decoration: none; - display: inline-flex; - align-items: center; - justify-content: center; -} - -.btn-secondary:hover { - border-color: var(--cyan); - background: #11273b; -} - -.hero-visual { - flex: 1; - display: flex; - justify-content: center; -} - -.hero-chain { - display: flex; - align-items: center; - gap: 16px; - background: var(--panel); - padding: 32px 40px; - border-radius: 24px; - border: 1px solid var(--line); - box-shadow: 0 20px 40px rgba(0,0,0,0.3); -} - -.chain-step { - display: flex; - flex-direction: column; - align-items: center; - gap: 12px; -} - -.chain-icon { - width: 48px; - height: 48px; - background: #0d1b2a; - border: 1px solid #1a2f44; - border-radius: 12px; - display: flex; - align-items: center; - justify-content: center; - font-size: 24px; -} - -.chain-step span { - font-size: 12px; - font-weight: 600; - color: var(--muted); -} - -.chain-arrow { - color: #334155; - font-size: 20px; -} - -/* Demo Section */ -.product-demo-section { - padding: 40px 0 80px; - border-top: 1px solid var(--line); -} - -.demo-header { - display: flex; - justify-content: space-between; - align-items: flex-end; - margin-bottom: 32px; -} - -.demo-header h2 { - font-size: 32px; - margin: 0; - font-weight: 800; -} - -.demo-status { - display: flex; - align-items: center; - gap: 8px; - font-size: 14px; - color: var(--muted); - background: var(--panel); - padding: 10px 16px; - border-radius: 999px; - border: 1px solid var(--line); -} - -.demo-status strong { - color: var(--cyan); -} - -.storyline-stepper { - display: flex; - justify-content: space-between; - margin-bottom: 40px; - background: var(--panel); - padding: 20px; - border-radius: 16px; - border: 1px solid var(--line); -} - -.story-step { - display: flex; - align-items: center; - gap: 12px; - flex: 1; -} - -.step-indicator { - width: 24px; - height: 24px; - border-radius: 50%; - border: 2px solid #2a4a66; - display: flex; - align-items: center; - justify-content: center; - background: #050910; -} - -.step-dot { - width: 8px; - height: 8px; - border-radius: 50%; - background: transparent; -} - -.step-active .step-indicator { border-color: var(--cyan); } -.step-active .step-dot { background: var(--cyan); box-shadow: 0 0 8px var(--cyan); } -.step-done .step-indicator { border-color: var(--green); background: var(--green); } -.step-done .step-dot { background: transparent; } -.step-pending { opacity: 0.5; } - -.step-content { - display: flex; - flex-direction: column; -} - -.step-content strong { font-size: 14px; margin-bottom: 4px; } -.step-content span { font-size: 11px; color: var(--muted); } - -.demo-layout { - display: grid; - grid-template-columns: 1fr 400px; - gap: 24px; - align-items: start; -} - -.demo-scene-container { - background: var(--panel); - border: 1px solid var(--line); - border-radius: 20px; - padding: 20px; -} - -/* Sorter scene sizing fix */ -.demo-scene-container .scene-wrap { - width: 100%; -} -.demo-scene-container .sorter-svg { - width: 100%; - height: auto; - max-height: 600px; -} -.demo-scene-container .scene-title-row { - display: none; -} - -.demo-proof-container { - display: flex; - flex-direction: column; - gap: 16px; -} - -/* Redesigned Proof Card */ -.proof-card { - background: var(--panel); - border: 1px solid var(--line); - border-radius: 20px; - padding: 24px; - display: flex; - flex-direction: column; - gap: 20px; -} - -.proof-card-stopped { - border-color: var(--red); - box-shadow: 0 0 20px rgba(251, 61, 78, 0.1); -} - -.proof-card-empty { - text-align: center; - align-items: center; - justify-content: center; - min-height: 300px; -} - -.proof-card-header { - border-bottom: 1px solid var(--line); - padding-bottom: 16px; -} - -.proof-card-header h3 { margin: 4px 0 0; font-size: 20px; } - -.proof-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 12px; -} +.decision-tree .pass { border-color: rgba(74, 222, 128, 0.65); } +.decision-tree .fail { border-color: rgba(245, 158, 11, 0.7); } -.proof-item { - background: #0d1b2a; - border: 1px solid #1a2f44; - border-radius: 12px; - padding: 12px; - display: flex; - flex-direction: column; - gap: 4px; -} - -.proof-label { - font-size: 11px; - color: var(--muted); - text-transform: uppercase; -} - -.proof-item strong { - font-size: 16px; -} - -.proof-details { - background: #0d1b2a; - border: 1px solid #1a2f44; - border-radius: 12px; - padding: 12px; - display: flex; - flex-direction: column; - gap: 8px; - font-size: 13px; -} - -.detail-row { - display: flex; - justify-content: space-between; -} -.detail-row span { color: var(--muted); } - -.proof-reason { - background: rgba(56, 189, 248, 0.05); - border-left: 4px solid var(--cyan); - padding: 12px 16px; - border-radius: 0 12px 12px 0; - font-size: 14px; - line-height: 1.5; -} - -.demo-controls { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 12px; -} - -/* Scenarios */ -.scenario-cards-section { - padding: 60px 0; - border-top: 1px solid var(--line); -} - -.section-header { - margin-bottom: 40px; -} - -.section-header h2 { - font-size: 32px; - margin: 0 0 12px; - font-weight: 800; -} - -.section-header p { - font-size: 16px; - color: var(--muted); - margin: 0; - max-width: 600px; -} - -.scenario-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: 20px; -} - -.scenario-card { - background: var(--panel); - border: 1px solid var(--line); - border-radius: 16px; - padding: 20px; - display: flex; - flex-direction: column; - cursor: pointer; - transition: all 0.2s; -} - -.scenario-card:hover { - border-color: var(--cyan); - transform: translateY(-2px); -} - -.scenario-card.active { - border-color: var(--cyan); - background: rgba(56, 189, 248, 0.05); - box-shadow: 0 0 0 1px var(--cyan); -} - -.scenario-card-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - margin-bottom: 12px; -} - -.scenario-card-header h3 { - margin: 0; - font-size: 16px; -} - -.active-badge { - background: var(--cyan); - color: #000; - font-size: 10px; - padding: 2px 6px; - border-radius: 4px; - font-weight: 800; -} - -.scenario-desc { - font-size: 13px; - color: var(--muted); - margin: 0 0 16px; - line-height: 1.4; - flex: 1; -} - -.scenario-meta { - display: flex; - flex-direction: column; - gap: 8px; - margin-bottom: 16px; - font-size: 12px; - background: #0d1b2a; - padding: 10px; - border-radius: 8px; -} - -.meta-item { - display: flex; - flex-direction: column; -} - -.meta-label { color: #64748b; } -.meta-value { font-weight: 600; color: #cbd5e1; } - -.btn-small { - padding: 8px 16px; - font-size: 13px; - width: 100%; -} -.mt-auto { margin-top: auto; } - -/* Criteria */ -.criteria-cards-section { - padding: 60px 0; - border-top: 1px solid var(--line); -} - -.criteria-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); - gap: 20px; -} - -.criteria-card { - background: var(--panel); - border: 1px solid var(--line); - border-radius: 16px; - padding: 20px; - display: flex; - flex-direction: column; -} - -.criteria-card-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - margin-bottom: 12px; - gap: 12px; -} - -.criteria-card-header h3 { - margin: 0; - font-size: 15px; -} - -.criteria-evidence { - font-size: 13px; - color: var(--muted); - line-height: 1.5; - margin: 0 0 16px; - flex: 1; -} - -.criteria-footer { - font-size: 11px; - color: #64748b; - border-top: 1px solid var(--line); - padding-top: 12px; -} - -/* Engineering Details */ -.engineering-details-section { - padding: 40px 0 80px; - border-top: 1px solid var(--line); -} - -.engineering-toggle { - display: flex; - justify-content: space-between; - align-items: center; - background: var(--panel); - padding: 24px; - border-radius: 16px; - border: 1px solid var(--line); - cursor: pointer; -} - -.engineering-toggle h2 { - margin: 0; - font-size: 24px; -} - -.engineering-content { - margin-top: 24px; -} - -.engineering-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); - gap: 20px; -} - -.eng-panel { - background: var(--panel); - border: 1px solid var(--line); - border-radius: 16px; - padding: 16px; -} - -.eng-panel .panel { - border: none; - background: transparent; - box-shadow: none; - padding: 0; -} - -.eng-panel .event-table { - max-width: 100%; -} - -.site-footer { - padding: 40px 20px; - text-align: center; - border-top: 1px solid var(--line); - color: var(--muted); - font-size: 14px; - margin-top: auto; -} - -/* Responsive Overrides */ -body { - min-width: 0 !important; /* Remove old 1180px limit */ -} +/* ---------- Responsive ---------- */ -@media (max-width: 1200px) { +@media (max-width: 1199px) { .hero-section { - flex-direction: column; - text-align: center; - padding: 40px 0; - } - .hero-subtitle { - margin: 0 auto 32px; - } - .hero-badges { - justify-content: center; - } - .hero-actions { - justify-content: center; + grid-template-columns: 1fr; } + .hero-chain { - width: 100%; - justify-content: center; + grid-template-columns: repeat(4, minmax(0, 1fr)); } + .demo-layout { grid-template-columns: 1fr; } -} -@media (max-width: 900px) { - .story-step { - flex-direction: column; - text-align: center; - } - .step-content { - align-items: center; - } - .engineering-grid { - grid-template-columns: 1fr; - } - .header-nav { - display: none; - } - .demo-header { - flex-direction: column; - align-items: flex-start; - gap: 12px; - } .storyline-stepper { - flex-wrap: wrap; - gap: 16px; + grid-template-columns: repeat(3, minmax(0, 1fr)); } } -@media (max-width: 640px) { +@media (max-width: 899px) { + .storyline-stepper { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .metric-grid, + .state-list, + .actuator-grid, + .pid-bars { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 639px) { + .site-header { + align-items: stretch; + } + + .header-nav { + width: 100%; + } + + .nav-link { + flex: 1 1 calc(50% - 8px); + } + + .header-status-compact { + width: 100%; + justify-content: center; + } + + .hero-section { + padding-top: 28px; + } + .hero-title { - font-size: 32px; + font-size: clamp(1.75rem, 8vw, 2.2rem); } + + .hero-actions, + .demo-controls { + grid-template-columns: 1fr; + display: grid; + } + + .hero-actions .btn-primary, + .hero-actions .btn-secondary, + .demo-controls .btn-primary, + .demo-controls .btn-secondary { + width: 100%; + } + .hero-chain { - flex-wrap: wrap; - padding: 20px; + grid-template-columns: repeat(2, minmax(0, 1fr)); } - .chain-arrow { - display: none; + + .demo-mobile-hint { + display: block; } + + .demo-result-summary, .proof-grid { grid-template-columns: 1fr; } - .demo-controls { + + .storyline-stepper, + .scenario-grid, + .criteria-grid, + .engineering-grid { grid-template-columns: 1fr; } - .engineering-toggle h2 { - font-size: 18px; - } - .site-header { + + .engineering-toggle { flex-direction: column; - gap: 12px; + align-items: stretch; + } + + .engineering-toggle .btn-secondary { + width: 100%; + } + + .metric-grid, + .state-list, + .actuator-grid, + .pid-bars, + .timeline-summary { + grid-template-columns: 1fr; + } + + .timeline-row { + grid-template-columns: 14px minmax(0, 1fr); + } + + .timeline-row em:nth-child(n + 3) { + display: none; } } +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } + + button, + .scenario-card, + .criteria-card { + transition: none; + } +}