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
6dc1c171
Commit
6dc1c171
authored
Aug 14, 2026
by
tao355667
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: refine contact UI and admin filters
parent
424c9f87
Changes
6
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
60 additions
and
23 deletions
+60
-23
app.js
public/admin/app.js
+19
-7
style.css
public/admin/style.css
+7
-0
ContactDialog.astro
src/components/ContactDialog.astro
+23
-8
Base.astro
src/layouts/Base.astro
+2
-2
index.astro
src/pages/admin/index.astro
+4
-1
global.css
src/styles/global.css
+5
-5
No files found.
public/admin/app.js
View file @
6dc1c171
...
@@ -15,6 +15,7 @@ let currentUser = null;
...
@@ -15,6 +15,7 @@ let currentUser = null;
let
consultationTeachers
=
[];
let
consultationTeachers
=
[];
let
staffAccounts
=
[];
let
staffAccounts
=
[];
let
activeConsultationUser
=
null
;
let
activeConsultationUser
=
null
;
let
articleRecords
=
[];
const
consultationStatusLabels
=
{
unprocessed
:
"未处理"
,
added
:
"已添加"
,
not_added
:
"未添加"
};
const
consultationStatusLabels
=
{
unprocessed
:
"未处理"
,
added
:
"已添加"
,
not_added
:
"未添加"
};
const
ADMIN_STATE_KEY
=
"qiyouxue-admin-state"
;
const
ADMIN_STATE_KEY
=
"qiyouxue-admin-state"
;
...
@@ -563,15 +564,16 @@ function applyStatus(element, status) {
...
@@ -563,15 +564,16 @@ function applyStatus(element, status) {
element.textContent = label;
element.textContent = label;
}
}
async function loadArticles() {
function renderArticles() {
const articles = await api("
/
articles
").catch((error) => { alert(error.message); return []; });
const list = $("
#
article
-
list
");
const list = $("
#
article
-
list
");
list.innerHTML = "";
list.innerHTML = "";
const pending = articles.filter((item) => item.publishStatus !== "
live
").length;
if (!articleRecords.length) { list.innerHTML = '<div class="
empty
">还没有文章,点击“新建文章”开始。</div>'; return; }
$("
#
pending
-
badge
").textContent = `${pending} 篇草稿待发布`;
const selectedStatus = $("
#
article
-
status
-
filter
").value;
$("
#
pending
-
badge
").classList.toggle("
hidden
", pending === 0);
const visibleArticles = selectedStatus === "
all
"
if (!articles.length) { list.innerHTML = '<div class="
empty
">还没有文章,点击“新建文章”开始。</div>'; return; }
? articleRecords
for (const article of articles) {
: articleRecords.filter((item) => item.publishStatus === selectedStatus);
if (!visibleArticles.length) { list.innerHTML = '<div class="
empty
">当前筛选条件下没有文章。</div>'; return; }
for (const article of visibleArticles) {
const card = document.createElement("
article
");
const card = document.createElement("
article
");
card.className = "
article
-
row
";
card.className = "
article
-
row
";
card.innerHTML = '<div class="
article
-
summary
"><h2></h2><p><span class="
category
"></span><span class="
updated
"></span><span class="
status
"></span></p></div><div class="
row
-
actions
"></div>';
card.innerHTML = '<div class="
article
-
summary
"><h2></h2><p><span class="
category
"></span><span class="
updated
"></span><span class="
status
"></span></p></div><div class="
row
-
actions
"></div>';
...
@@ -589,6 +591,16 @@ async function loadArticles() {
...
@@ -589,6 +591,16 @@ async function loadArticles() {
}
}
}
}
async function loadArticles() {
articleRecords = await api("
/
articles
").catch((error) => { alert(error.message); return []; });
const pending = articleRecords.filter((item) => item.publishStatus !== "
live
").length;
$("
#
pending
-
badge
").textContent = `${pending} 篇草稿待发布`;
$("
#
pending
-
badge
").classList.toggle("
hidden
", pending === 0);
renderArticles();
}
$("
#
article
-
status
-
filter
").addEventListener("
change
", renderArticles);
function listAction(label, className, action) {
function listAction(label, className, action) {
const button = document.createElement("
button
");
const button = document.createElement("
button
");
button.type = "
button
";
button.type = "
button
";
...
...
public/admin/style.css
View file @
6dc1c171
...
@@ -47,6 +47,10 @@ input:focus, textarea:focus, select:focus { outline: 2px solid rgba(20,115,230,.
...
@@ -47,6 +47,10 @@ input:focus, textarea:focus, select:focus { outline: 2px solid rgba(20,115,230,.
.view-head
p
{
margin
:
0
0
5px
;
color
:
var
(
--primary-dark
);
font-size
:
11px
;
font-weight
:
700
;
letter-spacing
:
.16em
;
}
.view-head
p
{
margin
:
0
0
5px
;
color
:
var
(
--primary-dark
);
font-size
:
11px
;
font-weight
:
700
;
letter-spacing
:
.16em
;
}
.view-head
h1
{
margin
:
0
;
font-size
:
30px
;
}
.view-head
h1
{
margin
:
0
;
font-size
:
30px
;
}
.view-actions
{
display
:
flex
;
gap
:
9px
;
}
.view-actions
{
display
:
flex
;
gap
:
9px
;
}
.article-toolbar
{
align-items
:
center
;
justify-content
:
space-between
;
gap
:
18px
;
}
.article-status-filter
{
display
:
flex
;
align-items
:
center
;
gap
:
9px
;
color
:
var
(
--muted
);
font-size
:
13px
;
}
.article-status-filter
>
span
{
white-space
:
nowrap
;
}
.article-status-filter
select
{
width
:
156px
;
min-height
:
40px
;
padding-block
:
8px
;
}
.import-status
{
min-height
:
20px
;
margin
:
-16px
0
12px
;
color
:
var
(
--muted
);
font-size
:
13px
;
text-align
:
right
;
}
.import-status
{
min-height
:
20px
;
margin
:
-16px
0
12px
;
color
:
var
(
--muted
);
font-size
:
13px
;
text-align
:
right
;
}
.article-list
{
display
:
grid
;
gap
:
10px
;
}
.article-list
{
display
:
grid
;
gap
:
10px
;
}
.article-row
{
padding
:
18px
20px
;
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
gap
:
20px
;
background
:
white
;
border
:
1px
solid
var
(
--border
);
border-radius
:
14px
;
}
.article-row
{
padding
:
18px
20px
;
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
gap
:
20px
;
background
:
white
;
border
:
1px
solid
var
(
--border
);
border-radius
:
14px
;
}
...
@@ -182,6 +186,7 @@ input:focus, textarea:focus, select:focus { outline: 2px solid rgba(20,115,230,.
...
@@ -182,6 +186,7 @@ input:focus, textarea:focus, select:focus { outline: 2px solid rgba(20,115,230,.
.metric-grid
small
{
color
:
var
(
--muted
);
font-size
:
12px
;
}
.metric-grid
small
{
color
:
var
(
--muted
);
font-size
:
12px
;
}
.metric-grid
strong
{
color
:
#12365f
;
font-size
:
20px
;
}
.metric-grid
strong
{
color
:
#12365f
;
font-size
:
20px
;
}
.table-card
{
overflow-x
:
auto
;
overflow-y
:
hidden
;
background
:
white
;
border
:
1px
solid
var
(
--border
);
border-radius
:
10px
;
scrollbar-color
:
#b8c7d8
#f2f5f8
;
scrollbar-width
:
thin
;
}
.table-card
{
overflow-x
:
auto
;
overflow-y
:
hidden
;
background
:
white
;
border
:
1px
solid
var
(
--border
);
border-radius
:
10px
;
scrollbar-color
:
#b8c7d8
#f2f5f8
;
scrollbar-width
:
thin
;
}
.table-card
.empty
{
border
:
0
;
border-radius
:
0
;
}
.table-card
::-webkit-scrollbar
{
height
:
9px
;
}
.table-card
::-webkit-scrollbar
{
height
:
9px
;
}
.table-card
::-webkit-scrollbar-track
{
background
:
#f2f5f8
;
}
.table-card
::-webkit-scrollbar-track
{
background
:
#f2f5f8
;
}
.table-card
::-webkit-scrollbar-thumb
{
background
:
#b8c7d8
;
border
:
2px
solid
#f2f5f8
;
border-radius
:
999px
;
}
.table-card
::-webkit-scrollbar-thumb
{
background
:
#b8c7d8
;
border
:
2px
solid
#f2f5f8
;
border-radius
:
999px
;
}
...
@@ -283,6 +288,8 @@ input:focus, textarea:focus, select:focus { outline: 2px solid rgba(20,115,230,.
...
@@ -283,6 +288,8 @@ input:focus, textarea:focus, select:focus { outline: 2px solid rgba(20,115,230,.
@media
(
max-width
:
760px
)
{
@media
(
max-width
:
760px
)
{
.topbar
,
.view-head
,
.article-row
{
align-items
:
flex-start
;
flex-direction
:
column
;
}
.topbar
,
.view-head
,
.article-row
{
align-items
:
flex-start
;
flex-direction
:
column
;
}
.top-actions
{
width
:
100%
;
flex-wrap
:
wrap
;
}
.top-actions
{
width
:
100%
;
flex-wrap
:
wrap
;
}
.article-toolbar
{
align-items
:
stretch
;
flex-direction
:
column
;
}
.article-status-filter
{
justify-content
:
space-between
;
}
.view-actions
{
width
:
100%
;
}
.view-actions
{
width
:
100%
;
}
.view-actions
button
{
flex
:
1
;
}
.view-actions
button
{
flex
:
1
;
}
.field-row
,
.editor-grid
{
grid-template-columns
:
1
fr
;
}
.field-row
,
.editor-grid
{
grid-template-columns
:
1
fr
;
}
...
...
src/components/ContactDialog.astro
View file @
6dc1c171
...
@@ -22,16 +22,31 @@ import { site } from "../data/site";
...
@@ -22,16 +22,31 @@ import { site } from "../data/site";
<div class="contact-dialog__panel">
<div class="contact-dialog__panel">
<button class="contact-dialog__close" type="button" data-phone-dialog-close aria-label="关闭官方电话信息">×</button>
<button class="contact-dialog__close" type="button" data-phone-dialog-close aria-label="关闭官方电话信息">×</button>
<div class="phone-dialog__heading">
<div class="phone-dialog__heading">
<span>OFFICIAL CONTACT</span>
<h2 id="phone-dialog-title">官方联系方式</h2>
<h2 id="phone-dialog-title">官方电话</h2>
<p>课程咨询、服务反馈与商务合作,可通过以下官方渠道联系我们。</p>
<p>课程咨询、教师匹配与服务安排,欢迎致电了解。</p>
</div>
</div>
<a class="phone-dialog__number" href={`tel:${site.contact.phone}`}>{site.contact.phoneDisplay}</a>
<div class="official-contact-list">
<div class="phone-dialog__details">
<section class="official-contact-item official-contact-item--phone">
<span>服务时间:周一至周日 · 7×24 小时</span>
<span>客服电话</span>
<a href={`mailto:${site.contact.email}`}>{site.contact.email}</a>
<a href={`tel:${site.contact.phone}`}>{site.contact.phoneDisplay}</a>
<p>课程咨询与服务反馈</p>
</section>
<section class="official-contact-item">
<span>微信小程序</span>
<strong>启优学一对一</strong>
<p>搜索小程序名称提交预约</p>
</section>
<section class="official-contact-item">
<span>联系邮箱</span>
<a href={`mailto:${site.contact.email}`}>{site.contact.email}</a>
<p>商务合作与服务反馈</p>
</section>
<section class="official-contact-item">
<span>官方网站</span>
<a href={`https://${site.contact.website}`}>{site.contact.website}</a>
<p>官方课程与服务信息</p>
</section>
</div>
</div>
<a class="phone-dialog__call" href={`tel:${site.contact.phone}`}>立即拨打</a>
</div>
</div>
</dialog>
</dialog>
...
...
src/layouts/Base.astro
View file @
6dc1c171
...
@@ -18,5 +18,5 @@ const webPageLd = { "@context": "https://schema.org", "@type": "WebPage", "@id":
...
@@ -18,5 +18,5 @@ const webPageLd = { "@context": "https://schema.org", "@type": "WebPage", "@id":
const pageLd = Array.isArray(jsonLd) ? jsonLd : [jsonLd]; const serializedLd = JSON.stringify([organizationLd, websiteLd, webPageLd, ...pageLd.filter((item) => Object.keys(item).length > 0)]).replace(/
</g
,
"\\u003c");
const pageLd = Array.isArray(jsonLd) ? jsonLd : [jsonLd]; const serializedLd = JSON.stringify([organizationLd, websiteLd, webPageLd, ...pageLd.filter((item) => Object.keys(item).length > 0)]).replace(/
</g
,
"\\u003c");
---
---
<!doctype
html
><html
lang=
"zh-CN"
><head><meta
charset=
"UTF-8"
/><meta
name=
"viewport"
content=
"width=device-width, initial-scale=1"
/><meta
name=
"theme-color"
content=
"#1473E6"
/><meta
name=
"color-scheme"
content=
"light"
/><title>
{metaTitle}
</title><meta
name=
"description"
content=
{metaDescription}
/><meta
name=
"keywords"
content=
{metaKeywords}
/><meta
name=
"robots"
content=
{noindex
?
"
noindex
,
nofollow
"
:
"
index
,
follow
,
max-image-preview:large
,
max-snippet:-1
,
max-video-preview:-1
"}
/><link
rel=
"canonical"
href=
{canonical}
/>
{prevUrl
&&
<link
rel=
"prev"
href=
{prevUrl}
/>
}{nextUrl
&&
<link
rel=
"next"
href=
{nextUrl}
/>
}
<link
rel=
"icon"
href=
"/favicon.png"
type=
"image/png"
sizes=
"512x512"
/><link
rel=
"apple-touch-icon"
href=
"/favicon.png"
/><link
rel=
"manifest"
href=
"/site.webmanifest"
/><link
rel=
"alternate"
hreflang=
"zh-CN"
href=
{canonical}
/><link
rel=
"alternate"
hreflang=
"x-default"
href=
{canonical}
/><meta
property=
"og:type"
content=
{ogType}
/><meta
property=
"og:title"
content=
{metaTitle}
/><meta
property=
"og:description"
content=
{metaDescription}
/><meta
property=
"og:url"
content=
{canonical}
/><meta
property=
"og:image"
content=
{ogImage}
/><meta
property=
"og:image:alt"
content=
{imageAlt}
/><meta
property=
"og:site_name"
content=
{site.brand.name}
/><meta
property=
"og:locale"
content=
"zh_CN"
/>
{publishedTime
&&
<meta
property=
"article:published_time"
content=
{publishedTime}
/>
}{modifiedTime
&&
<meta
property=
"article:modified_time"
content=
{modifiedTime}
/>
}{articleSection
&&
<meta
property=
"article:section"
content=
{articleSection}
/>
}{articleAuthor
&&
<meta
property=
"article:author"
content=
{articleAuthor}
/>
}
<meta
name=
"twitter:card"
content=
"summary_large_image"
/><meta
name=
"twitter:title"
content=
{metaTitle}
/><meta
name=
"twitter:description"
content=
{metaDescription}
/><meta
name=
"twitter:image"
content=
{ogImage}
/><script
type=
"application/ld+json"
set:html=
{serializedLd}
is:inline
></script></head><body><a
class=
"skip-link"
href=
"#main-content"
>
跳到主要内容
</a><slot
/><ContactDialog
/><script
is:inline
>
<!doctype
html
><html
lang=
"zh-CN"
><head><meta
charset=
"UTF-8"
/><meta
name=
"viewport"
content=
"width=device-width, initial-scale=1"
/><meta
name=
"theme-color"
content=
"#1473E6"
/><meta
name=
"color-scheme"
content=
"light"
/><title>
{metaTitle}
</title><meta
name=
"description"
content=
{metaDescription}
/><meta
name=
"keywords"
content=
{metaKeywords}
/><meta
name=
"robots"
content=
{noindex
?
"
noindex
,
nofollow
"
:
"
index
,
follow
,
max-image-preview:large
,
max-snippet:-1
,
max-video-preview:-1
"}
/><link
rel=
"canonical"
href=
{canonical}
/>
{prevUrl
&&
<link
rel=
"prev"
href=
{prevUrl}
/>
}{nextUrl
&&
<link
rel=
"next"
href=
{nextUrl}
/>
}
<link
rel=
"icon"
href=
"/favicon.png"
type=
"image/png"
sizes=
"512x512"
/><link
rel=
"apple-touch-icon"
href=
"/favicon.png"
/><link
rel=
"manifest"
href=
"/site.webmanifest"
/><link
rel=
"alternate"
hreflang=
"zh-CN"
href=
{canonical}
/><link
rel=
"alternate"
hreflang=
"x-default"
href=
{canonical}
/><meta
property=
"og:type"
content=
{ogType}
/><meta
property=
"og:title"
content=
{metaTitle}
/><meta
property=
"og:description"
content=
{metaDescription}
/><meta
property=
"og:url"
content=
{canonical}
/><meta
property=
"og:image"
content=
{ogImage}
/><meta
property=
"og:image:alt"
content=
{imageAlt}
/><meta
property=
"og:site_name"
content=
{site.brand.name}
/><meta
property=
"og:locale"
content=
"zh_CN"
/>
{publishedTime
&&
<meta
property=
"article:published_time"
content=
{publishedTime}
/>
}{modifiedTime
&&
<meta
property=
"article:modified_time"
content=
{modifiedTime}
/>
}{articleSection
&&
<meta
property=
"article:section"
content=
{articleSection}
/>
}{articleAuthor
&&
<meta
property=
"article:author"
content=
{articleAuthor}
/>
}
<meta
name=
"twitter:card"
content=
"summary_large_image"
/><meta
name=
"twitter:title"
content=
{metaTitle}
/><meta
name=
"twitter:description"
content=
{metaDescription}
/><meta
name=
"twitter:image"
content=
{ogImage}
/><script
type=
"application/ld+json"
set:html=
{serializedLd}
is:inline
></script></head><body><a
class=
"skip-link"
href=
"#main-content"
>
跳到主要内容
</a><slot
/><ContactDialog
/><script
is:inline
>
const
header
=
document
.
querySelector
(
'[data-header]'
);
const
syncHeader
=
()
=>
header
?.
classList
.
toggle
(
'is-scrolled'
,
scrollY
>
12
);
addEventListener
(
'scroll'
,
syncHeader
,{
passive
:
true
});
syncHeader
();
const
observer
=
'IntersectionObserver'
in
window
?
new
IntersectionObserver
(
entries
=>
entries
.
forEach
(
entry
=>
{
if
(
entry
.
isIntersecting
){
entry
.
target
.
classList
.
add
(
'is-visible'
);
observer
.
unobserve
(
entry
.
target
)}}),{
threshold
:.
08
}):
null
;
document
.
querySelectorAll
(
'[data-reveal]'
).
forEach
(
el
=>
observer
?
observer
.
observe
(
el
):
el
.
classList
.
add
(
'is-visible'
));
document
.
querySelectorAll
(
'[data-mobile-nav] a'
).
forEach
(
link
=>
link
.
addEventListener
(
'click'
,()
=>
link
.
closest
(
'details'
)?.
removeAttribute
(
'open'
)));
const
dialog
=
document
.
querySelector
(
'[data-contact-dialog]'
);
const
phoneDialog
=
document
.
querySelector
(
'[data-phone-dialog]'
);
const
teacherDialog
=
document
.
querySelector
(
'[data-teacher-dialog]'
);
const
phoneTrigger
=
document
.
querySelector
(
'[data-phone-contact]'
);
const
form
=
dialog
?.
querySelector
(
'[data-consultation-form]'
);
const
copyWechat
=
teacherDialog
?.
querySelector
(
'[data-copy-teacher-wechat]'
);
const
resetConsultation
=
()
=>
{
form
?.
reset
();
const
error
=
dialog
?.
querySelector
(
'[data-consultation-error]'
);
if
(
error
)
error
.
textContent
=
''
};
document
.
addEventListener
(
'click'
,
event
=>
{
const
trigger
=
event
.
target
.
closest
(
'[data-open-contact]'
);
if
(
!
dialog
||!
trigger
)
return
;
event
.
preventDefault
();
resetConsultation
();
dialog
.
showModal
();
dialog
.
querySelector
(
'#consultation-phone'
)?.
focus
()});
phoneTrigger
?.
addEventListener
(
'click'
,
event
=>
{
if
(
matchMedia
(
'(max-width:760px)'
).
matches
)
return
;
event
.
preventDefault
()
;
phoneDialog
?.
showModal
()});
dialog
?.
querySelector
(
'[data-contact-dialog-close]'
)?.
addEventListener
(
'click'
,()
=>
dialog
.
close
());
phoneDialog
?.
querySelector
(
'[data-phone-dialog-close]'
)?.
addEventListener
(
'click'
,()
=>
phoneDialog
.
close
());
teacherDialog
?.
querySelector
(
'[data-teacher-dialog-close]'
)?.
addEventListener
(
'click'
,()
=>
teacherDialog
.
close
());[
dialog
,
phoneDialog
,
teacherDialog
].
forEach
(
item
=>
item
?.
addEventListener
(
'click'
,
event
=>
{
if
(
event
.
target
===
item
)
item
.
close
()}));
copyWechat
?.
addEventListener
(
'click'
,
async
()
=>
{
const
value
=
teacherDialog
.
querySelector
(
'[data-teacher-wechat]'
).
textContent
.
trim
();
if
(
!
value
)
return
;
try
{
await
navigator
.
clipboard
.
writeText
(
value
);
copyWechat
.
textContent
=
'已复制'
;
setTimeout
(()
=>
{
copyWechat
.
textContent
=
'复制'
},
1600
)}
catch
{
copyWechat
.
textContent
=
'复制失败'
;
setTimeout
(()
=>
{
copyWechat
.
textContent
=
'复制'
},
1600
)}});
form
?.
addEventListener
(
'submit'
,
async
event
=>
{
event
.
preventDefault
();
const
phone
=
String
(
new
FormData
(
form
).
get
(
'phone'
)
||
''
).
replace
(
/
\s
+/g
,
''
);
const
error
=
dialog
.
querySelector
(
'[data-consultation-error]'
);
const
submit
=
form
.
querySelector
(
'button[type="submit"]'
);
error
.
textContent
=
''
;
if
(
!
/^1
[
3-9
]\d{9}
$/
.
test
(
phone
)){
error
.
textContent
=
'请输入正确的 11 位手机号'
;
return
}
submit
.
disabled
=
true
;
submit
.
textContent
=
'正在领取…'
;
try
{
const
response
=
await
fetch
(
'/api/cms/consultations/claim'
,{
method
:
'POST'
,
headers
:{
'Content-Type'
:
'application/json'
},
body
:
JSON
.
stringify
({
phone
,
source
:
'免费领取学情分析'
,
page
:
location
.
href
})});
const
result
=
await
response
.
json
();
if
(
!
response
.
ok
)
throw
new
Error
(
result
.
error
||
'提交失败,请稍后重试'
);
teacherDialog
.
querySelector
(
'[data-teacher-name]'
).
textContent
=
result
.
teacher
.
name
;
teacherDialog
.
querySelector
(
'[data-teacher-title]'
).
textContent
=
result
.
teacher
.
title
;
teacherDialog
.
querySelector
(
'[data-teacher-qr]'
).
src
=
result
.
teacher
.
qrCodeUrl
;
teacherDialog
.
querySelector
(
'[data-teacher-wechat]'
).
textContent
=
result
.
teacher
.
wechat
||
''
;
teacherDialog
.
querySelector
(
'[data-teacher-wechat-wrap]'
).
classList
.
toggle
(
'hidden'
,
!
result
.
teacher
.
wechat
);
copyWechat
.
textContent
=
'复制'
;
dialog
.
close
();
teacherDialog
.
showModal
()}
catch
(
requestError
){
error
.
textContent
=
requestError
.
message
}
finally
{
submit
.
disabled
=
false
;
submit
.
textContent
=
'立即领取'
}});
const
header
=
document
.
querySelector
(
'[data-header]'
);
const
syncHeader
=
()
=>
header
?.
classList
.
toggle
(
'is-scrolled'
,
scrollY
>
12
);
addEventListener
(
'scroll'
,
syncHeader
,{
passive
:
true
});
syncHeader
();
const
observer
=
'IntersectionObserver'
in
window
?
new
IntersectionObserver
(
entries
=>
entries
.
forEach
(
entry
=>
{
if
(
entry
.
isIntersecting
){
entry
.
target
.
classList
.
add
(
'is-visible'
);
observer
.
unobserve
(
entry
.
target
)}}),{
threshold
:.
08
}):
null
;
document
.
querySelectorAll
(
'[data-reveal]'
).
forEach
(
el
=>
observer
?
observer
.
observe
(
el
):
el
.
classList
.
add
(
'is-visible'
));
document
.
querySelectorAll
(
'[data-mobile-nav] a'
).
forEach
(
link
=>
link
.
addEventListener
(
'click'
,()
=>
link
.
closest
(
'details'
)?.
removeAttribute
(
'open'
)));
const
dialog
=
document
.
querySelector
(
'[data-contact-dialog]'
);
const
phoneDialog
=
document
.
querySelector
(
'[data-phone-dialog]'
);
const
teacherDialog
=
document
.
querySelector
(
'[data-teacher-dialog]'
);
const
form
=
dialog
?.
querySelector
(
'[data-consultation-form]'
);
const
copyWechat
=
teacherDialog
?.
querySelector
(
'[data-copy-teacher-wechat]'
);
const
resetConsultation
=
()
=>
{
form
?.
reset
();
const
error
=
dialog
?.
querySelector
(
'[data-consultation-error]'
);
if
(
error
)
error
.
textContent
=
''
};
document
.
addEventListener
(
'click'
,
event
=>
{
const
trigger
=
event
.
target
.
closest
(
'[data-open-contact]'
);
if
(
!
dialog
||!
trigger
)
return
;
event
.
preventDefault
();
resetConsultation
();
dialog
.
showModal
();
dialog
.
querySelector
(
'#consultation-phone'
)?.
focus
()});
document
.
addEventListener
(
'click'
,
event
=>
{
if
(
!
event
.
target
.
closest
(
'[data-phone-contact]'
))
return
;
phoneDialog
?.
showModal
()});
dialog
?.
querySelector
(
'[data-contact-dialog-close]'
)?.
addEventListener
(
'click'
,()
=>
dialog
.
close
());
phoneDialog
?.
querySelector
(
'[data-phone-dialog-close]'
)?.
addEventListener
(
'click'
,()
=>
phoneDialog
.
close
());
teacherDialog
?.
querySelector
(
'[data-teacher-dialog-close]'
)?.
addEventListener
(
'click'
,()
=>
teacherDialog
.
close
());[
dialog
,
phoneDialog
,
teacherDialog
].
forEach
(
item
=>
item
?.
addEventListener
(
'click'
,
event
=>
{
if
(
event
.
target
===
item
)
item
.
close
()}));
copyWechat
?.
addEventListener
(
'click'
,
async
()
=>
{
const
value
=
teacherDialog
.
querySelector
(
'[data-teacher-wechat]'
).
textContent
.
trim
();
if
(
!
value
)
return
;
try
{
await
navigator
.
clipboard
.
writeText
(
value
);
copyWechat
.
textContent
=
'已复制'
;
setTimeout
(()
=>
{
copyWechat
.
textContent
=
'复制'
},
1600
)}
catch
{
copyWechat
.
textContent
=
'复制失败'
;
setTimeout
(()
=>
{
copyWechat
.
textContent
=
'复制'
},
1600
)}});
form
?.
addEventListener
(
'submit'
,
async
event
=>
{
event
.
preventDefault
();
const
phone
=
String
(
new
FormData
(
form
).
get
(
'phone'
)
||
''
).
replace
(
/
\s
+/g
,
''
);
const
error
=
dialog
.
querySelector
(
'[data-consultation-error]'
);
const
submit
=
form
.
querySelector
(
'button[type="submit"]'
);
error
.
textContent
=
''
;
if
(
!
/^1
[
3-9
]\d{9}
$/
.
test
(
phone
)){
error
.
textContent
=
'请输入正确的 11 位手机号'
;
return
}
submit
.
disabled
=
true
;
submit
.
textContent
=
'正在领取…'
;
try
{
const
response
=
await
fetch
(
'/api/cms/consultations/claim'
,{
method
:
'POST'
,
headers
:{
'Content-Type'
:
'application/json'
},
body
:
JSON
.
stringify
({
phone
,
source
:
'免费领取学情分析'
,
page
:
location
.
href
})});
const
result
=
await
response
.
json
();
if
(
!
response
.
ok
)
throw
new
Error
(
result
.
error
||
'提交失败,请稍后重试'
);
teacherDialog
.
querySelector
(
'[data-teacher-name]'
).
textContent
=
result
.
teacher
.
name
;
teacherDialog
.
querySelector
(
'[data-teacher-title]'
).
textContent
=
result
.
teacher
.
title
;
teacherDialog
.
querySelector
(
'[data-teacher-qr]'
).
src
=
result
.
teacher
.
qrCodeUrl
;
teacherDialog
.
querySelector
(
'[data-teacher-wechat]'
).
textContent
=
result
.
teacher
.
wechat
||
''
;
teacherDialog
.
querySelector
(
'[data-teacher-wechat-wrap]'
).
classList
.
toggle
(
'hidden'
,
!
result
.
teacher
.
wechat
);
copyWechat
.
textContent
=
'复制'
;
dialog
.
close
();
teacherDialog
.
showModal
()}
catch
(
requestError
){
error
.
textContent
=
requestError
.
message
}
finally
{
submit
.
disabled
=
false
;
submit
.
textContent
=
'立即领取'
}});
</script><
a
class=
"floating-phone"
href=
{`tel:${site.contact.phone}`}
data-phone-contact
aria-label=
{`官方电话
${
site
.
contact
.
phoneDisplay
},点击查看联系信息或拨号
`}
><svg
viewBox=
"0 0 24 24"
aria-hidden=
"true"
><path
d=
"M7.2 4.5 5.4 5.8c-.8.6-.9 1.7-.6 2.6 1.7 5.2 5.1 8.6 10.3 10.3.9.3 2 0 2.6-.6l1.3-1.8c.4-.5.3-1.2-.2-1.6l-2.7-2.2c-.5-.4-1.2-.3-1.6.1l-1.1 1.3c-2-1-3.7-2.7-4.7-4.7l1.3-1.1c.4-.4.5-1.1.1-1.6L8.8 4.7c-.4-.5-1.1-.6-1.6-.2Z"
/></svg><span>
官方电话
</span></a><button
class=
"floating-consultation"
type=
"button"
data-open-contact
aria-label=
"在线咨询,打开学情分析表单"
><svg
viewBox=
"0 0 24 24"
aria-hidden=
"true"
><path
d=
"M4 5.5h16v11H9l-5 3v-14Z"
/></svg><span>
在线咨询
</span><i
aria-hidden=
"true"
>
1
</i></button></body></html>
</script><
button
class=
"floating-phone floating-phone--desktop"
type=
"button"
data-phone-contact
aria-label=
"官方电话,查看官方联系方式"
><svg
viewBox=
"0 0 24 24"
aria-hidden=
"true"
><path
d=
"M7.2 4.5 5.4 5.8c-.8.6-.9 1.7-.6 2.6 1.7 5.2 5.1 8.6 10.3 10.3.9.3 2 0 2.6-.6l1.3-1.8c.4-.5.3-1.2-.2-1.6l-2.7-2.2c-.5-.4-1.2-.3-1.6.1l-1.1 1.3c-2-1-3.7-2.7-4.7-4.7l1.3-1.1c.4-.4.5-1.1.1-1.6L8.8 4.7c-.4-.5-1.1-.6-1.6-.2Z"
/></svg><span>
官方电话
</span></button><a
class=
"floating-phone floating-phone--mobile"
href=
{`tel:${site.contact.phone}`}
aria-label=
{`拨打官方电话
${
site
.
contact
.
phoneDisplay
}
`}
><svg
viewBox=
"0 0 24 24"
aria-hidden=
"true"
><path
d=
"M7.2 4.5 5.4 5.8c-.8.6-.9 1.7-.6 2.6 1.7 5.2 5.1 8.6 10.3 10.3.9.3 2 0 2.6-.6l1.3-1.8c.4-.5.3-1.2-.2-1.6l-2.7-2.2c-.5-.4-1.2-.3-1.6.1l-1.1 1.3c-2-1-3.7-2.7-4.7-4.7l1.3-1.1c.4-.4.5-1.1.1-1.6L8.8 4.7c-.4-.5-1.1-.6-1.6-.2Z"
/></svg><span>
官方电话
</span></a><button
class=
"floating-consultation"
type=
"button"
data-open-contact
aria-label=
"在线咨询,打开学情分析表单"
><svg
viewBox=
"0 0 24 24"
aria-hidden=
"true"
><path
d=
"M4 5.5h16v11H9l-5 3v-14Z"
/></svg><span>
在线咨询
</span><i
aria-hidden=
"true"
>
1
</i></button></body></html>
src/pages/admin/index.astro
View file @
6dc1c171
...
@@ -66,7 +66,10 @@
...
@@ -66,7 +66,10 @@
</main>
</main>
<main
id=
"list-view"
class=
"view hidden content-role"
>
<main
id=
"list-view"
class=
"view hidden content-role"
>
<div
class=
"view-toolbar"
><div
class=
"view-actions"
><button
id=
"manage-categories"
class=
"ghost"
>
管理分类
</button><button
id=
"import-article"
class=
"ghost"
>
导入文件
</button><button
id=
"new-article"
class=
"primary"
>
+ 新建文章
</button></div></div>
<div
class=
"view-toolbar article-toolbar"
>
<label
class=
"article-status-filter"
for=
"article-status-filter"
><span>
发布状态
</span><select
id=
"article-status-filter"
aria-label=
"按发布状态筛选文章"
><option
value=
"all"
>
全部状态
</option><option
value=
"live"
>
已上线
</option><option
value=
"new-draft"
>
新建未发布
</option><option
value=
"edited-draft"
>
修改未发布
</option></select></label>
<div
class=
"view-actions"
><button
id=
"manage-categories"
class=
"ghost"
>
管理分类
</button><button
id=
"import-article"
class=
"ghost"
>
导入文件
</button><button
id=
"new-article"
class=
"primary"
>
+ 新建文章
</button></div>
</div>
<input
id=
"article-file"
class=
"hidden"
type=
"file"
accept=
".md,.docx,text/markdown,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
/>
<input
id=
"article-file"
class=
"hidden"
type=
"file"
accept=
".md,.docx,text/markdown,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
/>
<p
id=
"import-status"
class=
"import-status"
aria-live=
"polite"
></p>
<p
id=
"import-status"
class=
"import-status"
aria-live=
"polite"
></p>
<div
id=
"article-list"
class=
"article-list"
></div>
<div
id=
"article-list"
class=
"article-list"
></div>
...
...
src/styles/global.css
View file @
6dc1c171
...
@@ -3,7 +3,7 @@
...
@@ -3,7 +3,7 @@
.eyebrow
{
display
:
flex
;
align-items
:
center
;
gap
:
10px
;
margin-bottom
:
16px
;
color
:
var
(
--brand-blue
);
font-size
:
11px
;
font-weight
:
700
;
letter-spacing
:
.16em
}
.eyebrow
>
span
{
width
:
28px
;
height
:
2px
;
background
:
var
(
--brand-accent
)}
.eyebrow--light
,
.section-heading--light
.eyebrow
{
color
:
#d8eefb
}
.section-heading
{
max-width
:
700px
}
.section-heading--center
{
margin-inline
:
auto
;
text-align
:
center
}
.section-heading--center
.eyebrow
{
justify-content
:
center
}
.section-heading
h2
{
margin-bottom
:
20px
;
font-size
:
clamp
(
34px
,
4vw
,
54px
);
font-weight
:
600
;
letter-spacing
:
-.025em
}
.section-heading
>
p
{
margin
:
0
;
color
:
var
(
--text-secondary
);
font-size
:
17px
}
.section-heading--light
h2
{
color
:
#fff
}
.section-heading--light
>
p
{
color
:
#c9deea
}
.section-intro-row
{
display
:
flex
;
align-items
:
end
;
justify-content
:
space-between
;
gap
:
50px
;
margin-bottom
:
48px
}
.text-link
{
color
:
var
(
--brand-blue
);
font-weight
:
600
;
white-space
:
nowrap
}
.text-link
span
{
color
:
var
(
--brand-accent
)}
.text-link--light
{
color
:
#fff
}
.eyebrow
{
display
:
flex
;
align-items
:
center
;
gap
:
10px
;
margin-bottom
:
16px
;
color
:
var
(
--brand-blue
);
font-size
:
11px
;
font-weight
:
700
;
letter-spacing
:
.16em
}
.eyebrow
>
span
{
width
:
28px
;
height
:
2px
;
background
:
var
(
--brand-accent
)}
.eyebrow--light
,
.section-heading--light
.eyebrow
{
color
:
#d8eefb
}
.section-heading
{
max-width
:
700px
}
.section-heading--center
{
margin-inline
:
auto
;
text-align
:
center
}
.section-heading--center
.eyebrow
{
justify-content
:
center
}
.section-heading
h2
{
margin-bottom
:
20px
;
font-size
:
clamp
(
34px
,
4vw
,
54px
);
font-weight
:
600
;
letter-spacing
:
-.025em
}
.section-heading
>
p
{
margin
:
0
;
color
:
var
(
--text-secondary
);
font-size
:
17px
}
.section-heading--light
h2
{
color
:
#fff
}
.section-heading--light
>
p
{
color
:
#c9deea
}
.section-intro-row
{
display
:
flex
;
align-items
:
end
;
justify-content
:
space-between
;
gap
:
50px
;
margin-bottom
:
48px
}
.text-link
{
color
:
var
(
--brand-blue
);
font-weight
:
600
;
white-space
:
nowrap
}
.text-link
span
{
color
:
var
(
--brand-accent
)}
.text-link--light
{
color
:
#fff
}
.button
{
display
:
inline-flex
;
align-items
:
center
;
justify-content
:
center
;
gap
:
12px
;
min-height
:
52px
;
padding
:
0
25px
;
border
:
1px
solid
var
(
--brand-blue
);
border-radius
:
9px
;
color
:
#fff
;
background
:
var
(
--brand-blue
);
font-size
:
14px
;
font-weight
:
600
;
letter-spacing
:
.02em
;
transition
:
background
.2s
,
border-color
.2s
,
box-shadow
.2s
,
color
.2s
,
transform
.2s
}
.button
:hover
{
border-color
:
var
(
--primary-hover
);
background
:
var
(
--primary-hover
);
transform
:
translateY
(
-2px
)}
.button--small
{
min-height
:
42px
;
padding
:
0
18px
;
font-size
:
13px
}
.button--accent
{
border-color
:
var
(
--brand-accent
);
color
:
var
(
--text-primary
);
background
:
var
(
--brand-accent
)}
.button--accent
:hover
{
border-color
:
var
(
--accent-hover
);
background
:
var
(
--accent-hover
)}
.button--outline
{
color
:
var
(
--brand-blue
);
background
:
#fff
}
.button--outline
:hover
{
color
:
#fff
}
.button--ghost
{
border-color
:
rgba
(
255
,
255
,
255
,
.55
);
background
:
transparent
}
.button--white
{
border-color
:
#fff
;
color
:
var
(
--brand-blue-dark
);
background
:
#fff
}
.button--white
:hover
{
border-color
:
#fff
;
color
:
#0b5eba
;
background
:
#edf6ff
;
box-shadow
:
0
9px
22px
rgba
(
6
,
52
,
105
,
.2
)}
.hero__actions
{
display
:
flex
;
flex-wrap
:
wrap
;
gap
:
12px
}
.button
{
display
:
inline-flex
;
align-items
:
center
;
justify-content
:
center
;
gap
:
12px
;
min-height
:
52px
;
padding
:
0
25px
;
border
:
1px
solid
var
(
--brand-blue
);
border-radius
:
9px
;
color
:
#fff
;
background
:
var
(
--brand-blue
);
font-size
:
14px
;
font-weight
:
600
;
letter-spacing
:
.02em
;
transition
:
background
.2s
,
border-color
.2s
,
box-shadow
.2s
,
color
.2s
,
transform
.2s
}
.button
:hover
{
border-color
:
var
(
--primary-hover
);
background
:
var
(
--primary-hover
);
transform
:
translateY
(
-2px
)}
.button--small
{
min-height
:
42px
;
padding
:
0
18px
;
font-size
:
13px
}
.button--accent
{
border-color
:
var
(
--brand-accent
);
color
:
var
(
--text-primary
);
background
:
var
(
--brand-accent
)}
.button--accent
:hover
{
border-color
:
var
(
--accent-hover
);
background
:
var
(
--accent-hover
)}
.button--outline
{
color
:
var
(
--brand-blue
);
background
:
#fff
}
.button--outline
:hover
{
color
:
#fff
}
.button--ghost
{
border-color
:
rgba
(
255
,
255
,
255
,
.55
);
background
:
transparent
}
.button--white
{
border-color
:
#fff
;
color
:
var
(
--brand-blue-dark
);
background
:
#fff
}
.button--white
:hover
{
border-color
:
#fff
;
color
:
#0b5eba
;
background
:
#edf6ff
;
box-shadow
:
0
9px
22px
rgba
(
6
,
52
,
105
,
.2
)}
.hero__actions
{
display
:
flex
;
flex-wrap
:
wrap
;
gap
:
12px
}
.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
)
7
2%
,
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
::after
{
position
:
absolute
;
inset
:
-1px
;
z-index
:
1
;
pointer-events
:
none
;
content
:
""
;
background
:
linear-gradient
(
90deg
,
#fff
0
,
rgba
(
255
,
255
,
255
,
.82
)
7%
,
transparent
23%
,
transparent
77%
,
rgba
(
255
,
255
,
255
,
.38
)
91%
,
#fff
100%
),
linear-gradient
(
180deg
,
#fff
0
,
rgba
(
255
,
255
,
255
,
.42
)
9%
,
transparent
24%
,
transparent
75%
,
rgba
(
255
,
255
,
255
,
.72
)
91%
,
#fff
100%
)}
.image-frame
img
{
width
:
100%
;
aspect-ratio
:
1.22
;
object-fit
:
cover
;
mix-blend-mode
:
multiply
;
-webkit-mask-image
:
radial-gradient
(
ellipse
94%
94%
at
56%
50%
,
#000
50%
,
rgba
(
0
,
0
,
0
,
.96
)
63%
,
rgba
(
0
,
0
,
0
,
.5
)
82%
,
transparent
100%
);
mask-image
:
radial-gradient
(
ellipse
94%
94%
at
56%
50%
,
#000
50%
,
rgba
(
0
,
0
,
0
,
.96
)
63%
,
rgba
(
0
,
0
,
0
,
.5
)
8
2%
,
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
}
.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
)}
.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
)}
...
@@ -132,10 +132,10 @@
...
@@ -132,10 +132,10 @@
.contact-hero
.breadcrumb
a
:hover
{
color
:
#fff
}
.contact-hero
.breadcrumb
a
:hover
{
color
:
#fff
}
/* 固定咨询入口:让在线咨询始终保持在页面可操作范围内。 */
/* 固定咨询入口:让在线咨询始终保持在页面可操作范围内。 */
.floating-consultation
{
position
:
fixed
;
right
:
24px
;
bottom
:
24px
;
z-index
:
90
;
display
:
inline-flex
;
width
:
1
98px
;
height
:
62px
;
align-items
:
center
;
justify-content
:
center
;
gap
:
11px
;
padding
:
0
22px
;
border
:
0
;
border-radius
:
32px
;
color
:
#fff
;
background
:
#1d2b49
;
box-shadow
:
0
12px
26px
rgba
(
20
,
37
,
68
,
.2
);
cursor
:
pointer
;
font-size
:
20px
;
font-weight
:
700
;
letter-spacing
:
.02em
;
transition
:
transform
.2s
,
box-shadow
.2s
,
background
.2s
}
.floating-consultation
svg
{
width
:
25px
;
height
:
25px
;
fill
:
none
;
stroke
:
currentColor
;
stroke-linecap
:
round
;
stroke-linejoin
:
round
;
stroke-width
:
1.8
}
.floating-consultation
:hover
{
background
:
#14213a
;
box-shadow
:
0
16px
30px
rgba
(
20
,
37
,
68
,
.27
);
transform
:
translateY
(
-2px
)}
.floating-consultation
:focus-visible
{
outline
:
3px
solid
rgba
(
20
,
115
,
230
,
.42
);
outline-offset
:
4px
}
.floating-consultation
i
{
position
:
absolute
;
top
:
-7px
;
right
:
-4px
;
display
:
grid
;
width
:
28px
;
height
:
28px
;
place-items
:
center
;
border-radius
:
50%
;
color
:
#fff
;
background
:
#ff4f5d
;
font-size
:
14
px
;
font-style
:
normal
;
font-weight
:
700
;
box-shadow
:
0
2px
6px
rgba
(
255
,
79
,
93
,
.22
)}
.floating-consultation
{
position
:
fixed
;
right
:
24px
;
bottom
:
24px
;
z-index
:
90
;
display
:
inline-flex
;
width
:
1
64px
;
height
:
52px
;
align-items
:
center
;
justify-content
:
center
;
gap
:
9px
;
padding
:
0
18px
;
border
:
0
;
border-radius
:
27px
;
color
:
#fff
;
background
:
#1d2b49
;
box-shadow
:
0
10px
22px
rgba
(
20
,
37
,
68
,
.18
);
cursor
:
pointer
;
font-size
:
16px
;
font-weight
:
700
;
letter-spacing
:
.02em
;
transition
:
transform
.2s
,
box-shadow
.2s
,
background
.2s
}
.floating-consultation
svg
{
width
:
21px
;
height
:
21px
;
fill
:
none
;
stroke
:
currentColor
;
stroke-linecap
:
round
;
stroke-linejoin
:
round
;
stroke-width
:
1.8
}
.floating-consultation
:hover
{
background
:
#14213a
;
box-shadow
:
0
14px
26px
rgba
(
20
,
37
,
68
,
.25
);
transform
:
translateY
(
-2px
)}
.floating-consultation
:focus-visible
{
outline
:
3px
solid
rgba
(
20
,
115
,
230
,
.42
);
outline-offset
:
4px
}
.floating-consultation
i
{
position
:
absolute
;
top
:
-6px
;
right
:
-3px
;
display
:
grid
;
width
:
24px
;
height
:
24px
;
place-items
:
center
;
border-radius
:
50%
;
color
:
#fff
;
background
:
#ff4f5d
;
font-size
:
12
px
;
font-style
:
normal
;
font-weight
:
700
;
box-shadow
:
0
2px
6px
rgba
(
255
,
79
,
93
,
.22
)}
.floating-phone
{
position
:
fixed
;
right
:
24px
;
bottom
:
98px
;
z-index
:
90
;
display
:
inline-flex
;
width
:
198px
;
height
:
62px
;
align-items
:
center
;
justify-content
:
center
;
gap
:
11px
;
padding
:
0
22px
;
border
:
0
;
border-radius
:
32px
;
color
:
#fff
;
background
:
#ff7900
;
box-shadow
:
0
12px
26px
rgba
(
204
,
92
,
0
,
.2
);
font-size
:
20px
;
font-weight
:
700
;
letter-spacing
:
.02em
;
text-decoration
:
none
;
transition
:
transform
.2s
,
box-shadow
.2s
,
background
.2s
}
.floating-phone
svg
{
width
:
25px
;
height
:
25px
;
fill
:
none
;
stroke
:
currentColor
;
stroke-linecap
:
round
;
stroke-linejoin
:
round
;
stroke-width
:
1.8
}
.floating-phone
:hover
{
background
:
#e86d00
;
box-shadow
:
0
16px
30px
rgba
(
204
,
92
,
0
,
.27
);
transform
:
translateY
(
-2px
)}
.floating-phone
:focus-visible
{
outline
:
3px
solid
rgba
(
255
,
121
,
0
,
.35
);
outline-offset
:
4px
}
.floating-phone
{
position
:
fixed
;
right
:
24px
;
bottom
:
88px
;
z-index
:
90
;
display
:
inline-flex
;
width
:
164px
;
height
:
52px
;
align-items
:
center
;
justify-content
:
center
;
gap
:
9px
;
padding
:
0
18px
;
border
:
0
;
border-radius
:
27px
;
color
:
#fff
;
background
:
#ff7900
;
box-shadow
:
0
10px
22px
rgba
(
204
,
92
,
0
,
.18
);
cursor
:
pointer
;
font-family
:
inherit
;
font-size
:
16px
;
font-weight
:
700
;
letter-spacing
:
.02em
;
text-decoration
:
none
;
transition
:
transform
.2s
,
box-shadow
.2s
,
background
.2s
}
.floating-phone--mobile
{
display
:
none
}
.floating-phone
svg
{
width
:
21px
;
height
:
21px
;
fill
:
none
;
stroke
:
currentColor
;
stroke-linecap
:
round
;
stroke-linejoin
:
round
;
stroke-width
:
1.8
}
.floating-phone
:hover
{
background
:
#e86d00
;
box-shadow
:
0
14px
26px
rgba
(
204
,
92
,
0
,
.25
);
transform
:
translateY
(
-2px
)}
.floating-phone
:focus-visible
{
outline
:
3px
solid
rgba
(
255
,
121
,
0
,
.35
);
outline-offset
:
4px
}
.contact-dialog--phone
{
width
:
min
(
440px
,
calc
(
100%
-
28px
))}
.contact-dialog--phone
.contact-dialog__panel
{
padding
:
32px
34px
28px
}
.phone-dialog__heading
>
span
{
display
:
block
;
margin-bottom
:
8px
;
color
:
#e86d00
;
font-size
:
10px
;
font-weight
:
800
;
letter-spacing
:
.16em
}
.phone-dialog__heading
h2
{
margin
:
0
0
8px
;
color
:
#1e3248
;
font-size
:
29px
}
.phone-dialog__heading
p
{
margin
:
0
;
color
:
#66798a
;
font-size
:
14px
;
line-height
:
1.7
}
.phone-dialog__number
{
display
:
block
;
margin
:
24px
0
18px
;
color
:
#1e3248
;
font-size
:
30px
;
font-weight
:
750
;
letter-spacing
:
.04em
}
.phone-dialog__details
{
display
:
grid
;
gap
:
5px
;
padding
:
14px
0
;
border-top
:
1px
solid
#e2eaf0
;
border-bottom
:
1px
solid
#e2eaf0
;
color
:
#718393
;
font-size
:
12px
;
line-height
:
1.6
}
.phone-dialog__details
a
{
color
:
var
(
--brand-blue
)}
.phone-dialog__call
{
display
:
flex
;
min-height
:
44px
;
margin-top
:
20px
;
align-items
:
center
;
justify-content
:
center
;
border-radius
:
9px
;
color
:
#fff
;
background
:
#ff7900
;
font-size
:
14px
;
font-weight
:
700
;
transition
:
background
.2s
,
transform
.2s
}
.phone-dialog__call
:hover
{
background
:
#e86d00
;
transform
:
translateY
(
-1px
)
}
.contact-dialog--phone
{
width
:
min
(
620px
,
calc
(
100%
-
28px
))}
.contact-dialog--phone
.contact-dialog__panel
{
padding
:
34px
36px
32px
}
.phone-dialog__heading
{
padding-right
:
42px
}
.phone-dialog__heading
h2
{
margin
:
0
0
8px
;
color
:
#1e3248
;
font-size
:
29px
}
.phone-dialog__heading
p
{
max-width
:
500px
;
margin
:
0
;
color
:
#66798a
;
font-size
:
14px
;
line-height
:
1.7
}
.official-contact-list
{
display
:
grid
;
grid-template-columns
:
1
fr
1
fr
;
margin-top
:
25px
;
overflow
:
hidden
;
border
:
1px
solid
#dbe6ef
;
border-radius
:
14px
;
background
:
#fff
}
.official-contact-item
{
min-width
:
0
;
padding
:
20px
21px
;
border-bottom
:
1px
solid
#e3ebf2
}
.official-contact-item
:nth-child
(
odd
)
{
border-right
:
1px
solid
#e3ebf2
}
.official-contact-item
:nth-last-child
(
-n
+
2
)
{
border-bottom
:
0
}
.official-contact-item
>
span
{
display
:
block
;
margin-bottom
:
7px
;
color
:
#748596
;
font-size
:
11px
;
font-weight
:
700
}
.official-contact-item
>
a
,
.official-contact-item
>
strong
{
display
:
block
;
overflow-wrap
:
anywhere
;
color
:
#1e3248
;
font-size
:
17px
;
font-weight
:
750
;
line-height
:
1.45
}
.official-contact-item
>
a
:hover
{
color
:
var
(
--brand-blue
)}
.official-contact-item--phone
>
a
{
color
:
var
(
--brand-blue
);
font-size
:
22px
;
letter-spacing
:
.025em
}
.official-contact-item
>
p
{
margin
:
6px
0
0
;
color
:
#7c8b99
;
font-size
:
11px
;
line-height
:
1.55
}
@media
(
max-width
:
760px
){
.floating-phone
{
right
:
16px
;
bottom
:
1
50px
;
width
:
148px
;
height
:
56px
;
gap
:
9px
;
padding
:
0
18px
;
font-size
:
18px
}
.floating-phone
svg
{
width
:
23px
;
height
:
23px
}
.floating-consultation
{
right
:
16px
;
bottom
:
84px
;
width
:
148px
;
height
:
56px
;
gap
:
9px
;
padding
:
0
18px
;
font-size
:
18px
}
.floating-consultation
svg
{
width
:
23px
;
height
:
23px
}
.floating-consultation
i
{
top
:
-6px
;
right
:
-3px
;
width
:
26px
;
height
:
26px
;
font-size
:
13px
}}
@media
(
max-width
:
760px
){
.floating-phone
{
right
:
16px
;
bottom
:
1
38px
;
width
:
132px
;
height
:
48px
;
gap
:
8px
;
padding
:
0
14px
;
font-size
:
15px
}
.floating-phone--desktop
{
display
:
none
}
.floating-phone--mobile
{
display
:
inline-flex
}
.floating-phone
svg
{
width
:
20px
;
height
:
20px
}
.floating-consultation
{
right
:
16px
;
bottom
:
80px
;
width
:
132px
;
height
:
48px
;
gap
:
8px
;
padding
:
0
14px
;
font-size
:
15px
}
.floating-consultation
svg
{
width
:
20px
;
height
:
20px
}
.floating-consultation
i
{
top
:
-5px
;
right
:
-2px
;
width
:
22px
;
height
:
22px
;
font-size
:
11px
}
.official-contact-list
{
grid-template-columns
:
1
fr
}
.official-contact-item
:nth-child
(
odd
)
{
border-right
:
0
}
.official-contact-item
:nth-last-child
(
2
)
{
border-bottom
:
1px
solid
#e3ebf2
}
.official-contact-item
:last-child
{
border-bottom
:
0
}}
@media
(
max-width
:
1080px
){
@media
(
max-width
:
1080px
){
.subject-stage-grid
article
{
min-height
:
365px
}
.subject-stage-grid
article
{
min-height
:
365px
}
...
...
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