Commit 330212cd authored by tao355667's avatar tao355667

feat: expand homepage and fix admin captcha

parent 63264608
......@@ -8,6 +8,9 @@ let editingStatus = "new-unsaved";
let dirty = false;
let previewTimer = null;
let previewRequest = 0;
let captchaToken = "";
let captchaObjectUrl = "";
let captchaRequest = 0;
async function api(path, options = {}) {
const endpoint = path.startsWith("/") ? path : `/${path}`;
......@@ -24,10 +27,26 @@ async function api(path, options = {}) {
const show = (selector) => $(selector).classList.remove("hidden");
const hide = (selector) => $(selector).classList.add("hidden");
const refreshCaptcha = () => {
$("#captcha-image").src = `${API_BASE}/captcha?t=${Date.now()}`;
async function refreshCaptcha() {
const request = ++captchaRequest;
captchaToken = "";
$("#captcha").value = "";
};
$("#captcha-image").removeAttribute("src");
try {
const response = await fetch(`${API_BASE}/captcha?t=${Date.now()}`, { cache: "no-store" });
if (!response.ok) throw new Error(`验证码加载失败(${response.status})`);
const token = response.headers.get("X-Captcha-Token") || "";
const blob = await response.blob();
if (request !== captchaRequest) return;
if (!token) throw new Error("验证码令牌缺失");
if (captchaObjectUrl) URL.revokeObjectURL(captchaObjectUrl);
captchaObjectUrl = URL.createObjectURL(blob);
captchaToken = token;
$("#captcha-image").src = captchaObjectUrl;
} catch (error) {
if (request === captchaRequest) $("#login-error").textContent = error.message;
}
}
function showLogin() {
show("#login");
......@@ -52,7 +71,7 @@ $("#login-form").addEventListener("submit", async (event) => {
event.preventDefault();
$("#login-error").textContent = "";
try {
await api("/session", { method: "POST", body: { password: $("#password").value, captcha: $("#captcha").value } });
await api("/session", { method: "POST", body: { password: $("#password").value, captcha: $("#captcha").value, captchaToken } });
$("#password").value = "";
$("#captcha").value = "";
hide("#login"); show("#app");
......
......@@ -35,8 +35,8 @@ input, textarea, select { width: 100%; padding: 11px 13px; color: var(--text); b
input:focus, textarea:focus, select:focus { outline: 2px solid rgba(20,115,230,.14); border-color: var(--primary); }
.topbar { min-height: 68px; padding: 12px 28px; position: sticky; top: 0; z-index: 10; display: flex; align-items: center; justify-content: space-between; gap: 22px; color: white; background: var(--primary); }
.topbar > div:first-child { display: flex; align-items: center; gap: 13px; }
.topbar strong { color: var(--primary); font-size: 19px; }
.topbar span { font-size: 13px; opacity: .76; }
.topbar strong { color: white; font-size: 19px; }
.topbar span { color: white; font-size: 13px; opacity: .76; }
.top-actions { display: flex; align-items: center; gap: 9px; }
.topbar .ghost { color: white; border-color: rgba(255,255,255,.24); }
.pending-badge { padding: 5px 10px; color: #cfe2ff; background: rgba(20,115,230,.12); border: 1px solid rgba(20,115,230,.3); border-radius: 999px; }
......
export const site = {
seo: {
title: "启优学一对一|小学初中高中在线个性化辅导",
description: "启优学一对一面向小学、初中和高中学生,通过学情诊断、教师匹配、在线一对一授课、课后反馈与阶段复盘,制定有针对性的学习方案。",
description: "启优学一对一面向小学、初中和高中学生提供在线个性化辅导,通过学情诊断、教师匹配、专属学习方案、课堂反馈和阶段复盘,帮助学生针对性巩固基础、改善学习方法。",
keywords: ["启优学", "启优学一对一", "在线一对一辅导", "小学一对一", "初中一对一", "高中一对一", "个性化辅导"],
imageAlt: "启优学在线一对一课堂",
},
......@@ -22,6 +22,7 @@ export const site = {
{ value: "85%+", label: "来自 985 / 211 或重点师范院校" },
{ value: "20万+", label: "累计服务学员" },
],
scenarios: ["基础夯实", "同步校内", "偏科补弱", "阶段复习", "升学冲刺", "拔高培优"],
},
services: [
{
......@@ -77,14 +78,17 @@ export const site = {
vision: "启发引导,因材施教;不只关注一次结果,更陪伴学生建立可以持续使用的学习能力。",
},
workflow: [
{ number: "01", title: "沟通", desc: "了解年级、教材、进度、近期试卷与当前目标。" },
{ number: "02", title: "诊断", desc: "区分知识缺口、方法问题和学习习惯问题。" },
{ number: "03", title: "匹配", desc: "综合学段、学科、基础与沟通特点匹配教师。" },
{ number: "04", title: "复盘", desc: "根据课堂与练习反馈持续调整后续安排。" },
{ number: "01", title: "需求沟通", desc: "了解年级、地区、教材、校内进度、近期试卷与阶段目标。" },
{ number: "02", title: "三维学情诊断", desc: "结合试卷、错题与学习进度,区分知识缺口、方法和习惯问题。" },
{ number: "03", title: "教师匹配", desc: "综合学段、学科、教材版本、当前基础与沟通特点匹配固定教师。" },
{ number: "04", title: "互动授课", desc: "通过提问、作答、即时反馈与难度调整,检查学生是否真正理解。" },
{ number: "05", title: "课后反馈", desc: "记录本节内容、掌握情况、仍需巩固的问题与下一次安排。" },
{ number: "06", title: "阶段复盘", desc: "根据课堂、练习与阶段表现持续调整后续学习方案。" },
],
faq: [
{ q: "启优学一对一是什么?", a: "启优学一对一是面向小学、初中、高中学生的在线一对一辅导品牌,主要提供小初高校内学科个性化辅导服务。课程以学情诊断为起点,根据年级、学科基础、教材版本、薄弱点和学习目标匹配教师。" },
{ q: "主要提供哪些课程?", a: "覆盖基础夯实、同步校内、偏科补弱、阶段复习、升学冲刺和拔高培优等学习场景。课程根据学生实际学情制定,不是统一模板课。" },
{ q: "适合哪些学生?", a: "适合存在单科短板、偏科拖分、基础断层、学习习惯不稳定、校内进度跟不上、考试复习效率较低,或需要升学冲刺与拔高培优的小初高学生。正式上课前会先沟通学情,再判断是否适合一对一辅导。" },
{ q: "覆盖哪些年级和科目?", a: "覆盖小学1-6年级、初中1-3年级和高中1-3年级,支持语文、数学、英语、物理、化学、生物、地理、道德与法治等常见校内学科。具体开课情况以咨询确认为准。" },
{ q: "可以只辅导一科吗?", a: "可以,尤其适合单科拖分、偏科明显或某一学科基础断层的学生。平台会先判断具体知识漏洞,再匹配对应学科教师,不强制多科打包。" },
{ q: "如何匹配教师?", a: "根据学生年级、学科、教材版本、当前基础、学习目标和沟通特点进行匹配。匹配成功后通常安排固定教师长期授课;如适配度明显不合适,可联系课程服务人员协助评估调整。" },
......
......@@ -117,8 +117,8 @@ function captchaMac(code: unknown, expiresAt: number): string {
.digest("hex");
}
function captchaValid(request: Request, answer: unknown): boolean {
const value = cookies(request).cms_captcha;
function captchaValid(request: Request, answer: unknown, token?: unknown): boolean {
const value = String(token || cookies(request).cms_captcha || "");
if (!value || !answer) return false;
const separator = value.lastIndexOf(".");
const expiresAt = Number(value.slice(0, separator));
......@@ -208,11 +208,13 @@ export async function handleCmsApi(request: Request, routeValue: string, clientA
if (route === "captcha" && method === "GET") {
const code = randomCaptcha();
const expiresAt = Date.now() + CAPTCHA_TTL;
const token = `${expiresAt}.${captchaMac(code, expiresAt)}`;
return new Response(captchaSvg(code), {
headers: {
"Cache-Control": "no-store",
"Content-Type": "image/svg+xml; charset=utf-8",
"Set-Cookie": cookieHeader(request, "cms_captcha", `${expiresAt}.${captchaMac(code, expiresAt)}`, CAPTCHA_TTL / 1000),
"Set-Cookie": cookieHeader(request, "cms_captcha", token, CAPTCHA_TTL / 1000),
"X-Captcha-Token": token,
},
});
}
......@@ -224,7 +226,7 @@ export async function handleCmsApi(request: Request, routeValue: string, clientA
if (blockedFor) return json({ error: `尝试过于频繁,请 ${Math.ceil(blockedFor / 60)} 分钟后再试` }, 429);
const body = await bodyOf(request);
const clearCaptcha = cookieHeader(request, "cms_captcha", "", 0);
if (!captchaValid(request, body.captcha)) {
if (!captchaValid(request, body.captcha, body.captchaToken)) {
return json({ error: "验证码错误或已过期" }, 400, { "Set-Cookie": clearCaptcha });
}
if (!await verifyPassword(body.password)) {
......
......@@ -18,24 +18,24 @@ const subjects = [
{ icon: "chemistry", tone: "green", name: "化学实验", desc: "发现元素世界,理解物质变化" },
{ icon: "history", tone: "purple", name: "历史人文", desc: "穿越时空长河,传承文明智慧" },
] as const;
const jsonLd = [{ "@context": "https://schema.org", "@type": "Service", name: "启优学在线一对一个性化辅导", provider: { "@id": new URL("/#organization", Astro.site).href }, areaServed: { "@type": "Country", name: "中国" }, audience: { "@type": "EducationalAudience", educationalRole: "student" }, description: site.home.subhead }, { "@context": "https://schema.org", "@type": "FAQPage", mainEntity: site.faq.slice(0, 4).map((item) => ({ "@type": "Question", name: item.q, acceptedAnswer: { "@type": "Answer", text: item.a } })) }];
const jsonLd = [{ "@context": "https://schema.org", "@type": "Service", name: "启优学在线一对一个性化辅导", provider: { "@id": new URL("/#organization", Astro.site).href }, areaServed: { "@type": "Country", name: "中国" }, audience: { "@type": "EducationalAudience", educationalRole: "student" }, description: site.seo.description }, { "@context": "https://schema.org", "@type": "FAQPage", mainEntity: site.faq.slice(0, 5).map((item) => ({ "@type": "Question", name: item.q, acceptedAnswer: { "@type": "Answer", text: item.a } })) }];
---
<Base title={site.seo.title} jsonLd={jsonLd}><Header /><main id="main-content">
<section class="home-hero"><div class="container home-hero__grid"><div class="home-hero__copy" data-reveal><div class="hero-label"><strong>小初高在线辅导</strong></div><h1><span>优质师资直达,个性化辅导看得见</span><em>打破地域壁垒,让优质教育触手可及</em></h1><p>{site.home.subhead}</p><div class="hero__actions"><a class="button" href="/contact/">免费领取学情分析</a><a class="button button--outline" href="#workflow">了解学习流程</a></div><div class="hero__proof"><span>固定教师</span><span>互动课堂</span><span>课程回放</span><span>阶段复盘</span></div></div><div class="home-hero__visual" data-reveal><div class="image-frame"><Image src={heroImage} widths={[580, 760, 980]} sizes="(max-width: 760px) 100vw, 52vw" alt="启优学老师与学生在线一对一互动学习" format="webp" loading="eager" fetchpriority="high" /></div></div></div><div class="container stage-ruler" aria-label="覆盖学段"><span>小学 1-6 年级</span><i></i><span>初中 1-3 年级</span><i></i><span>高中 1-3 年级</span></div></section>
<section class="subject-overview section-pad"><div class="container"><div class="subject-overview__head" data-reveal><p class="subject-overview__label">启优学一对一在线辅导</p><h2>小学、初中、高中<br />个性化学习支持</h2><p class="subject-overview__lead">通过学情诊断、教师匹配与阶段复盘,为学生制定有针对性的在线一对一学习方案。</p><p class="subject-overview__intro">启优学一对一面向小学、初中和高中学生,提供学情诊断、教师匹配、专属学习方案、课堂过程反馈和课后学习报告,帮助家长清晰了解孩子的学习进展。</p></div><div class="subject-overview__grid">{subjects.map((item) => <article class={`subject-card subject-card--${item.tone}`} data-reveal><div class="subject-card__icon"><SubjectIcon name={item.icon} /></div><div><h3>{item.name}</h3><p>{item.desc}</p></div></article>)}</div></div></section>
<section class="subject-overview section-pad"><div class="container"><div class="subject-overview__head" data-reveal><p class="subject-overview__label">启优学一对一在线辅导</p><h2>小学、初中、高中<br />个性化学习支持</h2><p class="subject-overview__lead">通过学情诊断、教师匹配与阶段复盘,为学生制定有针对性的在线一对一学习方案。</p><p class="subject-overview__intro">启优学一对一面向小学、初中和高中学生,提供学情诊断、教师匹配、专属学习方案、课堂过程反馈和课后学习报告,帮助家长清晰了解孩子的学习进展。除下列学科外,还支持生物、地理、道德与法治等常见校内科目,具体开课情况以咨询确认为准。</p></div><div class="subject-overview__grid">{subjects.map((item) => <article class={`subject-card subject-card--${item.tone}`} data-reveal><div class="subject-card__icon"><SubjectIcon name={item.icon} /></div><div><h3>{item.name}</h3><p>{item.desc}</p></div></article>)}</div></div></section>
<section class="stat-band"><div class="container stat-grid">{site.home.stats.map((stat) => <div data-reveal><strong>{stat.value}</strong><p>{stat.label}</p></div>)}</div><p class="data-note">师资数据来源于启优学一对一教务系统内部统计,截至 2026 年 7 月;相关数据会随师资及业务情况动态更新。</p></section>
<section id="services" class="section-pad section-blue"><div class="container"><div class="section-intro-row"><SectionHeading title="从当前最需要解决的问题开始" description="课程不按统一模板推进。先看学生现在在哪里,再决定近期先做什么。" /><a class="text-link" href="/services/">查看全部学习需求</a></div><div class="need-grid">{site.services.map((item) => <a class="need-card" href={`/services/${item.slug}/`} data-reveal><h3>{item.name}</h3><p>{item.intro}</p><strong>了解适配方式</strong></a>)}</div></div></section>
<section id="services" class="section-pad section-blue"><div class="container"><div class="section-intro-row"><SectionHeading title="从当前最需要解决的问题开始" description="课程不按统一模板推进。先看学生现在在哪里,再决定近期先做什么。" /><a class="text-link" href="/services/">查看全部学习需求</a></div><div class="need-grid">{site.services.map((item) => <a class="need-card" href={`/services/${item.slug}/`} data-reveal><h3>{item.name}</h3><p>{item.intro}</p><strong>了解适配方式</strong></a>)}</div><div class="need-scope" data-reveal><strong>覆盖学习场景</strong><div>{site.home.scenarios.map((item) => <span>{item}</span>)}</div><p>可选择单科辅导,也可在学情诊断后规划多科分科辅导;不强制多科打包。</p></div></div></section>
<section id="workflow" class="section-pad section-white"><div class="container learning-map"><div class="learning-map__copy"><SectionHeading title="学习不是一条直线,而是一轮轮看见问题、解决问题" description="启优学把一次辅导拆成四个可理解的环节,家长知道为什么这样安排,学生知道下一步做什么。" /><a class="button button--outline" href="/methodology/">查看完整教学服务</a></div><div class="learning-map__steps">{site.workflow.map((item) => <article data-reveal><div><h3>{item.title}</h3><p>{item.desc}</p></div></article>)}</div></div></section>
<section id="workflow" class="section-pad section-white"><div class="container learning-map"><div class="learning-map__copy"><SectionHeading title="六项课程服务环节" description="从诊断到复盘,每一步都有清楚的任务和可核验标准,家长知道为什么这样安排,学生知道下一步做什么。" /><a class="button button--outline" href="/methodology/">查看完整教学服务</a></div><div class="learning-map__steps">{site.workflow.map((item) => <article data-reveal><div><h3>{item.title}</h3><p>{item.desc}</p></div></article>)}</div></div></section>
<section id="teachers" class="section-pad teacher-section"><div class="container teacher-layout"><div><h2><em>20000+</em> 签约严选教师<br />匹配的不只是学科</h2><p>综合学生所在地区、学段、学科、教材进度、当前基础、阶段目标与沟通特点进行匹配。教师录取率约 3%,85% 以上来自 985 / 211 或重点师范院校,均要求持有教师资格证。</p><div class="teacher-tags"><span>平均 5—8 年一线经验</span><span>固定教师持续跟进</span><span>分学段分学科匹配</span></div></div><div class="teacher-ledger" data-reveal><div class="ledger-head"><strong>匹配维度</strong></div>{["所在地区与教材版本", "学生学段与意向学科", "当前基础与具体薄弱点", "阶段目标与可用学习时间", "授课节奏与沟通特点"].map((item) => <div><strong>{item}</strong></div>)}</div></div></section>
<section id="teachers" class="section-pad teacher-section"><div class="container teacher-layout"><div><h2><em>20000+</em> 签约严选教师<br />匹配的不只是学科</h2><p>教师录取率约 3%,85% 以上来自 985 / 211 或重点师范院校,均要求持有教师资格证。教师平均拥有 5-8 年一线教学经验,其中包含省级骨干教师、教研组长及具有阅卷经验的教师;结合四大本地化教研中心,综合学生所在地区、学段、学科、教材进度、当前基础、阶段目标与沟通特点进行匹配。</p><div class="teacher-tags"><span>100% 持证要求</span><span>平均 5-8 年一线经验</span><span>四大本地化教研中心</span><span>固定教师持续跟进</span></div></div><div class="teacher-ledger" data-reveal><div class="ledger-head"><strong>匹配维度</strong></div>{["所在地区与教材版本", "学生学段与意向学科", "当前基础与具体薄弱点", "阶段目标与可用学习时间", "授课节奏与沟通特点"].map((item) => <div><strong>{item}</strong></div>)}</div></div></section>
<section class="section-pad section-accent"><div class="container feedback-layout"><div class="feedback-board" data-reveal><div class="feedback-board__head"><span>本节学习反馈</span><strong>数学 · 函数基础</strong></div><div class="feedback-score"><span>掌握情况</span><strong>继续巩固</strong></div><ul><li><div><strong>本节内容</strong><p>梳理函数概念与自变量取值范围</p></div></li><li><div><strong>课堂观察</strong><p>概念理解清楚,分类讨论仍容易遗漏</p></div></li><li><div><strong>下节安排</strong><p>从同类错题检查分类讨论步骤</p></div></li></ul></div><div><SectionHeading title="不是上完就结束,学习进展需要被记录" description="课程支持回放;课后反馈会说明学习内容、课堂表现、仍需巩固的问题及后续安排。具体反馈形式以实际课程方案为准。" /><div class="feedback-points"><span>课堂互动</span><span>课程回放</span><span>错题复盘</span><span>阶段沟通</span></div></div></div></section>
<section class="section-pad section-white"><div class="container faq-preview"><div><SectionHeading title="开始一对一辅导前,家长通常会问" description="把课程适配、教师匹配和学习反馈的重要信息公开说明。" /><a class="text-link" href="/faq/">查看全部常见问题</a></div><div class="faq-list">{site.faq.slice(0, 4).map((item, index) => <details data-reveal open={index === 0}><summary><strong>{item.q}</strong><i></i></summary><div><p>{item.a}</p></div></details>)}</div></div></section>
<section class="section-pad section-white"><div class="container faq-preview"><div><SectionHeading title="开始一对一辅导前,家长通常会问" description="把课程适配、教师匹配和学习反馈的重要信息公开说明。" /><a class="text-link" href="/faq/">查看全部常见问题</a></div><div class="faq-list">{site.faq.slice(0, 5).map((item, index) => <details data-reveal open={index === 0}><summary><strong>{item.q}</strong><i></i></summary><div><p>{item.a}</p></div></details>)}</div></div></section>
{latest.length > 0 && <section class="section-pad section-blue"><div class="container"><div class="section-intro-row"><SectionHeading title="学习资讯与家长指南" description="围绕学情判断、学科方法、阶段复习和在线学习服务的公开说明。" /><a class="text-link" href="/articles/">查看全部资讯</a></div><div class="article-grid">{latest.map((item) => <article class="article-card" data-reveal><a href={`/articles/${item.id}/`}><div class="article-card__meta"><span>{item.data.category}</span><time>{fmtDate(item.data.date)}</time></div><h2>{item.data.title}</h2><p>{item.data.excerpt}</p><strong>阅读全文</strong></a></article>)}</div></div></section>}
<CTA />
......
......@@ -5,7 +5,7 @@
.site-header{position:sticky;z-index:100;top:0;height:76px;border-bottom:1px solid rgba(220,232,239,.8);background:rgba(252,253,252,.96);transition:box-shadow .25s}.site-header.is-scrolled{box-shadow:0 8px 24px rgba(7,59,131,.08)}.site-header__inner{display:flex;align-items:center;height:100%;gap:24px}.brand{width:132px;height:62px;overflow:hidden}.brand img{width:100%;height:100%;object-fit:cover;object-position:center}.desktop-nav{margin-left:auto}.desktop-nav>ul{display:flex;margin:0;padding:0;list-style:none}.nav-item{position:relative}.nav-link{position:relative;display:block;padding:27px 13px 24px;color:#304957;font-size:14px;font-weight:650}.nav-link::after{position:absolute;right:13px;bottom:16px;left:13px;height:2px;content:"";background:var(--brand-accent);transform:scaleX(0);transition:transform .25s}.nav-link:hover,.nav-link[aria-current=page]{color:var(--brand-blue)}.nav-link:hover::after,.nav-link[aria-current=page]::after{transform:scaleX(1)}.nav-dropdown{position:absolute;top:calc(100% - 4px);left:50%;visibility:hidden;width:220px;padding-top:10px;opacity:0;transform:translate(-50%,8px);transition:.2s}.nav-item:hover .nav-dropdown,.nav-item:focus-within .nav-dropdown{visibility:visible;opacity:1;transform:translate(-50%,0)}.nav-dropdown ul{margin:0;padding:8px;border:1px solid var(--border);border-radius:10px;background:#fff;box-shadow:var(--shadow-hover);list-style:none}.nav-dropdown a{display:flex;justify-content:space-between;padding:10px 12px;border-radius:7px;font-size:13px}.nav-dropdown a:hover{color:var(--brand-blue);background:var(--bg-blue-soft)}.mobile-nav{display:none;margin-left:auto}.mobile-nav summary{display:grid;width:40px;height:40px;padding:10px;cursor:pointer;list-style:none}.mobile-nav summary span{height:2px;background:var(--brand-blue-dark)}
.home-hero{overflow:hidden;padding:56px 0 0;background:#fff}.home-hero__grid{display:grid;grid-template-columns:1.08fr .92fr;align-items:center;gap:20px}.hero-label{display:flex;align-items:center;margin-bottom:20px;color:var(--brand-blue);font-size:14px;font-weight:600}.home-hero h1{margin-bottom:25px;font-size:clamp(30px,2.65vw,40px);line-height:1.32;letter-spacing:-.03em}.home-hero h1 span,.home-hero h1 em{display:block;white-space:nowrap}.home-hero h1 em,.page-hero h1 em{color:var(--brand-blue);font-style:normal}.home-hero__copy{position:relative;z-index:2}.home-hero__copy>p{max-width:600px;margin-bottom:32px;color:var(--text-secondary);font-size:18px}.hero__proof{display:flex;flex-wrap:wrap;gap:16px;margin-top:30px}.hero__proof span{display:flex;align-items:center;gap:8px;color:var(--text-secondary);font-size:13px}.hero__proof span::before{width:6px;height:6px;border-radius:50%;content:"";background:var(--brand-accent)}.home-hero__visual{position:relative;margin-left:-36px}.image-frame{position:relative;overflow:hidden;background:#fff}.image-frame img{width:100%;aspect-ratio:1.22;object-fit:cover;mix-blend-mode:multiply;-webkit-mask-image:radial-gradient(ellipse 83% 86% at 55% 50%,#000 58%,rgba(0,0,0,.9) 72%,transparent 100%);mask-image:radial-gradient(ellipse 83% 86% at 55% 50%,#000 58%,rgba(0,0,0,.9) 72%,transparent 100%)}.stage-ruler{display:flex;align-items:center;justify-content:center;gap:25px;margin-top:32px;padding:20px 0;border-top:1px solid var(--divider);color:var(--brand-blue-dark);font-size:14px;font-weight:750}.stage-ruler i{width:5px;height:5px;border-radius:50%;background:var(--brand-accent)}
.subject-overview{background:#fff}.subject-overview__head{max-width:920px;margin:0 auto 62px;text-align:center}.subject-overview__label{margin-bottom:14px;color:var(--brand-blue);font-size:14px;font-weight:600}.subject-overview__head h2{margin-bottom:22px;font-size:clamp(38px,4.8vw,64px);font-weight:600;line-height:1.18;letter-spacing:-.04em}.subject-overview__lead{max-width:760px;margin:0 auto 22px;color:var(--brand-blue);font-size:19px;font-weight:600}.subject-overview__intro{max-width:900px;margin:0 auto;color:var(--text-secondary);font-size:16px}.subject-overview__grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}.subject-card{display:flex;min-height:190px;padding:30px;align-items:flex-start;gap:22px;border:1px solid transparent;border-radius:18px;background:var(--subject-bg);box-shadow:0 8px 28px rgba(20,115,230,.05);transition:transform .2s,box-shadow .2s,border-color .2s}.subject-card:hover{border-color:var(--subject-border);box-shadow:0 14px 34px rgba(20,115,230,.1);transform:translateY(-3px)}.subject-card__icon{display:grid;width:56px;height:56px;place-items:center;flex:0 0 auto;border-radius:15px;color:var(--subject-color);background:rgba(255,255,255,.76)}.subject-card__icon svg{width:32px;height:32px;fill:none;stroke:currentColor;stroke-width:2.2;stroke-linecap:round;stroke-linejoin:round}.subject-overview__grid h3{margin:3px 0 12px;color:var(--subject-title);font-size:27px;font-weight:600}.subject-overview__grid p{margin:0;color:var(--text-secondary);font-size:14px}.subject-card--blue{--subject-bg:#eef6ff;--subject-color:#1473e6;--subject-title:#145da8;--subject-border:#b9d8fb}.subject-card--rose{--subject-bg:#fff1f3;--subject-color:#c64d67;--subject-title:#9d3550;--subject-border:#f1c4ce}.subject-card--orange{--subject-bg:#fff5e9;--subject-color:#d87827;--subject-title:#a95718;--subject-border:#f2d1ad}.subject-card--indigo{--subject-bg:#edf3ff;--subject-color:#456fc2;--subject-title:#31569e;--subject-border:#c5d4f2}.subject-card--green{--subject-bg:#edf8ef;--subject-color:#3d9460;--subject-title:#287344;--subject-border:#bfdfc8}.subject-card--purple{--subject-bg:#f5f0fb;--subject-color:#8055aa;--subject-title:#643b8a;--subject-border:#d9c7e9}
.stat-band{padding:46px 0;background:#eaf4ff}.stat-grid{display:grid;grid-template-columns:repeat(4,1fr)}.stat-grid>div{padding:0 30px;border-right:1px solid #c8def5;text-align:center}.stat-grid>div:last-child{border:0}.stat-grid strong{display:block;color:var(--brand-blue);font-size:clamp(30px,4vw,48px);font-weight:700;letter-spacing:-.03em}.stat-grid p{margin:8px 0 0;color:#315575;font-size:13px}.data-note{max-width:900px;margin:24px auto 0;color:#67819a;font-size:11px;text-align:center}.need-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:16px}.need-card{display:flex;min-height:280px;padding:28px;flex-direction:column;border:1px solid var(--border);border-radius:16px;background:#fff;box-shadow:var(--shadow-card);transition:.25s}.need-card:hover{border-color:#b9d7fa;box-shadow:var(--shadow-hover);transform:translateY(-3px)}.need-card h3{margin:12px 0 14px;font-size:27px}.need-card p{color:var(--text-secondary)}.need-card>strong{margin-top:auto;color:var(--brand-blue);font-size:13px}.learning-map{display:grid;grid-template-columns:.9fr 1.1fr;gap:90px}.learning-map__copy{position:sticky;top:120px;align-self:start}.learning-map__copy .button{margin-top:30px}.learning-map__steps article{padding:26px 0;border-bottom:1px solid var(--divider)}.learning-map__steps h3{margin:0 0 8px;font-size:24px}.learning-map__steps p{margin:0;color:var(--text-secondary)}.teacher-section{background:#eaf4ff}.teacher-layout{display:grid;grid-template-columns:1fr .9fr;gap:80px;align-items:center}.teacher-layout h2{margin-bottom:22px;font-size:clamp(38px,4.6vw,60px);letter-spacing:-.04em}.teacher-layout h2 em{color:var(--brand-blue);font-style:normal}.teacher-layout>div>p{color:var(--text-secondary);font-size:17px}.teacher-tags{display:flex;flex-wrap:wrap;gap:8px;margin-top:26px}.teacher-tags span{padding:7px 11px;border:1px solid #bfd8f3;border-radius:6px;color:#315575;background:#fff;font-size:12px}.teacher-ledger{padding:26px;border:1px solid #c8def5;border-radius:18px;background:#fff;color:var(--text-primary);box-shadow:var(--shadow-card)}.ledger-head{padding-bottom:20px;border-bottom:2px solid var(--brand-blue)}.teacher-ledger>div:not(:first-child){padding:16px 0;border-bottom:1px solid var(--divider)}.feedback-layout{display:grid;grid-template-columns:1fr 1fr;gap:90px;align-items:center}.feedback-board{overflow:hidden;border:1px solid #cfe3fb;border-radius:16px;background:#fff;box-shadow:var(--shadow-card)}.feedback-board__head{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;color:#fff;background:#1473e6}.feedback-board__head span{font-size:12px}.feedback-score{display:flex;align-items:center;justify-content:space-between;padding:18px 24px;border-bottom:1px solid var(--divider)}.feedback-score strong{color:var(--brand-blue)}.feedback-board ul{margin:0;padding:8px 24px 20px;list-style:none}.feedback-board li{padding:16px 0;border-bottom:1px solid var(--divider)}.feedback-board li:last-child{border:0}.feedback-board li p{margin:4px 0 0;color:var(--text-secondary);font-size:13px}.feedback-points{display:flex;flex-wrap:wrap;gap:9px;margin-top:26px}.feedback-points span{padding:8px 12px;border:1px solid #cfe3fb;border-radius:8px;background:#fff;color:var(--brand-blue-dark);font-size:12px;font-weight:750}.faq-preview,.faq-layout{display:grid;grid-template-columns:.65fr 1.35fr;gap:80px}.faq-preview>div:first-child{position:sticky;top:110px;align-self:start}.faq-list details{border-bottom:1px solid var(--border)}.faq-list summary{display:grid;grid-template-columns:1fr 24px;gap:14px;align-items:center;padding:23px 0;cursor:pointer;list-style:none}.faq-list summary>strong{font-size:17px}.faq-list summary>i{position:relative;width:18px;height:18px}.faq-list summary>i::before,.faq-list summary>i::after{position:absolute;top:8px;left:3px;width:12px;height:2px;content:"";background:var(--brand-blue)}.faq-list summary>i::after{transform:rotate(90deg);transition:.2s}.faq-list details[open] summary>i::after{transform:none}.faq-list details>div{padding:0 38px 20px 0;color:var(--text-secondary)}
.stat-band{padding:46px 0;background:#eaf4ff}.stat-grid{display:grid;grid-template-columns:repeat(4,1fr)}.stat-grid>div{padding:0 30px;border-right:1px solid #c8def5;text-align:center}.stat-grid>div:last-child{border:0}.stat-grid strong{display:block;color:var(--brand-blue);font-size:clamp(30px,4vw,48px);font-weight:700;letter-spacing:-.03em}.stat-grid p{margin:8px 0 0;color:#315575;font-size:13px}.data-note{max-width:900px;margin:24px auto 0;color:#67819a;font-size:11px;text-align:center}.need-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:16px}.need-card{display:flex;min-height:280px;padding:28px;flex-direction:column;border:1px solid var(--border);border-radius:16px;background:#fff;box-shadow:var(--shadow-card);transition:.25s}.need-card:hover{border-color:#b9d7fa;box-shadow:var(--shadow-hover);transform:translateY(-3px)}.need-card h3{margin:12px 0 14px;font-size:27px}.need-card p{color:var(--text-secondary)}.need-card>strong{margin-top:auto;color:var(--brand-blue);font-size:13px}.need-scope{display:grid;grid-template-columns:auto 1fr;gap:14px 28px;align-items:center;margin-top:22px;padding:24px 28px;border:1px solid var(--border);border-radius:14px;background:rgba(255,255,255,.72)}.need-scope>strong{color:var(--brand-blue);font-size:14px}.need-scope>div{display:flex;flex-wrap:wrap;gap:8px}.need-scope span{padding:6px 10px;border-radius:6px;color:#315575;background:#fff;font-size:12px}.need-scope p{grid-column:2;margin:0;color:var(--text-muted);font-size:12px}.learning-map{display:grid;grid-template-columns:.9fr 1.1fr;gap:90px}.learning-map__copy{position:sticky;top:120px;align-self:start}.learning-map__copy .button{margin-top:30px}.learning-map__steps article{padding:26px 0;border-bottom:1px solid var(--divider)}.learning-map__steps h3{margin:0 0 8px;font-size:24px}.learning-map__steps p{margin:0;color:var(--text-secondary)}.teacher-section{background:#eaf4ff}.teacher-layout{display:grid;grid-template-columns:1fr .9fr;gap:80px;align-items:center}.teacher-layout h2{margin-bottom:22px;font-size:clamp(38px,4.6vw,60px);letter-spacing:-.04em}.teacher-layout h2 em{color:var(--brand-blue);font-style:normal}.teacher-layout>div>p{color:var(--text-secondary);font-size:17px}.teacher-tags{display:flex;flex-wrap:wrap;gap:8px;margin-top:26px}.teacher-tags span{padding:7px 11px;border:1px solid #bfd8f3;border-radius:6px;color:#315575;background:#fff;font-size:12px}.teacher-ledger{padding:26px;border:1px solid #c8def5;border-radius:18px;background:#fff;color:var(--text-primary);box-shadow:var(--shadow-card)}.ledger-head{padding-bottom:20px;border-bottom:2px solid var(--brand-blue)}.teacher-ledger>div:not(:first-child){padding:16px 0;border-bottom:1px solid var(--divider)}.feedback-layout{display:grid;grid-template-columns:1fr 1fr;gap:90px;align-items:center}.feedback-board{overflow:hidden;border:1px solid #cfe3fb;border-radius:16px;background:#fff;box-shadow:var(--shadow-card)}.feedback-board__head{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;color:#fff;background:#1473e6}.feedback-board__head span{font-size:12px}.feedback-score{display:flex;align-items:center;justify-content:space-between;padding:18px 24px;border-bottom:1px solid var(--divider)}.feedback-score strong{color:var(--brand-blue)}.feedback-board ul{margin:0;padding:8px 24px 20px;list-style:none}.feedback-board li{padding:16px 0;border-bottom:1px solid var(--divider)}.feedback-board li:last-child{border:0}.feedback-board li p{margin:4px 0 0;color:var(--text-secondary);font-size:13px}.feedback-points{display:flex;flex-wrap:wrap;gap:9px;margin-top:26px}.feedback-points span{padding:8px 12px;border:1px solid #cfe3fb;border-radius:8px;background:#fff;color:var(--brand-blue-dark);font-size:12px;font-weight:750}.faq-preview,.faq-layout{display:grid;grid-template-columns:.65fr 1.35fr;gap:80px}.faq-preview>div:first-child{position:sticky;top:110px;align-self:start}.faq-list details{border-bottom:1px solid var(--border)}.faq-list summary{display:grid;grid-template-columns:1fr 24px;gap:14px;align-items:center;padding:23px 0;cursor:pointer;list-style:none}.faq-list summary>strong{font-size:17px}.faq-list summary>i{position:relative;width:18px;height:18px}.faq-list summary>i::before,.faq-list summary>i::after{position:absolute;top:8px;left:3px;width:12px;height:2px;content:"";background:var(--brand-blue)}.faq-list summary>i::after{transform:rotate(90deg);transition:.2s}.faq-list details[open] summary>i::after{transform:none}.faq-list details>div{padding:0 38px 20px 0;color:var(--text-secondary)}
.page-hero{min-height:430px;padding:92px 0 78px;background:var(--bg-blue-soft)}.page-hero__inner{max-width:930px}.breadcrumb{margin-bottom:34px;color:var(--text-muted);font-size:12px}.breadcrumb a:hover{color:var(--brand-blue)}.page-hero h1{margin-bottom:24px;font-size:clamp(45px,6vw,72px);line-height:1.12;letter-spacing:-.055em}.page-hero p{max-width:760px;margin:0;color:var(--text-secondary);font-size:18px}.anchor-nav{position:sticky;z-index:20;top:76px;border-block:1px solid var(--border);background:rgba(255,255,255,.96)}.anchor-nav .container{display:grid;grid-template-columns:repeat(4,1fr)}.anchor-nav a{padding:18px;border-right:1px solid var(--border);color:var(--text-secondary);font-size:13px;text-align:center}.anchor-nav a span{margin-right:8px;color:var(--brand-blue)}.service-list{display:grid;gap:18px}.service-detail{scroll-margin-top:150px;border:1px solid var(--border);border-radius:16px;background:#fff}.service-detail__content{display:grid;grid-template-columns:.85fr 1.15fr;gap:60px;padding:46px}.service-detail__summary h2{margin-bottom:16px;font-size:40px}.service-detail__summary .lead{color:var(--text-secondary);font-size:17px}.service-detail__delivery ul{margin:0 0 24px;padding:0;list-style:none}.service-detail__delivery li{display:flex;gap:12px;padding:12px 0;border-bottom:1px solid var(--divider);color:var(--text-secondary)}.service-detail__delivery li span{color:var(--brand-blue);font-weight:900}.capability-pills{display:flex;flex-wrap:wrap;gap:8px}.capability-pills span{padding:7px 11px;border-radius:6px;background:var(--bg-blue-soft);color:var(--brand-blue);font-size:12px}.stage-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:45px}.stage-grid article{padding:32px;border-top:4px solid var(--brand-accent);background:var(--bg-blue-soft)}.stage-grid span{color:var(--brand-blue);font-size:12px}.stage-grid h3{margin:18px 0 9px;font-size:32px}.stage-grid p{margin:0;color:var(--text-secondary)}
.service-hero{padding:86px 0;background:#fff}.service-hero__inner{display:grid;grid-template-columns:1fr .85fr;gap:75px;align-items:center}.service-hero h1{font-size:clamp(44px,5vw,68px);letter-spacing:-.04em}.service-hero h1 em{display:block;margin-top:16px;color:var(--text-secondary);font-size:18px;font-style:normal;font-weight:400;letter-spacing:0;line-height:1.7}.plan-sheet{padding:26px;border:1px solid var(--border);border-radius:16px;background:var(--bg-blue-soft);box-shadow:var(--shadow-card)}.plan-sheet>div:first-child{padding-bottom:18px;border-bottom:2px solid var(--brand-blue)}.plan-sheet article{padding:14px 0;border-bottom:1px solid var(--border)}.plan-sheet article small{color:var(--brand-blue)}.plan-sheet article p{margin:2px 0 0}.plan-sheet>p{margin:15px 0 0;color:var(--text-muted);font-size:10px}.service-cap-layout{display:grid;grid-template-columns:.8fr 1.2fr;gap:70px}.service-cap-layout>div:first-child>h2{font-size:43px}.service-cap-layout>div:first-child>p{color:var(--text-secondary)}.service-cap-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}.service-cap-grid article{padding:25px;border:1px solid var(--border);border-radius:12px;background:#fff}.service-cap-grid h3{margin:0 0 9px}.service-cap-grid p{margin:0;color:var(--text-secondary)}.service-feature-layout{max-width:900px}.large-check-list{margin:0;padding:0;list-style:none}.large-check-list li{padding:22px 0;border-bottom:1px solid var(--divider)}.large-check-list p{margin:0;font-size:18px}.process-section,.delivery-section,.principle-section{color:#fff;background:#1473e6}.process-grid,.method-flow{display:grid;grid-template-columns:repeat(4,1fr);margin-top:48px}.process-grid article{padding:25px;border-right:1px solid rgba(255,255,255,.24)}.process-grid h3{margin:0;font-size:23px}.related-services{background:var(--bg-blue-soft)}.related-services .container{display:grid;grid-template-columns:repeat(3,1fr)}.related-services a{padding:25px;border-right:1px solid var(--border);text-align:center}
.about-intro-layout{display:grid;grid-template-columns:.8fr 1.2fr;gap:90px}.story-column p{padding:0 0 26px;border-bottom:1px solid var(--divider);color:var(--text-secondary);font-size:17px}.advantage-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-top:45px}.advantage-grid article{padding:28px;border:1px solid var(--border);border-radius:14px;background:#fff}.advantage-grid h3{margin:0 0 10px;font-size:21px}.advantage-grid p{margin:0;color:var(--text-secondary)}.principle-layout{display:grid;grid-template-columns:.8fr 1.2fr;gap:90px}.principle-layout h2{font-size:46px}.principle-layout>div:first-child>p{color:#eef7ff}.principle-layout article{padding:19px 0;border-bottom:1px solid rgba(255,255,255,.24)}.principle-layout article h3{margin:0 0 6px}.principle-layout article p{margin:0;color:#eef7ff}.vision-section{background:var(--bg-accent-soft)}.vision-card{padding:58px;border:1px solid #cfe3fb;border-radius:18px;background:#fff}.vision-card blockquote{max-width:950px;margin:0 0 25px;font-size:clamp(28px,4vw,48px);font-weight:700;line-height:1.35}.vision-card p{margin:0;color:var(--text-secondary)}.method-flow article{padding:25px;border-right:1px solid var(--border)}.method-flow h2{margin:0 0 12px;font-size:26px}.method-flow p{color:var(--text-secondary)}.delivery-layout{display:grid;grid-template-columns:.7fr 1.3fr;gap:75px}.delivery-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}.delivery-grid article{padding:25px;border:1px solid rgba(255,255,255,.25);border-radius:12px}.delivery-grid h3{margin:0 0 8px}.delivery-grid p{margin:0;color:#eef7ff}.principle-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:40px}.principle-grid article{padding:30px;border:1px solid var(--border);border-radius:14px;background:#fff}.principle-grid h3{margin:0 0 10px}.principle-grid p{margin:0;color:var(--text-secondary)}.faq-layout aside{position:sticky;top:120px;align-self:start}.faq-layout aside h2{font-size:38px}.faq-layout aside p{color:var(--text-secondary)}
......@@ -13,7 +13,7 @@
.article-layout{display:grid;grid-template-columns:250px 1fr;gap:40px}.article-sidebar{align-self:start;padding:24px;border:1px solid var(--border);border-radius:12px;background:#fff}.article-sidebar h2{font-size:18px}.article-sidebar a{display:flex;justify-content:space-between;padding:8px 0;color:var(--text-secondary);font-size:13px}.article-sidebar a:hover,.article-sidebar a.is-active{color:var(--brand-blue)}.article-list-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}.pagination{display:flex;justify-content:center;gap:8px;margin-top:40px}.pagination a,.pagination span{display:grid;place-items:center;width:38px;height:38px;border:1px solid var(--border);border-radius:6px;background:#fff}.pagination [aria-current=page]{color:#fff;background:var(--brand-blue)}.article-detail-head{padding:90px 0;color:#fff;background:#1473e6}.article-detail-head__inner{max-width:960px}.article-detail-category{display:inline-block;margin-bottom:20px;padding:6px 10px;border-radius:5px;color:var(--text-primary);background:var(--brand-accent);font-size:11px}.article-detail-head h1{font-size:clamp(40px,5vw,64px)}.article-detail-head__inner>p{color:#eef7ff;font-size:18px}.article-detail-meta{display:flex;gap:20px;color:#e5f3ff;font-size:12px}.article-detail-layout{display:grid;grid-template-columns:220px minmax(0,760px);justify-content:center;gap:70px;padding:70px 0}.article-detail-layout aside{color:var(--text-muted);font-size:13px}.article-detail-layout aside a{color:var(--brand-blue);font-weight:750}.article-prose{font-size:17px}.article-prose h2{margin-top:45px;font-size:30px}.article-prose h3{margin-top:30px}.article-prose p,.article-prose li{color:var(--text-secondary)}.article-prose img{border-radius:12px}.article-prose a{color:var(--brand-blue);text-decoration:underline}.article-prose blockquote{margin:25px 0;padding:20px;border-left:4px solid var(--brand-accent);background:var(--bg-accent-soft)}
.cta-band{padding:78px 0;color:#fff;background:var(--brand-blue)}.cta-band__inner{display:flex;align-items:center;justify-content:space-between;gap:60px}.cta-band h2{margin-bottom:14px;font-size:clamp(34px,4vw,52px)}.cta-band p{max-width:720px;margin:0;color:#edf7ff}.cta-band__actions{display:flex;align-items:center;gap:22px;flex-shrink:0}.site-footer{padding:70px 0 0;color:#fff;background:#000}.site-footer a{color:#fff}.footer-grid{display:grid;grid-template-columns:1.4fr .7fr .8fr 1fr;gap:50px}.footer-logo{display:block;width:150px;height:74px;overflow:hidden;border-radius:4px}.footer-logo img{width:100%;height:100%;object-fit:cover}.footer-brand>p{max-width:330px;margin-top:18px}.footer-grid h2{margin:0 0 18px;color:#fff;font-size:15px}.footer-grid>div:not(:first-child)>a{display:block;margin:9px 0;color:#fff;font-size:13px}.footer-grid a:hover{color:#fff}.footer-phone{color:#fff!important;font-size:23px!important;font-weight:700}.footer-contact p{font-size:12px}.footer-arrow{margin-top:22px!important;color:#fff!important}.footer-bottom{display:flex;justify-content:space-between;gap:30px;margin-top:55px;padding:22px 0;border-top:1px solid rgba(255,255,255,.3);font-size:11px}.footer-bottom p{margin:0}.contact-dialog{width:min(920px,calc(100% - 32px));padding:0;border:0;border-radius:18px;background:transparent}.contact-dialog::backdrop{background:rgba(31,64,96,.55)}.contact-dialog__panel{position:relative;padding:38px;border:1px solid var(--border);border-radius:18px;background:#fff}.contact-dialog__close{position:absolute;top:14px;right:15px;width:38px;height:38px;border:0;border-radius:50%;background:var(--bg-blue-soft);font-size:24px;cursor:pointer}.contact-dialog__heading{max-width:640px}.contact-dialog__heading h2{margin-bottom:10px;font-size:36px}.contact-dialog__heading p{color:var(--text-secondary)}.contact-dialog__grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:28px}.contact-dialog__grid article{padding:22px;border:1px solid var(--border);border-radius:12px;background:var(--bg-blue-soft)}.contact-dialog__grid h3{margin:0 0 8px}.contact-dialog__grid p{margin:6px 0 0;color:var(--text-muted);font-size:12px}.contact-dialog__primary{color:var(--brand-blue-dark);font-size:21px;font-weight:700}.contact-dialog__email,.contact-dialog__website{font-size:15px}.not-found{display:grid;place-items:center;min-height:70vh;padding:80px 0;background:var(--bg-blue-soft);text-align:center}.not-found>div>span{display:block;color:var(--brand-blue);font-size:100px;font-weight:700}
@media(max-width:1080px){.desktop-nav,.header-cta{display:none}.mobile-nav{display:block}.mobile-nav[open]>nav{position:absolute;top:76px;right:0;left:0;max-height:calc(100vh - 76px);padding:20px 24px 28px;overflow:auto;border-bottom:1px solid var(--border);background:#fff;box-shadow:var(--shadow-hover)}.mobile-nav__group{padding:12px 0;border-bottom:1px solid var(--divider)}.mobile-nav__primary{font-weight:800}.mobile-subnav{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px}.mobile-subnav a{padding:5px 8px;border-radius:5px;background:var(--bg-blue-soft);font-size:12px}.mobile-nav nav>.button{width:100%;margin-top:18px}.brand{margin-right:auto}.home-hero__grid{gap:25px}.subject-overview__grid{grid-template-columns:repeat(2,1fr)}.need-grid{grid-template-columns:1fr 1fr}.teacher-layout,.feedback-layout{gap:50px}.footer-grid{grid-template-columns:1.2fr 1fr 1fr}.footer-contact{grid-column:2/4}}
@media(max-width:760px){.container{width:min(calc(100% - 32px),var(--container))}.section-pad{padding:72px 0}.section-intro-row{display:block;margin-bottom:34px}.section-intro-row>.text-link{display:inline-block;margin-top:18px}.section-heading h2{font-size:36px}.home-hero{padding-top:38px}.home-hero__grid,.learning-map,.teacher-layout,.feedback-layout,.faq-preview,.faq-layout,.service-hero__inner,.service-cap-layout,.service-feature-layout,.about-intro-layout,.principle-layout,.delivery-layout,.contact-hero__grid,.visit-grid{grid-template-columns:1fr;gap:40px}.home-hero h1{font-size:34px;line-height:1.32}.home-hero h1 span,.home-hero h1 em{white-space:normal}.home-hero__copy>p{font-size:16px}.home-hero__visual{margin-top:10px;margin-left:0}.stage-ruler{gap:10px;font-size:11px}.subject-overview__head{margin-bottom:42px;text-align:left}.subject-overview__head h2{font-size:38px}.subject-overview__lead{font-size:17px}.subject-overview__grid{grid-template-columns:1fr}.subject-overview__grid article{min-height:auto;padding:24px}.stat-grid{grid-template-columns:1fr 1fr;row-gap:28px}.stat-grid>div:nth-child(2){border:0}.stat-grid strong{font-size:34px}.need-grid,.service-cap-grid,.stage-grid,.advantage-grid,.delivery-grid,.principle-grid,.article-grid,.article-list-grid,.contact-grid,.contact-dialog__grid{grid-template-columns:1fr}.need-card{min-height:260px}.need-card h3{margin-top:0}.learning-map__copy,.faq-preview>div:first-child,.faq-layout aside{position:static}.teacher-layout h2{font-size:40px}.feedback-layout .feedback-board{order:2}.faq-list details>div{padding-left:0}.page-hero{min-height:auto;padding:68px 0}.page-hero h1{font-size:43px}.anchor-nav{top:76px;overflow:auto}.anchor-nav .container{width:max-content;grid-template-columns:repeat(4,150px)}.service-detail__content{grid-template-columns:1fr;gap:25px;padding:28px}.service-hero{padding:62px 0}.process-grid,.method-flow{grid-template-columns:1fr 1fr}.process-grid article,.method-flow article{border-bottom:1px solid rgba(255,255,255,.24)}.related-services .container{display:block}.related-services a{border-bottom:1px solid var(--border)}.principle-layout h2,.visit-grid h2{font-size:38px}.contact-hours{display:none}.contact-grid article{min-height:auto}.contact-big-link{font-size:25px}.article-layout,.article-detail-layout{grid-template-columns:1fr}.article-sidebar{display:none}.article-detail-layout{padding:48px 16px}.cta-band__inner,.cta-band__actions{align-items:flex-start;flex-direction:column}.footer-grid{grid-template-columns:1fr 1fr}.footer-brand{grid-column:1/3}.footer-contact{grid-column:1/3}.footer-bottom{flex-direction:column}.contact-dialog__panel{padding:26px 20px}.contact-dialog__heading h2{font-size:29px}}
@media(max-width:760px){.container{width:min(calc(100% - 32px),var(--container))}.section-pad{padding:72px 0}.section-intro-row{display:block;margin-bottom:34px}.section-intro-row>.text-link{display:inline-block;margin-top:18px}.section-heading h2{font-size:36px}.home-hero{padding-top:38px}.home-hero__grid,.learning-map,.teacher-layout,.feedback-layout,.faq-preview,.faq-layout,.service-hero__inner,.service-cap-layout,.service-feature-layout,.about-intro-layout,.principle-layout,.delivery-layout,.contact-hero__grid,.visit-grid{grid-template-columns:1fr;gap:40px}.home-hero h1{font-size:34px;line-height:1.32}.home-hero h1 span,.home-hero h1 em{white-space:normal}.home-hero__copy>p{font-size:16px}.home-hero__visual{margin-top:10px;margin-left:0}.stage-ruler{gap:10px;font-size:11px}.subject-overview__head{margin-bottom:42px;text-align:left}.subject-overview__head h2{font-size:38px}.subject-overview__lead{font-size:17px}.subject-overview__grid{grid-template-columns:1fr}.subject-overview__grid article{min-height:auto;padding:24px}.stat-grid{grid-template-columns:1fr 1fr;row-gap:28px}.stat-grid>div:nth-child(2){border:0}.stat-grid strong{font-size:34px}.need-grid,.service-cap-grid,.stage-grid,.advantage-grid,.delivery-grid,.principle-grid,.article-grid,.article-list-grid,.contact-grid,.contact-dialog__grid{grid-template-columns:1fr}.need-card{min-height:260px}.need-card h3{margin-top:0}.need-scope{display:block}.need-scope>div{margin:14px 0}.need-scope p{margin:0}.learning-map__copy,.faq-preview>div:first-child,.faq-layout aside{position:static}.teacher-layout h2{font-size:40px}.feedback-layout .feedback-board{order:2}.faq-list details>div{padding-left:0}.page-hero{min-height:auto;padding:68px 0}.page-hero h1{font-size:43px}.anchor-nav{top:76px;overflow:auto}.anchor-nav .container{width:max-content;grid-template-columns:repeat(4,150px)}.service-detail__content{grid-template-columns:1fr;gap:25px;padding:28px}.service-hero{padding:62px 0}.process-grid,.method-flow{grid-template-columns:1fr 1fr}.process-grid article,.method-flow article{border-bottom:1px solid rgba(255,255,255,.24)}.related-services .container{display:block}.related-services a{border-bottom:1px solid var(--border)}.principle-layout h2,.visit-grid h2{font-size:38px}.contact-hours{display:none}.contact-grid article{min-height:auto}.contact-big-link{font-size:25px}.article-layout,.article-detail-layout{grid-template-columns:1fr}.article-sidebar{display:none}.article-detail-layout{padding:48px 16px}.cta-band__inner,.cta-band__actions{align-items:flex-start;flex-direction:column}.footer-grid{grid-template-columns:1fr 1fr}.footer-brand{grid-column:1/3}.footer-contact{grid-column:1/3}.footer-bottom{flex-direction:column}.contact-dialog__panel{padding:26px 20px}.contact-dialog__heading h2{font-size:29px}}
@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}*,*::before,*::after{animation-duration:.01ms!important;transition-duration:.01ms!important}[data-reveal]{opacity:1;transform:none}}
@media print{.site-header,.site-footer,.cta-band,.mobile-nav{display:none}[data-reveal]{opacity:1;transform:none}}
.brand{display:flex;width:108px;height:68px;align-items:center;overflow:visible}.brand img{width:100%;height:auto;max-height:66px;object-fit:contain}.footer-logo{display:flex;width:148px;height:100px;align-items:center;overflow:visible;border-radius:0}.footer-logo img{width:100%;height:auto;max-height:96px;object-fit:contain}
......
......@@ -41,7 +41,7 @@ async function login(password: string): Promise<{ response: Response; cookie?: s
"Content-Type": "application/json",
Cookie: cookieFrom(captchaResponse, "cms_captcha"),
},
body: JSON.stringify({ password, captcha: code }),
body: JSON.stringify({ password, captcha: code, captchaToken: captchaResponse.headers.get("X-Captcha-Token") }),
}), "session", `login-${requestNumber}`);
return {
response,
......@@ -64,6 +64,40 @@ async function changePassword(cookie: string, currentPassword: string, newPasswo
}), "account/password");
}
test("the displayed captcha remains valid when a later image request replaces the captcha cookie", async () => {
const displayedCaptcha = await handleCmsApi(
new Request("http://localhost/api/cms/captcha"),
"captcha",
"captcha-race-displayed",
);
const displayedCode = [...(await displayedCaptcha.text()).matchAll(/<text\b[^>]*>([^<])<\/text>/g)]
.map((match) => match[1])
.join("");
const displayedToken = displayedCaptcha.headers.get("X-Captcha-Token");
assert.equal(displayedCode.length, 4);
assert.ok(displayedToken);
const laterCaptcha = await handleCmsApi(
new Request("http://localhost/api/cms/captcha"),
"captcha",
"captcha-race-later",
);
const response = await handleCmsApi(new Request("http://localhost/api/cms/session", {
method: "POST",
headers: {
"Content-Type": "application/json",
Cookie: cookieFrom(laterCaptcha, "cms_captcha"),
},
body: JSON.stringify({
password: "environment-master-password",
captcha: displayedCode,
captchaToken: displayedToken,
}),
}), "session", "captcha-race-login");
assert.equal(response.status, 200);
});
test("users can change the persisted password while the .env password remains valid", async () => {
const initialLogin = await login("environment-master-password");
assert.equal(initialLogin.response.status, 200);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment