Repository navigation
Expand file tree
/
Copy pathscript.js
More file actions
261 lines (244 loc) · 23.7 KB
/
Copy pathscript.js
File metadata and controls
261 lines (244 loc) · 23.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
const translations = {
ko: {
metaDescription: "기출 PDF를 분석해 새로운 시험지로 만드는 AI ExamGenerator의 공개 포트폴리오입니다.",
navProduct: "제품", navEngineering: "엔지니어링", navArchitecture: "아키텍처", navDocs: "기술 문서",
service: "서비스 보기 <span aria-hidden=\"true\">↗</span>",
heroTitle: "기출 한 장이<br><span>새로운 시험지가</span><br>되는 순간",
heroDescription: "수능 기출 PDF의 문항 구조를 분석하고, 난이도와 유형을 보존한 변형 문제를 생성해 검증 가능한 시험지로 완성하는 교육용 AI 서비스입니다.",
liveService: "실제 서비스 보기 <span aria-hidden=\"true\">↗</span>", howBuilt: "어떻게 만들었나요?",
heroPoint1: "PDF·OCR 분석", heroPoint2: "검증 중심 생성", heroPoint3: "PDF·Word 출력",
tagAnalysis: "문항 구조 분석", tagVerified: "검증 완료",
process1: "읽고", process2: "변형하고", process3: "검증하고", process4: "출력합니다",
process2Sub: "유형 + 난이도", process3Sub: "정답 + 중복",
filmTitle: "새로운 출제 경험을<br>영상으로 만나보세요.",
filmDescription: "PDF 분석부터 시험지 완성까지, 하나의 흐름으로.",
productTitle: "교사의 출제 흐름을<br>하나의 제품으로 연결했습니다.",
productDescription: "AI 호출 자체보다 중요한 것은 출제 의도 보존, 결과 검증, 실패 복구와 일관된 출력이었습니다.",
tab1Title: "PDF 분석", tab1Sub: "텍스트·이미지·문항 구조", tab2Title: "문제 변형", tab2Sub: "유형·난이도·조건 보존",
tab3Title: "다단계 검증", tab3Sub: "정답·중복·지문 훼손 검사", tab4Title: "시험지 출력", tab4Sub: "PDF·Word·도형 렌더링",
secure: "안전하게 처리됨",
engineeringTitle: "데모가 아니라,<br>운영 가능한 시스템을 만들었습니다.",
engineeringDescription: "실패가 발생하는 지점을 전제로 설계하고 비용·시간·품질을 함께 관리했습니다.",
eng1Title: "멱등 크레딧 원장", eng1Text: "작업 재시도와 중복 요청에서도 차감·환불이 한 번만 반영되도록 거래 단위로 보호합니다.",
eng2Title: "부분 실패 복구", eng2Text: "청크 생성, 검증, 보충 생성 단계를 분리해 일부 실패가 전체 시험지를 무너뜨리지 않게 합니다.",
eng3Title: "결정적 도형 출력", eng3Text: "AI가 도형 명세를 만들고 애플리케이션이 검증된 SVG를 그려 화면·PDF·Word 결과를 맞춥니다.",
eng4Title: "관측 가능한 AI 파이프라인", eng4Text: "역할별 지연, 재시도, 캐시 적중과 실패 유형을 기록해 품질과 비용을 근거로 조정합니다.",
eng5Title: "원자적 릴리스", eng5Text: "서버에서 빌드·테스트한 릴리스를 한 번에 전환하고, 준비 확인에 실패하면 자동으로 되돌립니다. 마이그레이션은 이전 코드와 함께 돌 수 있게 설계합니다.",
eng6Title: "복구 가능한 백업", eng6Text: "백업은 공개키로 암호화된 뒤에만 서버 밖으로 나가며 서버는 이를 풀 수 없습니다. 매달 자동 복구 리허설이 복원본을 운영 데이터와 대조합니다.",
eng7Title: "교체되는 세션 토큰", eng7Text: "리프레시 토큰은 쓸 때마다 교체되고, 이미 쓴 토큰이 다시 오면 세션 전체를 끝냅니다. 일시적인 인프라 오류로는 로그아웃시키지 않습니다.",
eng8Title: "측정하는 AI 품질", eng8Text: "기출 묶음으로 추출 재현율, 검증 판정 분포, 인쇄 잔재, 정답·해설 일치를 측정합니다. 평가용 기출은 저작권 때문에 저장소에 올리지 않습니다.",
architectureTitle: "느린 AI 작업을 웹 요청에서 분리했습니다.",
architectureDescription: "사용자 요청은 빠르게 접수하고, 무거운 분석·생성·문서 출력은 비동기 워커가 처리합니다.",
caseTitle: "“수학 문제의 그림이<br>생성할 때마다 달라진다면?”",
caseText: "생성 모델이 직접 SVG 문자열을 쓰게 두는 대신, 제한된 JSON 도형 명세를 출력하게 했습니다. 프론트엔드의 안전한 파서와 렌더러가 같은 명세를 화면·PDF·Word에 일관되게 적용합니다.",
caseLink: "설계 자세히 보기 <span>→</span>",
scopeTitle: "보여줄 것은 선명하게,<br>지켜야 할 것은 안전하게.", includedTitle: "<span>✓</span> 이 저장소에 포함", excludedTitle: "<span>—</span> 의도적으로 비공개",
included1: "제품 경험과 인터랙티브 쇼케이스", included2: "고수준 시스템 아키텍처", included3: "핵심 엔지니어링 의사결정", included4: "공개 가능한 기술 스택과 화면",
excluded1: "운영 백엔드 및 모델 프롬프트", excluded2: "결제·인증·관리자 구현", excluded3: "서버 주소와 배포 자격 증명", excluded4: "사용자 데이터와 운영 로그",
closingTitle: "문제를 만드는 AI를 넘어,<br>출제 과정을 완성하는 제품.", closingService: "서비스 방문하기 ↗", github: "GitHub 프로필", lastUpdated: "마지막 업데이트: 2026-10-01",
menuOpen: "메뉴 열기", menuClose: "메뉴 닫기", soundOn: "영상 소리 켜기", soundOff: "영상 소리 끄기", soundOnText: "Sound on", soundOffText: "Sound off",
languageLabel: "언어 선택", navLabel: "주요 메뉴", homeLabel: "AI ExamGenerator 홈", heroVisualLabel: "AI ExamGenerator 제품 화면", heroImageAlt: "기출 PDF 분석부터 시험지 생성까지의 AI ExamGenerator 제품 화면", processLabel: "제품 처리 과정", filmLabel: "AI ExamGenerator 브랜드 영상", featureLabel: "제품 기능", architectureLabel: "시스템 아키텍처 다이어그램", stackLabel: "기술 스택", figureLabel: "결정적 함수 그래프 예시", figureImageLabel: "함수 그래프 SVG",
},
en: {
metaDescription: "AI ExamGenerator turns past-exam PDFs into verified, ready-to-use assessments.",
navProduct: "Product", navEngineering: "Engineering", navArchitecture: "Architecture", navDocs: "Tech notes",
service: "View live service <span aria-hidden=\"true\">↗</span>",
heroTitle: "One source PDF.<br><span>A whole new exam.</span><br>Ready to use.",
heroDescription: "An AI-assisted assessment platform that analyzes Korean SAT PDFs, preserves question intent and difficulty, and produces verified exam papers.",
liveService: "View live service <span aria-hidden=\"true\">↗</span>", howBuilt: "How was it built?",
heroPoint1: "PDF + OCR analysis", heroPoint2: "Verification-first generation", heroPoint3: "PDF + Word export",
tagAnalysis: "Structure analyzed", tagVerified: "Verified",
process1: "Read", process2: "Transform", process3: "Verify", process4: "Publish",
process2Sub: "Type + difficulty", process3Sub: "Answer + similarity",
filmTitle: "See a new way<br>to build assessments.",
filmDescription: "From source-PDF analysis to a finished exam, in one continuous workflow.",
productTitle: "The teacher's workflow,<br>connected in one product.",
productDescription: "The hard part was never a single AI call. It was intent preservation, verification, recovery, and consistent output.",
tab1Title: "PDF analysis", tab1Sub: "Text, figures, question structure", tab2Title: "Transformation", tab2Sub: "Type, difficulty, constraints",
tab3Title: "Multi-stage verification", tab3Sub: "Answers, similarity, passage integrity", tab4Title: "Exam publishing", tab4Sub: "PDF, Word, figure rendering",
secure: "Processed securely",
engineeringTitle: "Not a demo.<br>A system built to operate.",
engineeringDescription: "The pipeline assumes failure and manages quality, latency, and cost as first-class constraints.",
eng1Title: "Idempotent credit ledger", eng1Text: "Transaction boundaries ensure retries and duplicate requests can charge or refund a job only once.",
eng2Title: "Partial-failure recovery", eng2Text: "Chunk generation, verification, repair, and supplemental generation keep one failure from collapsing the full exam.",
eng3Title: "Deterministic figures", eng3Text: "AI emits a constrained figure specification; the application validates and renders consistent SVG for web, PDF, and Word.",
eng4Title: "Observable AI pipeline", eng4Text: "Role-level latency, retries, cache hits, and failure classes guide quality and cost decisions.",
eng5Title: "Atomic releases", eng5Text: "Releases are built and tested on the host, switched in one step, and rolled back automatically if the readiness check fails. Migrations stay compatible with the previous code.",
eng6Title: "Restorable backups", eng6Text: "Backups leave the server only after public-key encryption the server itself cannot reverse. A monthly automated rehearsal restores one and checks it against production.",
eng7Title: "Rotating session tokens", eng7Text: "Refresh tokens rotate on every use, and a replayed token ends the whole session. Transient infrastructure errors never log users out.",
eng8Title: "Measured AI quality", eng8Text: "Extraction recall, verification mix, print debris, and answer–explanation consistency are measured on past papers that are never committed, for copyright reasons.",
architectureTitle: "Long-running AI work stays outside the web request.",
architectureDescription: "Requests are accepted quickly while asynchronous workers handle extraction, generation, verification, and document rendering.",
caseTitle: "“What if a math figure changes<br>every time it is generated?”",
caseText: "Instead of letting a model write arbitrary SVG, the model produces a constrained JSON figure specification. A safe parser and renderer apply the same specification across the UI, PDF, and Word output.",
caseLink: "Read the design note <span>→</span>",
scopeTitle: "Show the work clearly.<br>Keep production protected.", includedTitle: "<span>✓</span> Included here", excludedTitle: "<span>—</span> Intentionally private",
included1: "Product experience and interactive showcase", included2: "High-level system architecture", included3: "Selected engineering decisions", included4: "Public-safe stack and product visuals",
excluded1: "Production backend and model prompts", excluded2: "Billing, authentication, and admin internals", excluded3: "Server addresses and deployment credentials", excluded4: "User data and production logs",
closingTitle: "More than AI-generated questions.<br>A complete assessment workflow.", closingService: "Visit live service ↗", github: "GitHub profile", lastUpdated: "Last updated: 2026-10-01",
menuOpen: "Open menu", menuClose: "Close menu", soundOn: "Turn video sound on", soundOff: "Turn video sound off", soundOnText: "Sound on", soundOffText: "Sound off",
languageLabel: "Language", navLabel: "Primary navigation", homeLabel: "AI ExamGenerator home", heroVisualLabel: "AI ExamGenerator product overview", heroImageAlt: "AI ExamGenerator workflow from source-PDF analysis to a finished exam", processLabel: "Product workflow", filmLabel: "AI ExamGenerator brand film", featureLabel: "Product features", architectureLabel: "System architecture diagram", stackLabel: "Technology stack", figureLabel: "Deterministic function-plot example", figureImageLabel: "Function plot rendered as SVG",
},
};
const bindings = [
[".site-header nav a[href='#product']", "navProduct"], [".site-header nav a[href='#engineering']", "navEngineering"], [".site-header nav a[href='#architecture']", "navArchitecture"], [".site-header nav a[href^='docs/ARCHITECTURE']", "navDocs"],
[".header-cta", "service", true], [".hero h1", "heroTitle", true], [".hero-description", "heroDescription"], [".hero .primary-button", "liveService", true], [".hero .secondary-button", "howBuilt"],
[".hero-points li:nth-child(1)", "heroPoint1"], [".hero-points li:nth-child(2)", "heroPoint2"], [".hero-points li:nth-child(3)", "heroPoint3"], [".tag-analysis", "tagAnalysis"], [".tag-verified", "tagVerified"],
[".process-strip div:nth-of-type(1) strong", "process1"], [".process-strip div:nth-of-type(2) strong", "process2"], [".process-strip div:nth-of-type(3) strong", "process3"], [".process-strip div:nth-of-type(4) strong", "process4"], [".process-strip div:nth-of-type(2) small", "process2Sub"], [".process-strip div:nth-of-type(3) small", "process3Sub"],
[".brand-film-copy h2", "filmTitle", true], [".brand-film-copy>span", "filmDescription"],
[".product-section .section-heading h2", "productTitle", true], [".product-section .section-heading>p:last-child", "productDescription"],
[".feature-tab[data-feature='analyze'] strong", "tab1Title"], [".feature-tab[data-feature='analyze'] small", "tab1Sub"], [".feature-tab[data-feature='transform'] strong", "tab2Title"], [".feature-tab[data-feature='transform'] small", "tab2Sub"], [".feature-tab[data-feature='verify'] strong", "tab3Title"], [".feature-tab[data-feature='verify'] small", "tab3Sub"], [".feature-tab[data-feature='publish'] strong", "tab4Title"], [".feature-tab[data-feature='publish'] small", "tab4Sub"], [".secure-pill", "secure"],
[".engineering-section .section-heading h2", "engineeringTitle", true], [".engineering-section .section-heading>p:last-child", "engineeringDescription"],
[".engineering-grid article:nth-child(1) h3", "eng1Title"], [".engineering-grid article:nth-child(1) p", "eng1Text"], [".engineering-grid article:nth-child(2) h3", "eng2Title"], [".engineering-grid article:nth-child(2) p", "eng2Text"], [".engineering-grid article:nth-child(3) h3", "eng3Title"], [".engineering-grid article:nth-child(3) p", "eng3Text"], [".engineering-grid article:nth-child(4) h3", "eng4Title"], [".engineering-grid article:nth-child(4) p", "eng4Text"],
[".engineering-grid article:nth-child(5) h3", "eng5Title"], [".engineering-grid article:nth-child(5) p", "eng5Text"], [".engineering-grid article:nth-child(6) h3", "eng6Title"], [".engineering-grid article:nth-child(6) p", "eng6Text"], [".engineering-grid article:nth-child(7) h3", "eng7Title"], [".engineering-grid article:nth-child(7) p", "eng7Text"], [".engineering-grid article:nth-child(8) h3", "eng8Title"], [".engineering-grid article:nth-child(8) p", "eng8Text"],
[".architecture-section .section-heading h2", "architectureTitle"], [".architecture-section .section-heading>p:last-child", "architectureDescription"],
[".case-copy h2", "caseTitle", true], [".case-copy>p:not(.eyebrow)", "caseText"], [".case-copy>a", "caseLink", true],
[".public-scope h2", "scopeTitle", true], [".included h3", "includedTitle", true], [".excluded h3", "excludedTitle", true],
[".included li:nth-child(1)", "included1"], [".included li:nth-child(2)", "included2"], [".included li:nth-child(3)", "included3"], [".included li:nth-child(4)", "included4"], [".excluded li:nth-child(1)", "excluded1"], [".excluded li:nth-child(2)", "excluded2"], [".excluded li:nth-child(3)", "excluded3"], [".excluded li:nth-child(4)", "excluded4"],
[".closing-content h2", "closingTitle", true], [".closing-content .white-button", "closingService"], [".closing-content .ghost-button", "github"], ["footer .footer-updated", "lastUpdated"],
];
const featureContent = {
ko: {
analyze: { kicker: "PDF ANALYSIS", title: "문항을 읽는 것부터<br>정확해야 하니까.", status: "분석 중", cardTitle: "수학 · 미적분", checks: ["문항 경계 감지", "수식 구조 보존", "선택지 5개 확인"], progress: "42%" },
transform: { kicker: "CONTROLLED TRANSFORMATION", title: "달라져도,<br>출제 의도는 남도록.", status: "변형 완료", cardTitle: "변형 A · 난이도 유지", checks: ["유형 일치", "조건 수치 변형", "선택지 중복 없음"], progress: "68%" },
verify: { kicker: "MULTI-STAGE VERIFICATION", title: "생성보다 더 오래,<br>결과를 의심합니다.", status: "검증 완료", cardTitle: "교차 검증 · Q17", checks: ["정답 교차 확인", "지문 훼손 검사", "위험 문항 재검토"], progress: "86%" },
publish: { kicker: "DETERMINISTIC OUTPUT", title: "화면에서 본 그대로,<br>시험지까지 완성합니다.", status: "출력 준비", cardTitle: "제1회 수학 영역", checks: ["2단 지면 구성", "수식·도형 렌더링", "PDF·Word 준비"], progress: "100%" },
},
en: {
analyze: { kicker: "PDF ANALYSIS", title: "Reliable output starts<br>with accurate reading.", status: "Analyzing", cardTitle: "Math · Calculus", checks: ["Question boundaries detected", "Formula structure preserved", "Five choices confirmed"], progress: "42%" },
transform: { kicker: "CONTROLLED TRANSFORMATION", title: "Change the question,<br>preserve its intent.", status: "Transformed", cardTitle: "Variant A · Difficulty held", checks: ["Question type matched", "Constraints transformed", "No duplicate choices"], progress: "68%" },
verify: { kicker: "MULTI-STAGE VERIFICATION", title: "We spend longer checking<br>than generating.", status: "Verified", cardTitle: "Cross-check · Q17", checks: ["Answer cross-checked", "Passage integrity checked", "Risk item reviewed"], progress: "86%" },
publish: { kicker: "DETERMINISTIC OUTPUT", title: "What you review is what<br>the final exam delivers.", status: "Ready", cardTitle: "Math Assessment · Set 01", checks: ["Two-column layout", "Math and figures rendered", "PDF and Word ready"], progress: "100%" },
},
};
let currentLanguage = localStorage.getItem("portfolio-language") === "en" ? "en" : "ko";
const tabs = document.querySelectorAll(".feature-tab");
const title = document.querySelector("#demo-title");
const kicker = document.querySelector("#demo-kicker");
const status = document.querySelector("#demo-status");
const cardTitle = document.querySelector("#demo-card-title");
const checks = document.querySelector("#demo-checks");
const progress = document.querySelector("#demo-progress-bar");
const menuButton = document.querySelector(".menu-button");
const nav = document.querySelector("header nav");
const video = document.querySelector(".brand-film-video");
const soundToggle = document.querySelector(".sound-toggle");
function activeFeature() {
return document.querySelector(".feature-tab.active")?.dataset.feature || "analyze";
}
function renderFeature(feature, animate = false) {
const content = featureContent[currentLanguage][feature];
const render = () => {
kicker.textContent = content.kicker;
title.innerHTML = content.title;
status.textContent = content.status;
cardTitle.textContent = content.cardTitle;
checks.innerHTML = content.checks.map((item) => `<li><span>✓</span> ${item}</li>`).join("");
progress.style.width = content.progress;
document.querySelector(".product-demo").classList.remove("switching");
};
if (!animate) return render();
document.querySelector(".product-demo").classList.add("switching");
window.setTimeout(render, 160);
}
function updateControlLabels() {
const copy = translations[currentLanguage];
menuButton.setAttribute("aria-label", document.body.classList.contains("menu-open") ? copy.menuClose : copy.menuOpen);
soundToggle.setAttribute("aria-label", video.muted ? copy.soundOn : copy.soundOff);
soundToggle.querySelector("b").textContent = video.muted ? copy.soundOnText : copy.soundOffText;
}
function applyLanguage(language) {
currentLanguage = language;
const copy = translations[language];
document.documentElement.lang = language;
document.title = language === "ko" ? "AI ExamGenerator — 제품·엔지니어링 포트폴리오" : "AI ExamGenerator — Product & Engineering Portfolio";
document.querySelector('meta[name="description"]').setAttribute("content", copy.metaDescription);
document.querySelector('meta[property="og:description"]').setAttribute("content", copy.metaDescription);
bindings.forEach(([selector, key, html]) => {
const element = document.querySelector(selector);
if (!element) return;
if (html) element.innerHTML = copy[key]; else element.textContent = copy[key];
});
document.querySelector(".language-switch").setAttribute("aria-label", copy.languageLabel);
nav.setAttribute("aria-label", copy.navLabel);
document.querySelector(".site-header>.brand").setAttribute("aria-label", copy.homeLabel);
document.querySelector(".hero-visual").setAttribute("aria-label", copy.heroVisualLabel);
document.querySelector(".hero-visual img").setAttribute("alt", copy.heroImageAlt);
document.querySelector(".process-strip").setAttribute("aria-label", copy.processLabel);
document.querySelector(".brand-film").setAttribute("aria-label", copy.filmLabel);
document.querySelector(".feature-nav").setAttribute("aria-label", copy.featureLabel);
document.querySelector(".architecture-map").setAttribute("aria-label", copy.architectureLabel);
document.querySelector(".stack-row").setAttribute("aria-label", copy.stackLabel);
document.querySelector(".figure-lab").setAttribute("aria-label", copy.figureLabel);
document.querySelector(".figure-lab svg").setAttribute("aria-label", copy.figureImageLabel);
document.querySelector(".site-header nav a[href^='docs/ARCHITECTURE']").setAttribute("href", language === "ko" ? "docs/ARCHITECTURE.md" : "docs/ARCHITECTURE_EN.md");
document.querySelector(".case-copy>a").setAttribute("href", language === "ko" ? "docs/ARCHITECTURE.md#결정적-도형-렌더링" : "docs/ARCHITECTURE_EN.md#4-deterministic-figure-rendering");
document.querySelector("footer a[href^='docs/ARCHITECTURE']").setAttribute("href", language === "ko" ? "docs/ARCHITECTURE.md" : "docs/ARCHITECTURE_EN.md");
document.querySelector("footer a[href^='docs/PUBLIC_SCOPE']").setAttribute("href", language === "ko" ? "docs/PUBLIC_SCOPE.md" : "docs/PUBLIC_SCOPE_EN.md");
document.querySelectorAll("[data-language]").forEach((button) => {
const active = button.dataset.language === language;
button.classList.toggle("active", active);
button.setAttribute("aria-pressed", String(active));
});
localStorage.setItem("portfolio-language", language);
renderFeature(activeFeature());
updateControlLabels();
}
tabs.forEach((tab) => {
tab.addEventListener("click", () => {
tabs.forEach((item) => {
item.classList.toggle("active", item === tab);
item.setAttribute("aria-selected", String(item === tab));
});
renderFeature(tab.dataset.feature, true);
});
});
document.querySelectorAll("[data-language]").forEach((button) => {
button.addEventListener("click", () => applyLanguage(button.dataset.language));
});
menuButton.addEventListener("click", () => {
const open = document.body.classList.toggle("menu-open");
menuButton.setAttribute("aria-expanded", String(open));
updateControlLabels();
});
nav.addEventListener("click", () => {
document.body.classList.remove("menu-open");
menuButton.setAttribute("aria-expanded", "false");
updateControlLabels();
});
soundToggle.addEventListener("click", () => {
video.muted = !video.muted;
soundToggle.setAttribute("aria-pressed", String(!video.muted));
updateControlLabels();
if (video.paused) video.play().catch(() => undefined);
});
const videoObserver = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && entry.intersectionRatio >= 0.35 && !document.hidden) {
video.play().catch(() => undefined);
} else {
video.pause();
}
},
{ threshold: [0, 0.35, 0.7] },
);
videoObserver.observe(video);
document.addEventListener("visibilitychange", () => {
if (document.hidden) video.pause();
});
const revealObserver = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("revealed");
revealObserver.unobserve(entry.target);
}
});
},
{ threshold: 0.12 },
);
document.querySelectorAll("section, .engineering-grid article").forEach((item) => {
item.classList.add("reveal-ready");
revealObserver.observe(item);
});
applyLanguage(currentLanguage);