diff --git a/app-instance/create-instance.sh b/app-instance/create-instance.sh index 9ec5c99..4a98987 100755 --- a/app-instance/create-instance.sh +++ b/app-instance/create-instance.sh @@ -18,6 +18,7 @@ AUTHZ_BASE_URL="" AUTHZ_INTERNAL_TOKEN="" AUTHZ_OUTLOOK_MCP_URL="" OUTLOOK_MCP_SERVER_ID="${OUTLOOK_MCP_SERVER_ID:-outlook_mcp}" +OUTLOOK_MCP_CALL_TIMEOUT_SECONDS="${OUTLOOK_MCP_CALL_TIMEOUT_SECONDS:-60}" USER_FILES_MAX_UPLOAD_BYTES="${USER_FILES_MAX_UPLOAD_BYTES:-}" EXTERNAL_CONNECTOR_BASE_URL="${EXTERNAL_CONNECTOR_BASE_URL:-http://external-connector:8787}" EXTERNAL_CONNECTOR_TOKEN="${EXTERNAL_CONNECTOR_TOKEN:-}" @@ -76,6 +77,8 @@ Optional: Managed Outlook MCP URL for AuthZ mode. --outlook-mcp-server-id Default Outlook MCP server id. Default: outlook_mcp + --outlook-mcp-call-timeout-seconds + Backend wait timeout for Outlook MCP calls. Default: 60 --user-files-max-upload-bytes Optional max upload size for the user file system. --external-connector-base-url @@ -557,6 +560,10 @@ while [[ $# -gt 0 ]]; do OUTLOOK_MCP_SERVER_ID="${2:-}" shift 2 ;; + --outlook-mcp-call-timeout-seconds) + OUTLOOK_MCP_CALL_TIMEOUT_SECONDS="${2:-}" + shift 2 + ;; --user-files-max-upload-bytes) USER_FILES_MAX_UPLOAD_BYTES="${2:-}" shift 2 @@ -774,6 +781,7 @@ RUN_ARGS=( -e "APP_BACKEND_PORT=18080" -e "BEAVER_ENABLE_SELF_RESTART=1" -e "BEAVER_OUTLOOK_MCP_SERVER_ID=${OUTLOOK_MCP_SERVER_ID}" + -e "BEAVER_OUTLOOK_MCP_CALL_TIMEOUT_SECONDS=${OUTLOOK_MCP_CALL_TIMEOUT_SECONDS}" -e "EXTERNAL_CONNECTOR_BASE_URL=${EXTERNAL_CONNECTOR_BASE_URL}" --label "beaver.instance.id=${INSTANCE_ID}" --label "beaver.instance.slug=${INSTANCE_SLUG}" diff --git a/deploy-control/.env.example b/deploy-control/.env.example index f22b2d2..1369d5f 100644 --- a/deploy-control/.env.example +++ b/deploy-control/.env.example @@ -16,6 +16,7 @@ APP_INSTANCE_API_BASE= DEFAULT_AUTHZ_BASE_URL=http://beaver-authz-service:19090 DEFAULT_AUTHZ_OUTLOOK_MCP_URL= DEFAULT_OUTLOOK_MCP_SERVER_ID=outlook_mcp +DEFAULT_OUTLOOK_MCP_CALL_TIMEOUT_SECONDS=60 DEFAULT_USER_FILES_MAX_UPLOAD_BYTES=5368709120 DEFAULT_EXTERNAL_CONNECTOR_BASE_URL=http://external-connector:8787 DEFAULT_EXTERNAL_CONNECTOR_TOKEN= diff --git a/deploy-control/README.md b/deploy-control/README.md index cbc8d88..ae29128 100644 --- a/deploy-control/README.md +++ b/deploy-control/README.md @@ -20,6 +20,7 @@ - `DEFAULT_AUTHZ_BASE_URL` - `DEFAULT_AUTHZ_OUTLOOK_MCP_URL` - `DEFAULT_OUTLOOK_MCP_SERVER_ID` +- `DEFAULT_OUTLOOK_MCP_CALL_TIMEOUT_SECONDS` - `DEPLOY_PUBLIC_BASE_DOMAIN` - `DEPLOY_PUBLIC_PORT` - `DEPLOY_PUBLIC_SCHEME` @@ -42,6 +43,7 @@ http://.localhost:8088 ```bash DEFAULT_AUTHZ_OUTLOOK_MCP_URL=http://10.6.80.29:8000/mcp DEFAULT_OUTLOOK_MCP_SERVER_ID=outlook_mcp +DEFAULT_OUTLOOK_MCP_CALL_TIMEOUT_SECONDS=60 ``` 这样 `deploy-control` 创建的新实例会自动写入一条默认 MCP server 配置,并默认使用 `oauth_backend_token` + `mcp:` 的 audience。 diff --git a/deploy-control/server.py b/deploy-control/server.py index b016dc7..bcde5d0 100755 --- a/deploy-control/server.py +++ b/deploy-control/server.py @@ -41,6 +41,9 @@ DEFAULT_AUTHZ_BASE_URL = os.environ.get("DEFAULT_AUTHZ_BASE_URL", "").strip() DEFAULT_AUTHZ_INTERNAL_TOKEN = os.environ.get("DEFAULT_AUTHZ_INTERNAL_TOKEN", "").strip() DEFAULT_AUTHZ_OUTLOOK_MCP_URL = os.environ.get("DEFAULT_AUTHZ_OUTLOOK_MCP_URL", "").strip() DEFAULT_OUTLOOK_MCP_SERVER_ID = os.environ.get("DEFAULT_OUTLOOK_MCP_SERVER_ID", "outlook_mcp").strip() or "outlook_mcp" +DEFAULT_OUTLOOK_MCP_CALL_TIMEOUT_SECONDS = ( + os.environ.get("DEFAULT_OUTLOOK_MCP_CALL_TIMEOUT_SECONDS", "60").strip() or "60" +) DEFAULT_USER_FILES_MAX_UPLOAD_BYTES = os.environ.get("DEFAULT_USER_FILES_MAX_UPLOAD_BYTES", "").strip() DEFAULT_EXTERNAL_CONNECTOR_BASE_URL = os.environ.get( "DEFAULT_EXTERNAL_CONNECTOR_BASE_URL", @@ -279,6 +282,7 @@ def create_or_get_instance(payload: dict[str, Any]) -> dict[str, Any]: if authz_outlook_mcp_url: command.extend(["--authz-outlook-mcp-url", authz_outlook_mcp_url]) command.extend(["--outlook-mcp-server-id", DEFAULT_OUTLOOK_MCP_SERVER_ID]) + command.extend(["--outlook-mcp-call-timeout-seconds", DEFAULT_OUTLOOK_MCP_CALL_TIMEOUT_SECONDS]) if DEFAULT_USER_FILES_MAX_UPLOAD_BYTES: command.extend(["--user-files-max-upload-bytes", DEFAULT_USER_FILES_MAX_UPLOAD_BYTES]) if DEFAULT_EXTERNAL_CONNECTOR_BASE_URL: diff --git a/deploy-control/tests/test_connector_instance_config.py b/deploy-control/tests/test_connector_instance_config.py index 50ce122..2c63506 100644 --- a/deploy-control/tests/test_connector_instance_config.py +++ b/deploy-control/tests/test_connector_instance_config.py @@ -35,6 +35,8 @@ def test_new_instance_receives_external_connector_configuration(monkeypatch) -> monkeypatch.setattr(server, "DEFAULT_EXTERNAL_CONNECTOR_TOKEN", "connector-token") monkeypatch.setattr(server, "DEFAULT_BEAVER_BRIDGE_TOKEN", "bridge-token") monkeypatch.setattr(server, "DEFAULT_INITIAL_SKILLS_DIR", "/srv/beaver/skills") + monkeypatch.setattr(server, "DEFAULT_AUTHZ_OUTLOOK_MCP_URL", "http://bw-outlook-mcp:8000/mcp") + monkeypatch.setattr(server, "DEFAULT_OUTLOOK_MCP_CALL_TIMEOUT_SECONDS", "60") def capture_command(args: list[str], **_kwargs: Any) -> str: commands.append(args) @@ -55,4 +57,5 @@ def test_new_instance_receives_external_connector_configuration(monkeypatch) -> assert command[command.index("--external-connector-token") + 1] == "connector-token" assert command[command.index("--bridge-token") + 1] == "bridge-token" assert command[command.index("--initial-skills-dir") + 1] == "/srv/beaver/skills" + assert command[command.index("--outlook-mcp-call-timeout-seconds") + 1] == "60" assert result["created"] is True diff --git a/docs/presentations/skill-replay-eval/assets/animations/animations.css b/docs/presentations/skill-replay-eval/assets/animations/animations.css new file mode 100644 index 0000000..1d00263 --- /dev/null +++ b/docs/presentations/skill-replay-eval/assets/animations/animations.css @@ -0,0 +1,138 @@ +/* html-ppt :: animations.css + * Apply by adding class="anim-" or data-anim="". + * Durations are deliberately snappy; tweak --anim-dur per element. + */ +:root{--anim-dur:.7s;--anim-ease:cubic-bezier(.4,0,.2,1)} + +/* ---------- FADE DIRECTIONALS ---------- */ +@keyframes kf-fade-up{from{opacity:0;transform:translateY(32px)}to{opacity:1;transform:none}} +@keyframes kf-fade-down{from{opacity:0;transform:translateY(-32px)}to{opacity:1;transform:none}} +@keyframes kf-fade-left{from{opacity:0;transform:translateX(-40px)}to{opacity:1;transform:none}} +@keyframes kf-fade-right{from{opacity:0;transform:translateX(40px)}to{opacity:1;transform:none}} +.anim-fade-up{animation:kf-fade-up var(--anim-dur) var(--anim-ease) both} +.anim-fade-down{animation:kf-fade-down var(--anim-dur) var(--anim-ease) both} +.anim-fade-left{animation:kf-fade-left var(--anim-dur) var(--anim-ease) both} +.anim-fade-right{animation:kf-fade-right var(--anim-dur) var(--anim-ease) both} + +/* ---------- RISE / DROP / ZOOM / BLUR / GLITCH ---------- */ +@keyframes kf-rise{from{opacity:0;transform:translateY(60px) scale(.97);filter:blur(6px)}to{opacity:1;transform:none;filter:none}} +@keyframes kf-drop{from{opacity:0;transform:translateY(-60px) scale(.97)}to{opacity:1;transform:none}} +@keyframes kf-zoom{0%{opacity:0;transform:scale(.6)}60%{transform:scale(1.04)}100%{opacity:1;transform:scale(1)}} +@keyframes kf-blur{from{opacity:0;filter:blur(18px)}to{opacity:1;filter:none}} +@keyframes kf-glitch{0%{opacity:0;transform:translateX(0);clip-path:inset(0 0 0 0)} + 20%{opacity:1;transform:translateX(-6px);clip-path:inset(20% 0 30% 0)} + 40%{transform:translateX(4px);clip-path:inset(50% 0 10% 0)} + 60%{transform:translateX(-3px);clip-path:inset(10% 0 60% 0)} + 80%{transform:translateX(2px);clip-path:inset(0 0 0 0)} + 100%{opacity:1;transform:none}} +.anim-rise-in{animation:kf-rise .9s var(--anim-ease) both} +.anim-drop-in{animation:kf-drop .8s var(--anim-ease) both} +.anim-zoom-pop{animation:kf-zoom .7s cubic-bezier(.22,1.3,.36,1) both} +.anim-blur-in{animation:kf-blur .8s var(--anim-ease) both} +.anim-glitch-in{animation:kf-glitch .8s steps(5,end) both} + +/* ---------- TYPEWRITER ---------- */ +.anim-typewriter{display:inline-block;overflow:hidden;white-space:nowrap;border-right:2px solid currentColor; + width:0;animation:kf-type 2.4s steps(40,end) forwards, kf-caret 1s step-end infinite} +@keyframes kf-type{to{width:100%}} +@keyframes kf-caret{50%{border-color:transparent}} + +/* ---------- GLOW / SHIMMER / GRADIENT-FLOW ---------- */ +@keyframes kf-neon{0%,100%{text-shadow:0 0 8px var(--accent),0 0 20px var(--accent)} + 50%{text-shadow:0 0 16px var(--accent),0 0 40px var(--accent),0 0 80px var(--accent)}} +.anim-neon-glow{animation:kf-neon 2s ease-in-out infinite} + +.anim-shimmer-sweep{position:relative;overflow:hidden} +.anim-shimmer-sweep::after{content:"";position:absolute;inset:0; + background:linear-gradient(110deg,transparent 40%,rgba(255,255,255,.55) 50%,transparent 60%); + transform:translateX(-100%);animation:kf-shimmer 2.4s var(--anim-ease) infinite} +@keyframes kf-shimmer{to{transform:translateX(100%)}} + +.anim-gradient-flow{background:linear-gradient(90deg,var(--accent),var(--accent-2,var(--accent)),var(--accent-3,var(--accent)),var(--accent)); + background-size:300% 100%;-webkit-background-clip:text;background-clip:text;color:transparent;-webkit-text-fill-color:transparent; + animation:kf-gradflow 4s linear infinite} +@keyframes kf-gradflow{to{background-position:300% 0}} + +/* ---------- STAGGER LIST ---------- */ +.anim-stagger-list > *{opacity:0;animation:kf-rise .65s var(--anim-ease) both} +.anim-stagger-list > *:nth-child(1){animation-delay:.05s} +.anim-stagger-list > *:nth-child(2){animation-delay:.15s} +.anim-stagger-list > *:nth-child(3){animation-delay:.25s} +.anim-stagger-list > *:nth-child(4){animation-delay:.35s} +.anim-stagger-list > *:nth-child(5){animation-delay:.45s} +.anim-stagger-list > *:nth-child(6){animation-delay:.55s} +.anim-stagger-list > *:nth-child(7){animation-delay:.65s} +.anim-stagger-list > *:nth-child(8){animation-delay:.75s} +.anim-stagger-list > *:nth-child(n+9){animation-delay:.85s} + +/* ---------- COUNTER-UP (JS-driven, marker class only) ---------- */ +.counter{font-variant-numeric:tabular-nums} + +/* ---------- SVG PATH DRAW ---------- */ +.anim-path-draw path,.anim-path-draw line,.anim-path-draw polyline,.anim-path-draw circle,.anim-path-draw rect{ + stroke-dasharray:1000;stroke-dashoffset:1000;animation:kf-draw 2s var(--anim-ease) forwards} +@keyframes kf-draw{to{stroke-dashoffset:0}} + +/* ---------- PARALLAX TILT (hover) ---------- */ +.anim-parallax-tilt{transform-style:preserve-3d;transition:transform .4s var(--anim-ease)} +.anim-parallax-tilt:hover{transform:perspective(900px) rotateX(6deg) rotateY(-8deg) translateZ(10px)} + +/* ---------- CARD FLIP 3D ---------- */ +@keyframes kf-flip{from{transform:perspective(1200px) rotateY(-90deg);opacity:0} + to{transform:perspective(1200px) rotateY(0);opacity:1}} +.anim-card-flip-3d{animation:kf-flip .9s var(--anim-ease) both;transform-style:preserve-3d;backface-visibility:hidden} + +/* ---------- CUBE ROTATE 3D ---------- */ +@keyframes kf-cube{from{transform:perspective(1200px) rotateX(20deg) rotateY(-90deg) translateZ(-200px);opacity:0} + to{transform:perspective(1200px) rotateX(0) rotateY(0) translateZ(0);opacity:1}} +.anim-cube-rotate-3d{animation:kf-cube 1s var(--anim-ease) both} + +/* ---------- PAGE TURN 3D ---------- */ +@keyframes kf-pageturn{from{transform:perspective(1600px) rotateY(-85deg);transform-origin:left center;opacity:0} + to{transform:perspective(1600px) rotateY(0);opacity:1}} +.anim-page-turn-3d{animation:kf-pageturn 1s var(--anim-ease) both;transform-origin:left center} + +/* ---------- PERSPECTIVE ZOOM ---------- */ +@keyframes kf-pzoom{from{opacity:0;transform:perspective(1400px) translateZ(-400px) rotateX(12deg)} + to{opacity:1;transform:none}} +.anim-perspective-zoom{animation:kf-pzoom 1s var(--anim-ease) both} + +/* ---------- MARQUEE SCROLL ---------- */ +.anim-marquee-scroll{display:flex;gap:48px;white-space:nowrap;animation:kf-marquee 20s linear infinite} +@keyframes kf-marquee{from{transform:translateX(0)}to{transform:translateX(-50%)}} + +/* ---------- KEN BURNS ---------- */ +@keyframes kf-kenburns{0%{transform:scale(1) translate(0,0)}100%{transform:scale(1.15) translate(-2%,-1%)}} +.anim-kenburns{animation:kf-kenburns 14s ease-in-out infinite alternate} + +/* ---------- CONFETTI BURST (pseudo — pure CSS sparkles) ---------- */ +.anim-confetti-burst{position:relative} +.anim-confetti-burst::before,.anim-confetti-burst::after{ + content:"";position:absolute;top:50%;left:50%;width:8px;height:8px;border-radius:50%; + background:var(--accent);box-shadow: + 20px -30px 0 var(--accent-2,var(--accent)),-25px -20px 0 var(--accent-3,var(--accent)), + 30px 20px 0 var(--good,#1aaf6c),-30px 25px 0 var(--warn,#f5a524), + 40px -10px 0 var(--bad,#e0445a),-45px 0 0 var(--accent), + 10px 40px 0 var(--accent-2,var(--accent)),-15px -40px 0 var(--accent-3,var(--accent)); + opacity:0;animation:kf-confetti 1.2s var(--anim-ease) forwards} +.anim-confetti-burst::after{animation-delay:.15s;transform:rotate(45deg)} +@keyframes kf-confetti{0%{opacity:0;transform:scale(.2)}30%{opacity:1}100%{opacity:0;transform:scale(2.2)}} + +/* ---------- SPOTLIGHT ---------- */ +@keyframes kf-spot{0%{clip-path:circle(0% at 50% 50%)}100%{clip-path:circle(140% at 50% 50%)}} +.anim-spotlight{animation:kf-spot 1.1s var(--anim-ease) both} + +/* ---------- MORPH SHAPE (SVG) ---------- */ +.anim-morph-shape path{animation:kf-morph 6s ease-in-out infinite alternate} +@keyframes kf-morph{0%{d:path("M60,120 Q120,20 180,120 T300,120")} + 100%{d:path("M60,120 Q120,220 180,120 T300,120")}} + +/* ---------- RIPPLE REVEAL ---------- */ +@keyframes kf-ripple{0%{clip-path:circle(0% at 20% 80%);opacity:.4} + 100%{clip-path:circle(160% at 20% 80%);opacity:1}} +.anim-ripple-reveal{animation:kf-ripple 1.2s var(--anim-ease) both} + +/* reduced motion */ +@media (prefers-reduced-motion: reduce){ + [class*="anim-"]{animation:none!important;transition:none!important} +} diff --git a/docs/presentations/skill-replay-eval/assets/base.css b/docs/presentations/skill-replay-eval/assets/base.css new file mode 100644 index 0000000..b8c1107 --- /dev/null +++ b/docs/presentations/skill-replay-eval/assets/base.css @@ -0,0 +1,150 @@ +/* html-ppt :: base.css — reset + shared tokens + layout primitives */ +/* Default tokens. Themes in assets/themes/*.css override the :root block. */ +:root { + --bg: #ffffff; + --bg-soft: #f7f7f8; + --surface: #ffffff; + --surface-2: #f2f2f4; + --border: rgba(0,0,0,.08); + --border-strong: rgba(0,0,0,.16); + --text-1: #111216; + --text-2: #55596a; + --text-3: #8a8f9e; + --accent: #3b6cff; + --accent-2: #7a5cff; + --accent-3: #ff5c8a; + --good: #1aaf6c; + --warn: #f5a524; + --bad: #e0445a; + --grad: linear-gradient(135deg,#3b6cff,#7a5cff 55%,#ff5c8a); + --grad-soft: linear-gradient(135deg,#eef2ff,#f5ecff 55%,#ffeef5); + --radius: 18px; + --radius-sm: 12px; + --radius-lg: 26px; + --shadow: 0 10px 30px rgba(18,24,40,.08), 0 2px 6px rgba(18,24,40,.04); + --shadow-lg: 0 24px 60px rgba(18,24,40,.14), 0 6px 16px rgba(18,24,40,.06); + --font-sans: 'Inter','Noto Sans SC',-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif; + --font-serif: 'Playfair Display','Noto Serif SC',Georgia,serif; + --font-mono: 'JetBrains Mono','IBM Plex Mono',SFMono-Regular,Menlo,monospace; + --font-display: var(--font-sans); + --letter-tight: -.03em; + --letter-normal: -.01em; + --ease: cubic-bezier(.4,0,.2,1); +} + +*,*::before,*::after{box-sizing:border-box} +html,body{margin:0;padding:0;background:var(--bg);color:var(--text-1); + font-family:var(--font-sans);font-weight:400;line-height:1.6; + -webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale; + letter-spacing:var(--letter-normal)} +img,svg,video{max-width:100%;display:block} +a{color:var(--accent);text-decoration:none} +a:hover{text-decoration:underline} +code,kbd,pre,samp{font-family:var(--font-mono)} + +/* ================= SLIDE SYSTEM ================= */ +.deck{position:relative;width:100vw;height:100vh;overflow:hidden;background:var(--bg)} +.slide{ + position:absolute;inset:0; + display:flex;flex-direction:column;justify-content:center; + padding:72px 96px; + box-sizing:border-box; + opacity:0;pointer-events:none; + transition:opacity .5s var(--ease), transform .5s var(--ease); + transform:translateX(30px); + overflow:hidden; +} +.slide.is-active{opacity:1;pointer-events:auto;transform:translateX(0);z-index:2} +.slide.is-prev{transform:translateX(-30px)} + +/* single-page standalone (used when a layout file is opened directly) */ +body.single .slide{position:relative;width:100vw;height:100vh;opacity:1;transform:none;pointer-events:auto} + +/* ================= TYPOGRAPHY ================= */ +.eyebrow{font-size:13px;font-weight:500;letter-spacing:.16em;text-transform:uppercase;color:var(--text-3)} +.kicker{font-size:14px;font-weight:600;color:var(--accent);letter-spacing:.08em;text-transform:uppercase} +h1.title,.h1{font-family:var(--font-display);font-size:72px;line-height:1.05;font-weight:800;letter-spacing:var(--letter-tight);margin:0 0 18px;color:var(--text-1)} +h2.title,.h2{font-family:var(--font-display);font-size:54px;line-height:1.1;font-weight:700;letter-spacing:var(--letter-tight);margin:0 0 14px} +h3,.h3{font-size:32px;line-height:1.2;font-weight:600;letter-spacing:var(--letter-normal);margin:0 0 10px} +h4,.h4{font-size:22px;line-height:1.3;font-weight:600;margin:0 0 8px} +.lede{font-size:22px;line-height:1.55;color:var(--text-2);font-weight:300;max-width:62ch} +.dim{color:var(--text-2)} +.dim2{color:var(--text-3)} +.mono{font-family:var(--font-mono)} +.serif{font-family:var(--font-serif)} +.gradient-text{background:var(--grad);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:transparent} + +/* ================= LAYOUT PRIMITIVES ================= */ +.stack>*+*{margin-top:14px} +.row{display:flex;gap:24px;align-items:center} +.row.wrap{flex-wrap:wrap} +.grid{display:grid;gap:24px} +.g2{grid-template-columns:repeat(2,1fr)} +.g3{grid-template-columns:repeat(3,1fr)} +.g4{grid-template-columns:repeat(4,1fr)} +.center{display:flex;align-items:center;justify-content:center;text-align:center} +.fill{flex:1} +.sp-t{padding-top:24px}.sp-b{padding-bottom:24px} +.mt-s{margin-top:8px}.mt-m{margin-top:18px}.mt-l{margin-top:32px} +.mb-s{margin-bottom:8px}.mb-m{margin-bottom:18px}.mb-l{margin-bottom:32px} + +/* ================= CARDS ================= */ +.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius); + padding:26px 28px;box-shadow:var(--shadow);position:relative;overflow:hidden} +.card-soft{background:var(--surface-2);border:1px solid var(--border)} +.card-outline{background:transparent;border:1.5px solid var(--border-strong);box-shadow:none} +.card-accent{background:var(--surface);border-top:3px solid var(--accent)} +.card-hover{transition:transform .3s var(--ease),box-shadow .3s var(--ease)} +.card-hover:hover{transform:translateY(-4px);box-shadow:var(--shadow-lg)} + +.pill{display:inline-block;padding:4px 12px;border-radius:999px;font-size:12px;font-weight:500; + background:var(--surface-2);color:var(--text-2);border:1px solid var(--border)} +.pill-accent{background:color-mix(in srgb,var(--accent) 12%,transparent);color:var(--accent);border-color:color-mix(in srgb,var(--accent) 28%,transparent)} + +/* ================= BARS / DIVIDERS ================= */ +.divider{height:1px;background:var(--border);width:100%} +.divider-accent{height:3px;width:72px;background:var(--accent);border-radius:2px} + +/* ================= CHROME (header/footer/progress) ================= */ +.deck-header{position:absolute;top:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between; + font-size:12px;color:var(--text-3);letter-spacing:.12em;text-transform:uppercase;z-index:10;pointer-events:none} +.deck-footer{position:absolute;bottom:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between; + font-size:12px;color:var(--text-3);z-index:10;pointer-events:none} +.slide-number::before{content:attr(data-current)} +.slide-number::after{content:" / " attr(data-total)} +.progress-bar{position:fixed;left:0;right:0;bottom:0;height:3px;background:transparent;z-index:20} +.progress-bar > span{display:block;height:100%;width:0;background:var(--accent);transition:width .3s var(--ease)} + +/* ================= PRESENTER / OVERVIEW ================= */ +.notes{display:none!important} +.notes-overlay{position:fixed;inset:auto 0 0 0;max-height:42vh;background:rgba(20,22,30,.95);color:#e8ebf4; + padding:20px 32px;font-size:16px;line-height:1.6;border-top:1px solid rgba(255,255,255,.1);transform:translateY(100%); + transition:transform .3s var(--ease);z-index:40;overflow:auto;font-family:var(--font-sans)} +.notes-overlay.open{transform:translateY(0)} +.overview{position:fixed;inset:0;background:rgba(10,12,18,.92);backdrop-filter:blur(12px);z-index:50; + display:none;padding:40px;overflow:auto} +.overview.open{display:grid;grid-template-columns:repeat(4,1fr);gap:20px;align-content:start} +.overview .thumb{background:var(--surface);border:1px solid var(--border);border-radius:12px; + aspect-ratio:16/9;overflow:hidden;cursor:pointer;position:relative;color:var(--text-1);padding:16px; + font-size:11px;transition:transform .2s var(--ease)} +.overview .thumb:hover{transform:scale(1.04)} +.overview .thumb .n{position:absolute;top:8px;left:10px;font-weight:700;font-size:14px;color:var(--text-3)} +.overview .thumb .t{position:absolute;bottom:10px;left:14px;right:14px;font-weight:600;color:var(--text-1)} + +/* ================= PRESENTER VIEW ================= */ +/* Presenter view opens in a separate popup window (S key). + * All presenter styles are self-contained in the popup HTML generated by runtime.js. + * The audience window (this file) is NOT affected — it stays as normal deck view. + * Only the .notes class below is needed to hide speaker notes from audience. */ + +/* ================= UTILITY ================= */ +.hidden{display:none!important} +.nowrap{white-space:nowrap} +.tr{text-align:right}.tc{text-align:center}.tl{text-align:left} +.uppercase{text-transform:uppercase;letter-spacing:.12em} + +/* ================= PRINT ================= */ +@media print{ + .slide{position:relative;opacity:1!important;transform:none!important;page-break-after:always;height:100vh} + .deck-header,.deck-footer,.progress-bar,.notes-overlay,.overview{display:none!important} +} diff --git a/docs/presentations/skill-replay-eval/assets/fonts.css b/docs/presentations/skill-replay-eval/assets/fonts.css new file mode 100644 index 0000000..64feb72 --- /dev/null +++ b/docs/presentations/skill-replay-eval/assets/fonts.css @@ -0,0 +1,9 @@ +/* html-ppt :: shared webfonts */ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700;800;900&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@200;300;400;500;600;700;900&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@300;400;600;700&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;0,800;1,400&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;700&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Archivo+Black&display=swap'); diff --git a/docs/presentations/skill-replay-eval/assets/runtime.js b/docs/presentations/skill-replay-eval/assets/runtime.js new file mode 100644 index 0000000..535907d --- /dev/null +++ b/docs/presentations/skill-replay-eval/assets/runtime.js @@ -0,0 +1,960 @@ +/* html-ppt :: runtime.js + * Keyboard-driven deck runtime. Zero dependencies. + * + * Features: + * ← → / space / PgUp PgDn / Home End navigation + * F fullscreen + * S presenter mode (opens a NEW WINDOW with current/next slide preview + notes + timer) + * The original window stays as audience view, synced via BroadcastChannel. + * Slide previews use CSS transform:scale() at design resolution for pixel-perfect layout. + * N quick notes overlay (bottom drawer) + * O slide overview grid + * T cycle themes (reads data-themes on or ) + * A cycle demo animation on current slide + * URL hash #/N deep-link to slide N (1-based) + * Progress bar auto-managed + */ +(function () { + 'use strict'; + + const ANIMS = ['fade-up','fade-down','fade-left','fade-right','rise-in','drop-in', + 'zoom-pop','blur-in','glitch-in','typewriter','neon-glow','shimmer-sweep', + 'gradient-flow','stagger-list','counter-up','path-draw','parallax-tilt', + 'card-flip-3d','cube-rotate-3d','page-turn-3d','perspective-zoom', + 'marquee-scroll','kenburns','confetti-burst','spotlight','morph-shape','ripple-reveal']; + + function ready(fn){ if(document.readyState!='loading')fn(); else document.addEventListener('DOMContentLoaded',fn);} + + /* ========== Parse URL for preview-only mode ========== + * When loaded as iframe.src = "index.html?preview=3", runtime enters a + * locked single-slide mode: only slide N is visible, no chrome, no keys, + * no hash updates. This is how the presenter window shows pixel-perfect + * previews — by loading the actual deck file in an iframe and telling it + * to display only a specific slide. + */ + function getPreviewIdx() { + const m = /[?&]preview=(\d+)/.exec(location.search || ''); + return m ? parseInt(m[1], 10) - 1 : -1; + } + + ready(function () { + const deck = document.querySelector('.deck'); + if (!deck) return; + const slides = Array.from(deck.querySelectorAll('.slide')); + if (!slides.length) return; + + const previewOnlyIdx = getPreviewIdx(); + const isPreviewMode = previewOnlyIdx >= 0 && previewOnlyIdx < slides.length; + + /* ===== Preview-only mode: show one slide, hide everything else ===== */ + if (isPreviewMode) { + function showSlide(i) { + slides.forEach((s, j) => { + const active = (j === i); + s.classList.toggle('is-active', active); + s.style.display = active ? '' : 'none'; + if (active) { + s.style.opacity = '1'; + s.style.transform = 'none'; + s.style.pointerEvents = 'auto'; + } + }); + } + showSlide(previewOnlyIdx); + /* Hide chrome that the presenter shouldn't see in preview */ + const hideSel = '.progress-bar, .notes-overlay, .overview, .notes, aside.notes, .speaker-notes'; + document.querySelectorAll(hideSel).forEach(el => { el.style.display = 'none'; }); + document.documentElement.setAttribute('data-preview', '1'); + document.body.setAttribute('data-preview', '1'); + /* Auto-detect theme base path for theme switching in preview mode */ + function getPreviewThemeBase() { + const base = document.documentElement.getAttribute('data-theme-base'); + if (base) return base; + const tl = document.getElementById('theme-link'); + if (tl) { + const raw = tl.getAttribute('href') || ''; + const ls = raw.lastIndexOf('/'); + if (ls >= 0) return raw.substring(0, ls + 1); + } + return 'assets/themes/'; + } + const previewThemeBase = getPreviewThemeBase(); + + /* Listen for postMessage from parent presenter window: + * - preview-goto: switch visible slide WITHOUT reloading + * - preview-theme: switch theme CSS link to match audience window */ + window.addEventListener('message', function(e) { + if (!e.data) return; + if (e.data.type === 'preview-goto') { + const n = parseInt(e.data.idx, 10); + if (n >= 0 && n < slides.length) showSlide(n); + } else if (e.data.type === 'preview-theme' && e.data.name) { + let link = document.getElementById('theme-link'); + if (!link) { + link = document.createElement('link'); + link.rel = 'stylesheet'; + link.id = 'theme-link'; + document.head.appendChild(link); + } + link.href = previewThemeBase + e.data.name + '.css'; + document.documentElement.setAttribute('data-theme', e.data.name); + } + }); + /* Signal to parent that preview iframe is ready */ + try { window.parent && window.parent.postMessage({ type: 'preview-ready' }, '*'); } catch(e) {} + return; + } + + let idx = 0; + const total = slides.length; + + /* ===== BroadcastChannel for presenter sync ===== */ + const CHANNEL_NAME = 'html-ppt-presenter-' + location.pathname; + let bc; + try { bc = new BroadcastChannel(CHANNEL_NAME); } catch(e) { bc = null; } + + // Are we running inside the presenter popup? (legacy flag, now unused) + const isPresenterWindow = false; + + /* ===== progress bar ===== */ + let bar = document.querySelector('.progress-bar'); + if (!bar) { + bar = document.createElement('div'); + bar.className = 'progress-bar'; + bar.innerHTML = ''; + document.body.appendChild(bar); + } + const barFill = bar.querySelector('span'); + + /* ===== notes overlay (N key) ===== */ + let notes = document.querySelector('.notes-overlay'); + if (!notes) { + notes = document.createElement('div'); + notes.className = 'notes-overlay'; + document.body.appendChild(notes); + } + + /* ===== overview grid (O key) ===== */ + let overview = document.querySelector('.overview'); + if (!overview) { + overview = document.createElement('div'); + overview.className = 'overview'; + slides.forEach((s, i) => { + const t = document.createElement('div'); + t.className = 'thumb'; + // Force 16:9 aspect ratio robustly + t.style.padding = '0 0 56.25% 0'; + t.style.height = '0'; + t.style.position = 'relative'; + t.style.overflow = 'hidden'; + + const title = s.getAttribute('data-title') || + (s.querySelector('h1,h2,h3')||{}).textContent || ('Slide '+(i+1)); + + // Create a container for the mini-slide + const mini = document.createElement('div'); + mini.className = 'mini-slide'; + mini.style.position = 'absolute'; + mini.style.top = '0'; + mini.style.left = '0'; + mini.style.width = '1920px'; + mini.style.height = '1080px'; + mini.style.transformOrigin = 'top left'; + mini.style.pointerEvents = 'none'; + mini.style.background = 'var(--bg)'; + + // Clone the slide content + const clone = s.cloneNode(true); + clone.className = 'slide is-active'; // force active styles + clone.style.position = 'absolute'; + clone.style.inset = '0'; + clone.style.transform = 'none'; + clone.style.opacity = '1'; + clone.style.padding = '72px 96px'; // ensure padding is kept + + mini.appendChild(clone); + t.appendChild(mini); + + // Add the number and title overlay + const overlay = document.createElement('div'); + overlay.style.position = 'absolute'; + overlay.style.inset = '0'; + overlay.style.background = 'linear-gradient(to bottom, rgba(0,0,0,0.2) 0%, transparent 40%, transparent 60%, rgba(0,0,0,0.8) 100%)'; + overlay.style.color = '#fff'; + overlay.style.zIndex = '10'; + overlay.style.pointerEvents = 'none'; + + const n = document.createElement('div'); + n.className = 'n'; + n.textContent = i + 1; + n.style.position = 'absolute'; + n.style.top = '12px'; + n.style.left = '16px'; + n.style.fontWeight = '700'; + n.style.fontSize = '16px'; + n.style.color = '#fff'; + n.style.textShadow = '0 1px 4px rgba(0,0,0,0.8)'; + + const text = document.createElement('div'); + text.className = 't'; + text.textContent = title.trim().slice(0,80); + text.style.position = 'absolute'; + text.style.bottom = '12px'; + text.style.left = '16px'; + text.style.right = '16px'; + text.style.fontWeight = '600'; + text.style.fontSize = '14px'; + text.style.color = '#fff'; + text.style.textShadow = '0 1px 4px rgba(0,0,0,0.8)'; + + overlay.appendChild(n); + overlay.appendChild(text); + t.appendChild(overlay); + + t.addEventListener('click', () => { go(i); toggleOverview(false); }); + overview.appendChild(t); + }); + document.body.appendChild(overview); + } + + /* ===== navigation ===== */ + function go(n, fromRemote){ + n = Math.max(0, Math.min(total-1, n)); + slides.forEach((s,i) => { + s.classList.toggle('is-active', i===n); + s.classList.toggle('is-prev', i { + const a = el.getAttribute('data-anim'); + el.classList.remove('anim-'+a); + void el.offsetWidth; + el.classList.add('anim-'+a); + }); + + // counter-up + slides[n].querySelectorAll('.counter').forEach(el => { + const target = parseFloat(el.getAttribute('data-to')||el.textContent); + const dur = parseInt(el.getAttribute('data-dur')||'1200',10); + const start = performance.now(); + const from = 0; + function tick(now){ + const t = Math.min(1,(now-start)/dur); + const v = from + (target-from)*(1-Math.pow(1-t,3)); + el.textContent = (target % 1 === 0) ? Math.round(v) : v.toFixed(1); + if (t<1) requestAnimationFrame(tick); + } + requestAnimationFrame(tick); + }); + + // Broadcast to other window (audience ↔ presenter) + if (!fromRemote && bc) { + bc.postMessage({ type: 'go', idx: n }); + } + } + + /* ===== listen for remote navigation / theme changes ===== */ + if (bc) { + bc.onmessage = function(e) { + if (!e.data) return; + if (e.data.type === 'go' && typeof e.data.idx === 'number') { + go(e.data.idx, true); + } else if (e.data.type === 'theme' && e.data.name) { + /* Sync theme across windows */ + const i = themes.indexOf(e.data.name); + if (i >= 0) themeIdx = i; + applyTheme(e.data.name); + } + }; + } + + function toggleNotes(force){ notes.classList.toggle('open', force!==undefined?force:!notes.classList.contains('open')); } + function toggleOverview(force){ + const isOpen = force!==undefined ? force : !overview.classList.contains('open'); + overview.classList.toggle('open', isOpen); + if (isOpen) { + requestAnimationFrame(() => { + const thumbs = overview.querySelectorAll('.thumb'); + if (thumbs.length) { + const scale = thumbs[0].clientWidth / 1920; + overview.querySelectorAll('.mini-slide').forEach(m => { + m.style.transform = 'scale(' + scale + ')'; + }); + } + }); + } + } + + /* ========== PRESENTER MODE — Magnetic-card popup window ========== */ + /* Opens a new window with 4 draggable, resizable cards: + * CURRENT — iframe(?preview=N) pixel-perfect preview of current slide + * NEXT — iframe(?preview=N+1) pixel-perfect preview of next slide + * SCRIPT — large speaker notes (逐字稿) + * TIMER — elapsed timer + page counter + controls + * Cards remember position/size in localStorage. + * Two windows sync via BroadcastChannel. + */ + let presenterWin = null; + + function openPresenterWindow() { + if (presenterWin && !presenterWin.closed) { + presenterWin.focus(); + return; + } + + // Build absolute URL of THIS deck file (without hash/query) + const deckUrl = location.protocol + '//' + location.host + location.pathname; + + // Collect slide titles + notes (HTML strings) + const slideMeta = slides.map((s, i) => { + const note = s.querySelector('.notes, aside.notes, .speaker-notes'); + return { + title: s.getAttribute('data-title') || + (s.querySelector('h1,h2,h3')||{}).textContent || ('Slide '+(i+1)), + notes: note ? note.innerHTML : '' + }; + }); + + /* Capture current theme so presenter previews match the audience */ + const currentTheme = root.getAttribute('data-theme') || (themes[themeIdx] || ''); + const presenterHTML = buildPresenterHTML(deckUrl, slideMeta, total, idx, CHANNEL_NAME, currentTheme); + + presenterWin = window.open('', 'html-ppt-presenter', 'width=1280,height=820,menubar=no,toolbar=no'); + if (!presenterWin) { + alert('请允许弹出窗口以使用演讲者视图'); + return; + } + presenterWin.document.open(); + presenterWin.document.write(presenterHTML); + presenterWin.document.close(); + } + + function buildPresenterHTML(deckUrl, slideMeta, total, startIdx, channelName, currentTheme) { + const metaJSON = JSON.stringify(slideMeta); + const deckUrlJSON = JSON.stringify(deckUrl); + const channelJSON = JSON.stringify(channelName); + const themeJSON = JSON.stringify(currentTheme || ''); + const storageKey = 'html-ppt-presenter:' + location.pathname; + + // Build the document as a single template string for clarity + return ` + + + +Presenter View + + + + +
+
+
+ + CURRENT + +
+
+
+
+ +
+
+ + NEXT + +
+
+
+
+ +
+
+ + SPEAKER SCRIPT · 逐字稿 +
+
+
+
+ +
+
+ + TIMER +
+
+
00:00
+
+ Slide + 1 / ${total} +
+
+ + + +
+
+
+
+
+ +
+ ← → 翻页 + R 重置计时 + Esc 关闭 + 拖动卡片头部移动 · 拖动右下角调整大小 + +
+ + + + diff --git a/docs/presentations/skill-replay-eval/style.css b/docs/presentations/skill-replay-eval/style.css new file mode 100644 index 0000000..e45060c --- /dev/null +++ b/docs/presentations/skill-replay-eval/style.css @@ -0,0 +1,511 @@ +/* Beaver Skill Replay Eval deck, based on html-ppt tech-sharing template. */ +.replay-root { + background: #08111d; +} + +.tpl-beaver-replay { + --bg: #08111d; + --bg-soft: #0d1726; + --surface: #101b2c; + --surface-2: #132235; + --border: rgba(147, 197, 253, .18); + --border-strong: rgba(147, 197, 253, .34); + --text-1: #eef6ff; + --text-2: #a9bfd7; + --text-3: #6f879f; + --accent: #64e3a1; + --accent-2: #7cc7ff; + --accent-3: #d9a6ff; + --good: #64e3a1; + --warn: #ffd166; + --bad: #ff7b7b; + --grad: linear-gradient(120deg, #64e3a1 0%, #7cc7ff 52%, #d9a6ff 100%); + --radius: 8px; + --radius-sm: 6px; + --radius-lg: 8px; + --shadow: 0 20px 60px rgba(0, 0, 0, .38); + --letter-tight: 0; + --letter-normal: 0; + font-family: "Inter", "Noto Sans SC", sans-serif; + background: var(--bg); + color: var(--text-1); +} + +.tpl-beaver-replay .slide { + padding: 50px 72px; + background: + linear-gradient(rgba(124, 199, 255, .04) 1px, transparent 1px), + linear-gradient(90deg, rgba(124, 199, 255, .04) 1px, transparent 1px), + linear-gradient(135deg, #08111d 0%, #0b1524 54%, #101426 100%); + background-size: 40px 40px, 40px 40px, auto; + color: var(--text-1); +} + +.tpl-beaver-replay .slide::before { + content: ""; + position: absolute; + inset: 0; + background: + linear-gradient(90deg, rgba(100, 227, 161, .1), transparent 22%, transparent 78%, rgba(124, 199, 255, .08)), + linear-gradient(180deg, rgba(217, 166, 255, .07), transparent 30%, transparent 82%, rgba(100, 227, 161, .05)); + opacity: .7; + pointer-events: none; + z-index: 0; +} + +.tpl-beaver-replay .slide > * { + position: relative; + z-index: 1; +} + +.tpl-beaver-replay .h1 { + font-size: 62px; + line-height: 1.06; + font-weight: 800; + letter-spacing: 0; + margin-bottom: 20px; + color: #fff; +} + +.tpl-beaver-replay .h2 { + font-size: 40px; + line-height: 1.12; + font-weight: 760; + letter-spacing: 0; + color: #fff; + margin-bottom: 16px; +} + +.tpl-beaver-replay h3, +.tpl-beaver-replay h4 { + color: #fff; + letter-spacing: 0; +} + +.tpl-beaver-replay .lede { + font-size: 19px; + line-height: 1.55; + color: var(--text-2); + max-width: 66ch; +} + +.tpl-beaver-replay .kicker { + color: var(--accent); + font-family: "JetBrains Mono", "IBM Plex Mono", monospace; + font-size: 13px; + font-weight: 700; + letter-spacing: 0; + text-transform: none; +} + +.tpl-beaver-replay .kicker::before { + content: "> "; +} + +.tpl-beaver-replay .mono { + font-family: "JetBrains Mono", "IBM Plex Mono", monospace; +} + +.tpl-beaver-replay .gradient-text { + background: var(--grad); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + color: transparent; +} + +.tpl-beaver-replay .deck-footer { + position: absolute; + left: 40px; + right: 40px; + bottom: 20px; + display: flex; + align-items: center; + justify-content: space-between; + color: var(--text-3); + font-family: "JetBrains Mono", "IBM Plex Mono", monospace; + letter-spacing: 0; +} + +.tpl-beaver-replay .card { + background: rgba(16, 27, 44, .92); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: none; +} + +.tpl-beaver-replay .card-accent { + border-top: 3px solid var(--accent); +} + +.tpl-beaver-replay .panel { + padding: 20px 22px; + background: rgba(8, 17, 29, .68); + border: 1px solid var(--border); + border-radius: 8px; +} + +.tpl-beaver-replay .tag, +.tpl-beaver-replay .pill { + display: inline-flex; + align-items: center; + min-height: 24px; + padding: 4px 10px; + border-radius: 6px; + background: rgba(19, 34, 53, .9); + border: 1px solid var(--border); + color: var(--text-2); + font-family: "JetBrains Mono", "IBM Plex Mono", monospace; + font-size: 12px; + letter-spacing: 0; +} + +.tpl-beaver-replay .tag.good { + color: var(--good); + border-color: rgba(100, 227, 161, .34); + background: rgba(100, 227, 161, .1); +} + +.tpl-beaver-replay .tag.warn { + color: var(--warn); + border-color: rgba(255, 209, 102, .34); + background: rgba(255, 209, 102, .1); +} + +.tpl-beaver-replay .tag.bad { + color: var(--bad); + border-color: rgba(255, 123, 123, .34); + background: rgba(255, 123, 123, .1); +} + +.tpl-beaver-replay .terminal { + background: #050a12; + border: 1px solid var(--border); + border-radius: 8px; + overflow: hidden; + box-shadow: var(--shadow); + font-family: "JetBrains Mono", "IBM Plex Mono", monospace; + font-size: 14px; + line-height: 1.62; +} + +.tpl-beaver-replay .terminal .bar { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 14px; + background: #0d1726; + border-bottom: 1px solid var(--border); + font-size: 12px; + color: var(--text-3); +} + +.tpl-beaver-replay .terminal .dot { + width: 10px; + height: 10px; + border-radius: 50%; + background: #ff7b7b; +} + +.tpl-beaver-replay .terminal .dot:nth-child(2) { + background: #ffd166; +} + +.tpl-beaver-replay .terminal .dot:nth-child(3) { + background: #64e3a1; +} + +.tpl-beaver-replay .terminal pre { + margin: 0; + padding: 20px 22px; + color: #dbeafe; + overflow: auto; + max-height: 420px; +} + +.tpl-beaver-replay .kw { color: #ff9f9f; } +.tpl-beaver-replay .fn { color: #d9a6ff; } +.tpl-beaver-replay .str { color: #9fe6ff; } +.tpl-beaver-replay .num { color: #7cc7ff; } +.tpl-beaver-replay .cmt { color: #6f879f; } + +.tpl-beaver-replay .speaker { + display: flex; + align-items: center; + gap: 14px; + margin-top: 22px; +} + +.tpl-beaver-replay .speaker .av { + width: 54px; + height: 54px; + border-radius: 50%; + background: var(--grad); +} + +.tpl-beaver-replay .speaker b { + display: block; + color: #fff; + font-size: 17px; +} + +.tpl-beaver-replay .speaker span { + color: var(--text-3); + font-size: 13px; + font-family: "JetBrains Mono", "IBM Plex Mono", monospace; +} + +.tpl-beaver-replay .agenda-row { + display: grid; + grid-template-columns: 58px 1fr 190px; + gap: 22px; + align-items: baseline; + padding: 15px 0; + border-bottom: 1px dashed var(--border); +} + +.tpl-beaver-replay .agenda-row .num { + color: var(--accent); + font-family: "JetBrains Mono", "IBM Plex Mono", monospace; +} + +.tpl-beaver-replay .agenda-row .t { + color: #fff; + font-size: 22px; + font-weight: 700; +} + +.tpl-beaver-replay .agenda-row .d { + color: var(--text-3); + font-size: 13px; + font-family: "JetBrains Mono", "IBM Plex Mono", monospace; +} + +.tpl-beaver-replay .flow { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 12px; + align-items: stretch; +} + +.tpl-beaver-replay .flow-step { + min-height: 120px; + padding: 16px; + border-radius: 8px; + background: rgba(16, 27, 44, .94); + border: 1px solid var(--border); +} + +.tpl-beaver-replay .flow-step .n { + display: inline-block; + margin-bottom: 10px; + color: var(--accent); + font-family: "JetBrains Mono", "IBM Plex Mono", monospace; + font-size: 13px; +} + +.tpl-beaver-replay .flow-step h4 { + margin: 0 0 8px; + font-size: 18px; +} + +.tpl-beaver-replay .flow-step p { + margin: 0; + color: var(--text-2); + font-size: 14px; + line-height: 1.5; +} + +.tpl-beaver-replay .split { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 26px; + align-items: start; +} + +.tpl-beaver-replay .compare { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 18px; +} + +.tpl-beaver-replay .compare .side { + min-height: 330px; + padding: 22px; + border: 1px solid var(--border); + border-radius: 8px; + background: rgba(8, 17, 29, .72); +} + +.tpl-beaver-replay .compare .side.candidate { + border-color: rgba(100, 227, 161, .4); + background: rgba(100, 227, 161, .08); +} + +.tpl-beaver-replay .metric-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 14px; +} + +.tpl-beaver-replay .metric { + padding: 18px; + border-radius: 8px; + border: 1px solid var(--border); + background: rgba(16, 27, 44, .9); + min-height: 104px; +} + +.tpl-beaver-replay .metric b { + display: block; + font-size: 23px; + line-height: 1.1; + color: #fff; +} + +.tpl-beaver-replay .metric span { + color: var(--text-3); + font-family: "JetBrains Mono", "IBM Plex Mono", monospace; + font-size: 12px; +} + +.tpl-beaver-replay .matrix { + display: grid; + grid-template-columns: 190px repeat(3, 1fr); + border: 1px solid var(--border); + border-radius: 8px; + overflow: hidden; +} + +.tpl-beaver-replay .matrix > div { + min-height: 76px; + padding: 14px; + border-right: 1px solid var(--border); + border-bottom: 1px solid var(--border); + background: rgba(16, 27, 44, .78); + font-size: 14px; +} + +.tpl-beaver-replay .matrix > div:nth-child(4n) { + border-right: 0; +} + +.tpl-beaver-replay .matrix .head { + min-height: 48px; + color: var(--accent-2); + background: rgba(124, 199, 255, .08); + font-family: "JetBrains Mono", "IBM Plex Mono", monospace; + font-size: 12px; +} + +.tpl-beaver-replay .matrix .rowhead { + color: #fff; + font-weight: 700; +} + +.tpl-beaver-replay .pipeline { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 18px; +} + +.tpl-beaver-replay .pipeline .phase { + min-height: 250px; + padding: 22px; + border-radius: 8px; + border: 1px solid var(--border); + background: rgba(16, 27, 44, .88); +} + +.tpl-beaver-replay .phase h3 { + font-size: 22px; + margin-bottom: 12px; +} + +.tpl-beaver-replay ul.clean { + list-style: none; + padding: 0; + margin: 0; +} + +.tpl-beaver-replay ul.clean li { + position: relative; + padding-left: 18px; + margin: 10px 0; + color: var(--text-2); + font-size: 15px; + line-height: 1.45; +} + +.tpl-beaver-replay ul.clean li::before { + content: ""; + position: absolute; + left: 0; + top: .65em; + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--accent); +} + +.tpl-beaver-replay .large-number { + font-family: "JetBrains Mono", "IBM Plex Mono", monospace; + font-size: 96px; + line-height: .9; + font-weight: 800; + color: var(--accent); +} + +.tpl-beaver-replay .source-line { + color: var(--text-3); + font-size: 12px; + font-family: "JetBrains Mono", "IBM Plex Mono", monospace; +} + +.tpl-beaver-replay .roadmap { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 14px; +} + +.tpl-beaver-replay .roadmap .item { + min-height: 150px; + padding: 18px; + background: rgba(16, 27, 44, .9); + border: 1px solid var(--border); + border-radius: 8px; +} + +.tpl-beaver-replay .roadmap .item b { + display: block; + margin-bottom: 10px; + color: #fff; + font-size: 18px; +} + +.tpl-beaver-replay .roadmap .item span { + color: var(--accent); + font-family: "JetBrains Mono", "IBM Plex Mono", monospace; + font-size: 12px; +} + +.tpl-beaver-replay .roadmap .item p { + color: var(--text-2); + font-size: 14px; + line-height: 1.5; +} + +.tpl-beaver-replay .center-mark { + display: inline-flex; + align-items: center; + justify-content: center; + width: 112px; + height: 112px; + border-radius: 50%; + border: 1px solid rgba(100, 227, 161, .38); + background: rgba(100, 227, 161, .08); + color: var(--accent); + font-family: "JetBrains Mono", "IBM Plex Mono", monospace; + font-size: 46px; + font-weight: 800; +}