Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in / Register
Toggle navigation
Q
qiyouxue
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
xuchentao
qiyouxue
Commits
330212cd
Commit
330212cd
authored
Aug 13, 2026
by
tao355667
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: expand homepage and fix admin captcha
parent
63264608
Changes
7
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
83 additions
and
24 deletions
+83
-24
app.js
public/admin/app.js
+23
-4
style.css
public/admin/style.css
+2
-2
site.ts
src/data/site.ts
+9
-5
cms-api.ts
src/lib/cms-api.ts
+6
-4
index.astro
src/pages/index.astro
+6
-6
global.css
src/styles/global.css
+2
-2
auth.test.ts
tests/auth.test.ts
+35
-1
No files found.
public/admin/app.js
View file @
330212cd
...
@@ -8,6 +8,9 @@ let editingStatus = "new-unsaved";
...
@@ -8,6 +8,9 @@ let editingStatus = "new-unsaved";
let
dirty
=
false
;
let
dirty
=
false
;
let
previewTimer
=
null
;
let
previewTimer
=
null
;
let
previewRequest
=
0
;
let
previewRequest
=
0
;
let
captchaToken
=
""
;
let
captchaObjectUrl
=
""
;
let
captchaRequest
=
0
;
async
function
api
(
path
,
options
=
{})
{
async
function
api
(
path
,
options
=
{})
{
const
endpoint
=
path
.
startsWith
(
"/"
)
?
path
:
`/
${
path
}
`
;
const
endpoint
=
path
.
startsWith
(
"/"
)
?
path
:
`/
${
path
}
`
;
...
@@ -24,10 +27,26 @@ async function api(path, options = {}) {
...
@@ -24,10 +27,26 @@ async function api(path, options = {}) {
const
show
=
(
selector
)
=>
$
(
selector
).
classList
.
remove
(
"hidden"
);
const
show
=
(
selector
)
=>
$
(
selector
).
classList
.
remove
(
"hidden"
);
const
hide
=
(
selector
)
=>
$
(
selector
).
classList
.
add
(
"hidden"
);
const
hide
=
(
selector
)
=>
$
(
selector
).
classList
.
add
(
"hidden"
);
const
refreshCaptcha
=
()
=>
{
async
function
refreshCaptcha
()
{
$
(
"#captcha-image"
).
src
=
`
${
API_BASE
}
/captcha?t=
${
Date
.
now
()}
`
;
const
request
=
++
captchaRequest
;
captchaToken
=
""
;
$
(
"#captcha"
).
value
=
""
;
$
(
"#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
()
{
function
showLogin
()
{
show
(
"#login"
);
show
(
"#login"
);
...
@@ -52,7 +71,7 @@ $("#login-form").addEventListener("submit", async (event) => {
...
@@ -52,7 +71,7 @@ $("#login-form").addEventListener("submit", async (event) => {
event
.
preventDefault
();
event
.
preventDefault
();
$
(
"#login-error"
).
textContent
=
""
;
$
(
"#login-error"
).
textContent
=
""
;
try
{
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
=
""
;
$
(
"#password"
).
value
=
""
;
$
(
"#captcha"
).
value
=
""
;
$
(
"#captcha"
).
value
=
""
;
hide
(
"#login"
);
show
(
"#app"
);
hide
(
"#login"
);
show
(
"#app"
);
...
...
public/admin/style.css
View file @
330212cd
...
@@ -35,8 +35,8 @@ input, textarea, select { width: 100%; padding: 11px 13px; color: var(--text); b
...
@@ -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
);
}
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
{
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
>
div
:first-child
{
display
:
flex
;
align-items
:
center
;
gap
:
13px
;
}
.topbar
strong
{
color
:
var
(
--primary
)
;
font-size
:
19px
;
}
.topbar
strong
{
color
:
white
;
font-size
:
19px
;
}
.topbar
span
{
font-size
:
13px
;
opacity
:
.76
;
}
.topbar
span
{
color
:
white
;
font-size
:
13px
;
opacity
:
.76
;
}
.top-actions
{
display
:
flex
;
align-items
:
center
;
gap
:
9px
;
}
.top-actions
{
display
:
flex
;
align-items
:
center
;
gap
:
9px
;
}
.topbar
.ghost
{
color
:
white
;
border-color
:
rgba
(
255
,
255
,
255
,
.24
);
}
.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
;
}
.pending-badge
{
padding
:
5px
10px
;
color
:
#cfe2ff
;
background
:
rgba
(
20
,
115
,
230
,
.12
);
border
:
1px
solid
rgba
(
20
,
115
,
230
,
.3
);
border-radius
:
999px
;
}
...
...
src/data/site.ts
View file @
330212cd
export
const
site
=
{
export
const
site
=
{
seo
:
{
seo
:
{
title
:
"启优学一对一|小学初中高中在线个性化辅导"
,
title
:
"启优学一对一|小学初中高中在线个性化辅导"
,
description
:
"启优学一对一面向小学、初中和高中学生
,通过学情诊断、教师匹配、在线一对一授课、课后反馈与阶段复盘,制定有针对性的学习方案
。"
,
description
:
"启优学一对一面向小学、初中和高中学生
提供在线个性化辅导,通过学情诊断、教师匹配、专属学习方案、课堂反馈和阶段复盘,帮助学生针对性巩固基础、改善学习方法
。"
,
keywords
:
[
"启优学"
,
"启优学一对一"
,
"在线一对一辅导"
,
"小学一对一"
,
"初中一对一"
,
"高中一对一"
,
"个性化辅导"
],
keywords
:
[
"启优学"
,
"启优学一对一"
,
"在线一对一辅导"
,
"小学一对一"
,
"初中一对一"
,
"高中一对一"
,
"个性化辅导"
],
imageAlt
:
"启优学在线一对一课堂"
,
imageAlt
:
"启优学在线一对一课堂"
,
},
},
...
@@ -22,6 +22,7 @@ export const site = {
...
@@ -22,6 +22,7 @@ export const site = {
{
value
:
"85%+"
,
label
:
"来自 985 / 211 或重点师范院校"
},
{
value
:
"85%+"
,
label
:
"来自 985 / 211 或重点师范院校"
},
{
value
:
"20万+"
,
label
:
"累计服务学员"
},
{
value
:
"20万+"
,
label
:
"累计服务学员"
},
],
],
scenarios
:
[
"基础夯实"
,
"同步校内"
,
"偏科补弱"
,
"阶段复习"
,
"升学冲刺"
,
"拔高培优"
],
},
},
services
:
[
services
:
[
{
{
...
@@ -77,14 +78,17 @@ export const site = {
...
@@ -77,14 +78,17 @@ export const site = {
vision
:
"启发引导,因材施教;不只关注一次结果,更陪伴学生建立可以持续使用的学习能力。"
,
vision
:
"启发引导,因材施教;不只关注一次结果,更陪伴学生建立可以持续使用的学习能力。"
,
},
},
workflow
:
[
workflow
:
[
{
number
:
"01"
,
title
:
"沟通"
,
desc
:
"了解年级、教材、进度、近期试卷与当前目标。"
},
{
number
:
"01"
,
title
:
"需求沟通"
,
desc
:
"了解年级、地区、教材、校内进度、近期试卷与阶段目标。"
},
{
number
:
"02"
,
title
:
"诊断"
,
desc
:
"区分知识缺口、方法问题和学习习惯问题。"
},
{
number
:
"02"
,
title
:
"三维学情诊断"
,
desc
:
"结合试卷、错题与学习进度,区分知识缺口、方法和习惯问题。"
},
{
number
:
"03"
,
title
:
"匹配"
,
desc
:
"综合学段、学科、基础与沟通特点匹配教师。"
},
{
number
:
"03"
,
title
:
"教师匹配"
,
desc
:
"综合学段、学科、教材版本、当前基础与沟通特点匹配固定教师。"
},
{
number
:
"04"
,
title
:
"复盘"
,
desc
:
"根据课堂与练习反馈持续调整后续安排。"
},
{
number
:
"04"
,
title
:
"互动授课"
,
desc
:
"通过提问、作答、即时反馈与难度调整,检查学生是否真正理解。"
},
{
number
:
"05"
,
title
:
"课后反馈"
,
desc
:
"记录本节内容、掌握情况、仍需巩固的问题与下一次安排。"
},
{
number
:
"06"
,
title
:
"阶段复盘"
,
desc
:
"根据课堂、练习与阶段表现持续调整后续学习方案。"
},
],
],
faq
:
[
faq
:
[
{
q
:
"启优学一对一是什么?"
,
a
:
"启优学一对一是面向小学、初中、高中学生的在线一对一辅导品牌,主要提供小初高校内学科个性化辅导服务。课程以学情诊断为起点,根据年级、学科基础、教材版本、薄弱点和学习目标匹配教师。"
},
{
q
:
"启优学一对一是什么?"
,
a
:
"启优学一对一是面向小学、初中、高中学生的在线一对一辅导品牌,主要提供小初高校内学科个性化辅导服务。课程以学情诊断为起点,根据年级、学科基础、教材版本、薄弱点和学习目标匹配教师。"
},
{
q
:
"主要提供哪些课程?"
,
a
:
"覆盖基础夯实、同步校内、偏科补弱、阶段复习、升学冲刺和拔高培优等学习场景。课程根据学生实际学情制定,不是统一模板课。"
},
{
q
:
"主要提供哪些课程?"
,
a
:
"覆盖基础夯实、同步校内、偏科补弱、阶段复习、升学冲刺和拔高培优等学习场景。课程根据学生实际学情制定,不是统一模板课。"
},
{
q
:
"适合哪些学生?"
,
a
:
"适合存在单科短板、偏科拖分、基础断层、学习习惯不稳定、校内进度跟不上、考试复习效率较低,或需要升学冲刺与拔高培优的小初高学生。正式上课前会先沟通学情,再判断是否适合一对一辅导。"
},
{
q
:
"覆盖哪些年级和科目?"
,
a
:
"覆盖小学1-6年级、初中1-3年级和高中1-3年级,支持语文、数学、英语、物理、化学、生物、地理、道德与法治等常见校内学科。具体开课情况以咨询确认为准。"
},
{
q
:
"覆盖哪些年级和科目?"
,
a
:
"覆盖小学1-6年级、初中1-3年级和高中1-3年级,支持语文、数学、英语、物理、化学、生物、地理、道德与法治等常见校内学科。具体开课情况以咨询确认为准。"
},
{
q
:
"可以只辅导一科吗?"
,
a
:
"可以,尤其适合单科拖分、偏科明显或某一学科基础断层的学生。平台会先判断具体知识漏洞,再匹配对应学科教师,不强制多科打包。"
},
{
q
:
"可以只辅导一科吗?"
,
a
:
"可以,尤其适合单科拖分、偏科明显或某一学科基础断层的学生。平台会先判断具体知识漏洞,再匹配对应学科教师,不强制多科打包。"
},
{
q
:
"如何匹配教师?"
,
a
:
"根据学生年级、学科、教材版本、当前基础、学习目标和沟通特点进行匹配。匹配成功后通常安排固定教师长期授课;如适配度明显不合适,可联系课程服务人员协助评估调整。"
},
{
q
:
"如何匹配教师?"
,
a
:
"根据学生年级、学科、教材版本、当前基础、学习目标和沟通特点进行匹配。匹配成功后通常安排固定教师长期授课;如适配度明显不合适,可联系课程服务人员协助评估调整。"
},
...
...
src/lib/cms-api.ts
View file @
330212cd
...
@@ -117,8 +117,8 @@ function captchaMac(code: unknown, expiresAt: number): string {
...
@@ -117,8 +117,8 @@ function captchaMac(code: unknown, expiresAt: number): string {
.
digest
(
"hex"
);
.
digest
(
"hex"
);
}
}
function
captchaValid
(
request
:
Request
,
answer
:
unknown
):
boolean
{
function
captchaValid
(
request
:
Request
,
answer
:
unknown
,
token
?:
unknown
):
boolean
{
const
value
=
cookies
(
request
).
cms_captcha
;
const
value
=
String
(
token
||
cookies
(
request
).
cms_captcha
||
""
)
;
if
(
!
value
||
!
answer
)
return
false
;
if
(
!
value
||
!
answer
)
return
false
;
const
separator
=
value
.
lastIndexOf
(
"."
);
const
separator
=
value
.
lastIndexOf
(
"."
);
const
expiresAt
=
Number
(
value
.
slice
(
0
,
separator
));
const
expiresAt
=
Number
(
value
.
slice
(
0
,
separator
));
...
@@ -208,11 +208,13 @@ export async function handleCmsApi(request: Request, routeValue: string, clientA
...
@@ -208,11 +208,13 @@ export async function handleCmsApi(request: Request, routeValue: string, clientA
if
(
route
===
"captcha"
&&
method
===
"GET"
)
{
if
(
route
===
"captcha"
&&
method
===
"GET"
)
{
const
code
=
randomCaptcha
();
const
code
=
randomCaptcha
();
const
expiresAt
=
Date
.
now
()
+
CAPTCHA_TTL
;
const
expiresAt
=
Date
.
now
()
+
CAPTCHA_TTL
;
const
token
=
`
${
expiresAt
}
.
${
captchaMac
(
code
,
expiresAt
)}
`
;
return
new
Response
(
captchaSvg
(
code
),
{
return
new
Response
(
captchaSvg
(
code
),
{
headers
:
{
headers
:
{
"Cache-Control"
:
"no-store"
,
"Cache-Control"
:
"no-store"
,
"Content-Type"
:
"image/svg+xml; charset=utf-8"
,
"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
...
@@ -224,7 +226,7 @@ export async function handleCmsApi(request: Request, routeValue: string, clientA
if
(
blockedFor
)
return
json
({
error
:
`尝试过于频繁,请
${
Math
.
ceil
(
blockedFor
/
60
)}
分钟后再试`
},
429
);
if
(
blockedFor
)
return
json
({
error
:
`尝试过于频繁,请
${
Math
.
ceil
(
blockedFor
/
60
)}
分钟后再试`
},
429
);
const
body
=
await
bodyOf
(
request
);
const
body
=
await
bodyOf
(
request
);
const
clearCaptcha
=
cookieHeader
(
request
,
"cms_captcha"
,
""
,
0
);
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
});
return
json
({
error
:
"验证码错误或已过期"
},
400
,
{
"Set-Cookie"
:
clearCaptcha
});
}
}
if
(
!
await
verifyPassword
(
body
.
password
))
{
if
(
!
await
verifyPassword
(
body
.
password
))
{
...
...
src/pages/index.astro
View file @
330212cd
...
@@ -18,24 +18,24 @@ const subjects = [
...
@@ -18,24 +18,24 @@ const subjects = [
{ icon: "chemistry", tone: "green", name: "化学实验", desc: "发现元素世界,理解物质变化" },
{ icon: "chemistry", tone: "green", name: "化学实验", desc: "发现元素世界,理解物质变化" },
{ icon: "history", tone: "purple", name: "历史人文", desc: "穿越时空长河,传承文明智慧" },
{ icon: "history", tone: "purple", name: "历史人文", desc: "穿越时空长河,传承文明智慧" },
] as const;
] 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">
<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="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 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-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>}
{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 />
<CTA />
...
...
src/styles/global.css
View file @
330212cd
...
@@ -5,7 +5,7 @@
...
@@ -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
)}
.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.08
fr
.92
fr
;
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
)}
.home-hero
{
overflow
:
hidden
;
padding
:
56px
0
0
;
background
:
#fff
}
.home-hero__grid
{
display
:
grid
;
grid-template-columns
:
1.08
fr
.92
fr
;
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
,
1
fr
);
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
}
.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
,
1
fr
);
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
,
1
fr
)}
.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
,
1
fr
);
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
:
.9
fr
1.1
fr
;
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
:
1
fr
.9
fr
;
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
:
1
fr
1
fr
;
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
:
.65
fr
1.35
fr
;
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
:
1
fr
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
,
1
fr
)}
.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
,
1
fr
);
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
1
fr
;
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
:
.9
fr
1.1
fr
;
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
:
1
fr
.9
fr
;
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
:
1
fr
1
fr
;
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
:
.65
fr
1.35
fr
;
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
:
1
fr
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
,
1
fr
)}
.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
:
.85
fr
1.15
fr
;
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
,
1
fr
);
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
)}
.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
,
1
fr
)}
.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
:
.85
fr
1.15
fr
;
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
,
1
fr
);
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
:
1
fr
.85
fr
;
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
:
.8
fr
1.2
fr
;
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
:
1
fr
1
fr
;
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
,
1
fr
);
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
,
1
fr
)}
.related-services
a
{
padding
:
25px
;
border-right
:
1px
solid
var
(
--border
);
text-align
:
center
}
.service-hero
{
padding
:
86px
0
;
background
:
#fff
}
.service-hero__inner
{
display
:
grid
;
grid-template-columns
:
1
fr
.85
fr
;
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
:
.8
fr
1.2
fr
;
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
:
1
fr
1
fr
;
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
,
1
fr
);
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
,
1
fr
)}
.related-services
a
{
padding
:
25px
;
border-right
:
1px
solid
var
(
--border
);
text-align
:
center
}
.about-intro-layout
{
display
:
grid
;
grid-template-columns
:
.8
fr
1.2
fr
;
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
,
1
fr
);
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
:
.8
fr
1.2
fr
;
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
:
.7
fr
1.3
fr
;
gap
:
75px
}
.delivery-grid
{
display
:
grid
;
grid-template-columns
:
1
fr
1
fr
;
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
,
1
fr
);
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
)}
.about-intro-layout
{
display
:
grid
;
grid-template-columns
:
.8
fr
1.2
fr
;
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
,
1
fr
);
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
:
.8
fr
1.2
fr
;
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
:
.7
fr
1.3
fr
;
gap
:
75px
}
.delivery-grid
{
display
:
grid
;
grid-template-columns
:
1
fr
1
fr
;
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
,
1
fr
);
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 @@
...
@@ -13,7 +13,7 @@
.article-layout
{
display
:
grid
;
grid-template-columns
:
250px
1
fr
;
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
,
1
fr
);
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
)}
.article-layout
{
display
:
grid
;
grid-template-columns
:
250px
1
fr
;
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
,
1
fr
);
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.4
fr
.7
fr
.8
fr
1
fr
;
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
:
1
fr
1
fr
;
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
}
.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.4
fr
.7
fr
.8
fr
1
fr
;
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
:
1
fr
1
fr
;
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
,
1
fr
)}
.need-grid
{
grid-template-columns
:
1
fr
1
fr
}
.teacher-layout
,
.feedback-layout
{
gap
:
50px
}
.footer-grid
{
grid-template-columns
:
1.2
fr
1
fr
1
fr
}
.footer-contact
{
grid-column
:
2
/
4
}}
@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
,
1
fr
)}
.need-grid
{
grid-template-columns
:
1
fr
1
fr
}
.teacher-layout
,
.feedback-layout
{
gap
:
50px
}
.footer-grid
{
grid-template-columns
:
1.2
fr
1
fr
1
fr
}
.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
:
1
fr
;
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
:
1
fr
}
.subject-overview__grid
article
{
min-height
:
auto
;
padding
:
24px
}
.stat-grid
{
grid-template-columns
:
1
fr
1
fr
;
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
:
1
fr
}
.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
:
1
fr
;
gap
:
25px
;
padding
:
28px
}
.service-hero
{
padding
:
62px
0
}
.process-grid
,
.method-flow
{
grid-template-columns
:
1
fr
1
fr
}
.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
:
1
fr
}
.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
:
1
fr
1
fr
}
.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
:
1
fr
;
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
:
1
fr
}
.subject-overview__grid
article
{
min-height
:
auto
;
padding
:
24px
}
.stat-grid
{
grid-template-columns
:
1
fr
1
fr
;
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
:
1
fr
}
.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
:
1
fr
;
gap
:
25px
;
padding
:
28px
}
.service-hero
{
padding
:
62px
0
}
.process-grid
,
.method-flow
{
grid-template-columns
:
1
fr
1
fr
}
.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
:
1
fr
}
.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
:
1
fr
1
fr
}
.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
(
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
}}
@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
}
.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
}
...
...
tests/auth.test.ts
View file @
330212cd
...
@@ -41,7 +41,7 @@ async function login(password: string): Promise<{ response: Response; cookie?: s
...
@@ -41,7 +41,7 @@ async function login(password: string): Promise<{ response: Response; cookie?: s
"Content-Type"
:
"application/json"
,
"Content-Type"
:
"application/json"
,
Cookie
:
cookieFrom
(
captchaResponse
,
"cms_captcha"
),
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
}
`
);
}),
"session"
,
`login-
${
requestNumber
}
`
);
return
{
return
{
response
,
response
,
...
@@ -64,6 +64,40 @@ async function changePassword(cookie: string, currentPassword: string, newPasswo
...
@@ -64,6 +64,40 @@ async function changePassword(cookie: string, currentPassword: string, newPasswo
}),
"account/password"
);
}),
"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
()
=>
{
test
(
"users can change the persisted password while the .env password remains valid"
,
async
()
=>
{
const
initialLogin
=
await
login
(
"environment-master-password"
);
const
initialLogin
=
await
login
(
"environment-master-password"
);
assert
.
equal
(
initialLogin
.
response
.
status
,
200
);
assert
.
equal
(
initialLogin
.
response
.
status
,
200
);
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment