moobile
Deploy to Production / deploy-production (push) Successful in 27s Details

This commit is contained in:
root 2026-02-14 03:30:37 +03:00
parent 0b5fb434db
commit b4b99491ae
13 changed files with 3939 additions and 3415 deletions

View File

@ -167,15 +167,14 @@ export default function ChatPage() {
}, [normalizeChat, refreshNavBadges]);
return (
<div className="ios26-dashboard" style={{ padding: '16px' }}>
<div className="ios26-dashboard ios26-chat-page" style={{ padding: '16px' }}>
<Box
className="ios26-chat-layout"
sx={{
display: 'grid',
gridTemplateColumns: '320px 1fr',
gap: 'var(--ios26-spacing)',
alignItems: 'stretch',
// учитываем padding контейнера (16px сверху и снизу),
// чтобы итоговая высота блока была ~90vh
height: 'calc(90vh - 32px)',
maxHeight: 'calc(90vh - 32px)',
overflow: 'hidden',

View File

@ -1,6 +1,19 @@
'use client';
import { useEffect, useState, useCallback, Suspense } from 'react';
const MOBILE_BREAKPOINT = 767;
function useIsMobile() {
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const mq = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT}px)`);
setIsMobile(mq.matches);
const listener = () => setIsMobile(mq.matches);
mq.addEventListener('change', listener);
return () => mq.removeEventListener('change', listener);
}, []);
return isMobile;
}
import { useRouter, usePathname } from 'next/navigation';
import { BottomNavigationBar } from '@/components/navigation/BottomNavigationBar';
import { TopNavigationBar } from '@/components/navigation/TopNavigationBar';
@ -22,6 +35,7 @@ export default function ProtectedLayout({
const router = useRouter();
const pathname = usePathname();
const { user, loading } = useAuth();
const isMobile = useIsMobile();
const [navBadges, setNavBadges] = useState<NavBadges | null>(null);
const [subscriptionChecked, setSubscriptionChecked] = useState(false);
@ -139,10 +153,24 @@ export default function ProtectedLayout({
return (
<NavBadgesProvider refreshNavBadges={refreshNavBadges}>
<SelectedChildProvider>
<div
className="protected-layout-root"
style={{
display: 'flex',
flexDirection: 'column',
minHeight: '100vh',
height: '100vh',
}}
>
{!isFullWidthPage && <TopNavigationBar user={user} />}
<main
className="protected-main"
data-no-nav={isLiveKit ? true : undefined}
data-full-width={isFullWidthPage ? true : undefined}
style={{
flex: 1,
minHeight: 0,
overflow: 'auto',
padding: isFullWidthPage ? '0' : '16px',
maxWidth: isFullWidthPage ? '100%' : '1200px',
margin: isFullWidthPage ? '0' : '0 auto',
@ -152,12 +180,18 @@ export default function ProtectedLayout({
</main>
{!isLiveKit && (
<Suspense fallback={null}>
<BottomNavigationBar userRole={user?.role} user={user} navBadges={navBadges} />
<BottomNavigationBar
userRole={user?.role}
user={user}
navBadges={navBadges}
notificationsSlot={isMobile && user ? <NotificationBell embedded /> : null}
/>
</Suspense>
)}
{!isLiveKit && user && (
{!isLiveKit && user && !isMobile && (
<NotificationBell />
)}
</div>
</SelectedChildProvider>
</NavBadgesProvider>
);

View File

@ -381,6 +381,7 @@ function ProfilePage() {
return (
<div
className="page-profile"
style={{
minHeight: '100vh',
padding: 24,
@ -389,6 +390,7 @@ function ProfilePage() {
}}
>
<div
className="page-profile-grid"
style={{
display: 'grid',
gridTemplateColumns: 'minmax(0, 440px) 1fr',
@ -618,13 +620,7 @@ function ProfilePage() {
{/* Поля — 2 колонки */}
<div style={{ padding: '0 24px 24px 24px' }}>
<div
style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '12px 16px',
}}
>
<div className="page-profile-fields" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px 16px' }}>
<div>
<label htmlFor="profile-first-name" style={{ display: 'block', fontSize: 12, fontWeight: 500, color: '#858585', marginBottom: 4 }}>Имя</label>
<input

View File

@ -405,10 +405,10 @@ export default function SchedulePage() {
};
return (
<div className="ios26-dashboard" style={{ padding: '16px' }}>
<div className="ios26-dashboard ios26-schedule-page" style={{ padding: '16px' }}>
{error && <ErrorDisplay error={error} onRetry={loadLessons} />}
<div style={{
<div className="ios26-schedule-layout" style={{
display: 'grid',
gridTemplateColumns: '5fr 2fr',
gap: 'var(--ios26-spacing)',
@ -417,6 +417,7 @@ export default function SchedulePage() {
// чтобы календарь не “схлопывался” на месяцах с меньшим количеством контента
minHeight: 'calc(100vh - 160px)',
}}>
<div className="ios26-schedule-calendar-wrap">
<Calendar
lessons={lessons}
lessonsLoading={lessonsLoading}
@ -425,7 +426,8 @@ export default function SchedulePage() {
onSelectEvent={handleSelectEvent}
onMonthChange={handleMonthChange}
/>
</div>
<div className="ios26-schedule-right-wrap">
<CheckLesson
selectedDate={selectedDate}
displayDate={displayDate}
@ -457,5 +459,6 @@ export default function SchedulePage() {
/>
</div>
</div>
</div>
);
}

View File

@ -413,6 +413,7 @@ export default function StudentsPage() {
return (
<div
className="page-students"
style={{
padding: '24px',
minHeight: '100vh',
@ -492,14 +493,7 @@ export default function StudentsPage() {
</p>
</md-elevated-card>
) : (
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(5, minmax(0, 1fr))',
gap: '16px',
alignItems: 'start',
}}
>
<div className="students-cards-grid">
{pendingInvitations.map((inv: any) => {
const st = inv.student || {};
const title = [st.first_name, st.last_name].filter(Boolean).join(' ') || st.email || 'Ученик';
@ -569,14 +563,7 @@ export default function StudentsPage() {
</p>
</md-elevated-card>
) : (
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(5, minmax(0, 1fr))',
gap: '16px',
alignItems: 'start',
}}
>
<div className="students-cards-grid">
{mentorshipRequests.map((req) => {
const st = req.student;
const title = [st.first_name, st.last_name].filter(Boolean).join(' ') || st.email || 'Ученик';
@ -734,14 +721,7 @@ export default function StudentsPage() {
<div>Загрузка студентов...</div>
</div>
) : (
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(5, minmax(0, 1fr))',
gap: '16px',
alignItems: 'start',
}}
>
<div className="students-cards-grid">
{(activeTab === 'students' ? filteredStudents : []).map((student: any) => {
const fullName = `${student?.user?.first_name || ''} ${student?.user?.last_name || ''}`.trim();
const title = fullName || student?.user?.email || 'Студент';
@ -946,6 +926,7 @@ export default function StudentsPage() {
{(selectedStudent || showAddPanel) && (
<div
className="students-side-panel"
style={{
position: 'fixed',
top: 0,

View File

@ -25,6 +25,8 @@ interface BottomNavigationBarProps {
userRole?: string;
user?: User | null;
navBadges?: NavBadges | null;
/** Слот для кнопки уведомлений (на мобильном — 4-й элемент в первом ряду). */
notificationsSlot?: React.ReactNode;
/** Выдвижная панель справа (3 колонки). При клике по пункту вызывается onClose. */
slideout?: boolean;
onClose?: () => void;
@ -57,7 +59,7 @@ function getBadgeCount(item: NavigationItem, navBadges: NavBadges | null | undef
}
}
export function BottomNavigationBar({ userRole, user, navBadges, slideout, onClose }: BottomNavigationBarProps) {
export function BottomNavigationBar({ userRole, user, navBadges, notificationsSlot, slideout, onClose }: BottomNavigationBarProps) {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
@ -113,8 +115,8 @@ export function BottomNavigationBar({ userRole, user, navBadges, slideout, onClo
return common;
}, [userRole]);
const firstRowItems = navigationItems.slice(0, 5);
const restItems = navigationItems.slice(5);
const firstRowItems = navigationItems.slice(0, notificationsSlot ? 3 : 5);
const restItems = navigationItems.slice(notificationsSlot ? 3 : 5);
const hasMore = restItems.length > 0;
// Подсветка активного таба по текущему URL
@ -270,22 +272,32 @@ export function BottomNavigationBar({ userRole, user, navBadges, slideout, onClo
<div
className={
'ios26-bottom-nav-first-row' +
(userRole === 'parent' ? ' ios26-bottom-nav-first-row--with-selector' : '')
(userRole === 'parent' ? ' ios26-bottom-nav-first-row--with-selector' : '') +
(notificationsSlot ? ' ios26-bottom-nav-first-row--with-notifications' : '')
}
>
{userRole === 'parent' && <ChildSelectorCompact />}
{userRole === 'parent' ? (
<div className="ios26-bottom-nav-first-row-buttons">
<div
className={
'ios26-bottom-nav-first-row-buttons' +
(notificationsSlot ? ' ios26-bottom-nav-first-row-buttons--with-notifications' : '')
}
>
{firstRowItems.map((item, i) => renderButton(item, i))}
{notificationsSlot}
</div>
) : (
firstRowItems.map((item, i) => renderButton(item, i))
<>
{firstRowItems.map((item, i) => renderButton(item, i))}
{notificationsSlot}
</>
)}
</div>
<div
className={'ios26-bottom-nav-rest' + (expanded ? ' ios26-bottom-nav-rest--expanded' : '')}
>
{restItems.map((item, i) => renderButton(item, 5 + i))}
{restItems.map((item, i) => renderButton(item, (notificationsSlot ? 3 : 5) + i))}
</div>
</div>
</div>

View File

@ -91,7 +91,7 @@ function NotificationItem({
const SCROLL_LOAD_MORE_THRESHOLD = 80;
export function NotificationBell() {
export function NotificationBell({ embedded }: { embedded?: boolean }) {
const refreshNavBadges = useNavBadgesRefresh();
const {
list,
@ -164,7 +164,16 @@ export function NotificationBell() {
<div
data-notification-bell
style={{
style={
embedded
? {
position: 'relative',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
}
: {
position: 'fixed',
right: BELL_POSITION.right,
bottom: BELL_POSITION.bottom,
@ -173,7 +182,8 @@ export function NotificationBell() {
alignItems: 'flex-end',
justifyContent: 'flex-end',
flexDirection: 'column',
}}
}
}
>
{/* Панель уведомлений — выезжает справа от колокольчика */}
{open && (
@ -182,8 +192,9 @@ export function NotificationBell() {
className="notification-panel-enter-active"
style={{
position: 'absolute',
right: 52,
bottom: 0,
...(embedded
? { bottom: '100%', marginBottom: 8, left: '50%', transform: 'translateX(-50%)' }
: { right: 52, bottom: 0 }),
width: PANEL_WIDTH,
maxHeight: PANEL_MAX_HEIGHT,
backgroundColor: 'var(--md-sys-color-surface)',
@ -295,7 +306,47 @@ export function NotificationBell() {
</div>
)}
{/* Кнопка-колокольчик */}
{/* Кнопка-колокольчик: в меню — как пункт навигации, иначе — круглая */}
{embedded ? (
<button
type="button"
className="ios26-bottom-nav-button"
aria-label={unreadCount > 0 ? `Уведомления: ${unreadCount} непрочитанных` : 'Уведомления'}
onClick={() => setOpen((o) => !o)}
>
<span style={{ position: 'relative', display: 'inline-flex' }}>
<span className="material-symbols-outlined ios26-bottom-nav-icon">
notifications
</span>
{unreadCount > 0 && (
<span
className="ios26-bottom-nav-badge"
style={{
position: 'absolute',
top: -8,
right: -16,
minWidth: 18,
height: 18,
borderRadius: 9,
background: 'var(--md-sys-color-error, #b3261e)',
color: '#fff',
fontSize: 11,
fontWeight: 600,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '0 4px',
boxSizing: 'border-box',
}}
title={`${unreadCount} непрочитанных`}
>
{unreadCount > 99 ? '99+' : unreadCount}
</span>
)}
</span>
<span className="ios26-bottom-nav-label">Уведомления</span>
</button>
) : (
<button
type="button"
aria-label={unreadCount > 0 ? `Уведомления: ${unreadCount} непрочитанных` : 'Уведомления'}
@ -345,6 +396,7 @@ export function NotificationBell() {
</span>
)}
</button>
)}
</div>
</>
);

View File

@ -188,6 +188,11 @@ export function ProfilePaymentTab() {
</ul>
<div className="ios26-plan-card__actions">
{free ? (
subscription ? (
<span className="ios26-plan-card__action" style={{ opacity: 0.8, cursor: 'default' }}>
Подписка уже активирована
</span>
) : (
<button
type="button"
className="ios26-plan-card__action"
@ -197,6 +202,7 @@ export function ProfilePaymentTab() {
>
{activatingPlanId === plan.id ? 'Активация...' : 'Активировать'}
</button>
)
) : (
<Link href="/payment" className="ios26-plan-card__action">
Подробнее и оплатить

View File

@ -1,17 +1,26 @@
'use client';
import { useState, useEffect } from 'react';
import { getReferralProfile, getReferralStats } from '@/api/referrals';
import { getReferralProfile, getReferralStats, getMyReferrals, type MyReferralItem } from '@/api/referrals';
import { LoadingSpinner } from '@/components/common/LoadingSpinner';
import { useToast } from '@/contexts/ToastContext';
const formatCurrency = (v: number) =>
new Intl.NumberFormat('ru-RU', { style: 'currency', currency: 'RUB', maximumFractionDigits: 0 }).format(v);
function formatDate(s: string) {
try {
return new Date(s).toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric' });
} catch {
return s;
}
}
export function ReferralsPageContent() {
const { showToast } = useToast();
const [profile, setProfile] = useState<any>(null);
const [stats, setStats] = useState<any>(null);
const [referralsList, setReferralsList] = useState<{ direct: MyReferralItem[]; indirect: MyReferralItem[] } | null>(null);
const [loading, setLoading] = useState(true);
const [copied, setCopied] = useState(false);
@ -19,6 +28,7 @@ export function ReferralsPageContent() {
Promise.all([
getReferralProfile().then(setProfile),
getReferralStats().then(setStats),
getMyReferrals().then(setReferralsList).catch(() => setReferralsList({ direct: [], indirect: [] })),
])
.finally(() => setLoading(false));
}, []);
@ -138,6 +148,58 @@ export function ReferralsPageContent() {
</div>
</div>
)}
{/* Список приглашённых рефералов */}
{referralsList && (referralsList.direct.length > 0 || referralsList.indirect.length > 0) && (
<div>
<div
style={{
fontSize: 11,
fontWeight: 600,
letterSpacing: '0.05em',
color: 'var(--md-sys-color-on-surface-variant)',
marginBottom: 8,
}}
>
ПРИГЛАШЁННЫЕ
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{referralsList.direct.length > 0 && (
<div>
<div style={{ fontSize: 12, color: 'var(--md-sys-color-on-surface-variant)', marginBottom: 6 }}>
Прямые рефералы ({referralsList.direct.length})
</div>
<ul style={{ margin: 0, paddingLeft: 20, listStyle: 'disc' }}>
{referralsList.direct.map((r: MyReferralItem, i: number) => (
<li key={i} style={{ marginBottom: 4, fontSize: 14, color: 'var(--md-sys-color-on-surface)' }}>
{r.email} {r.level}, {r.total_points} баллов, зарегистрирован {formatDate(r.created_at)}
</li>
))}
</ul>
</div>
)}
{referralsList.indirect.length > 0 && (
<div>
<div style={{ fontSize: 12, color: 'var(--md-sys-color-on-surface-variant)', marginBottom: 6 }}>
Рефералы ваших рефералов ({referralsList.indirect.length})
</div>
<ul style={{ margin: 0, paddingLeft: 20, listStyle: 'disc' }}>
{referralsList.indirect.map((r: MyReferralItem, i: number) => (
<li key={i} style={{ marginBottom: 4, fontSize: 14, color: 'var(--md-sys-color-on-surface)' }}>
{r.email} {r.level}, {r.total_points} баллов, зарегистрирован {formatDate(r.created_at)}
</li>
))}
</ul>
</div>
)}
</div>
</div>
)}
{referralsList && referralsList.direct.length === 0 && referralsList.indirect.length === 0 && (
<p style={{ fontSize: 14, color: 'var(--md-sys-color-on-surface-variant)' }}>
Пока никого нет. Поделитесь реферальной ссылкой когда кто-то зарегистрируется по ней, он появится здесь и вы получите уведомление.
</p>
)}
</div>
);
}

View File

@ -169,6 +169,10 @@ body:has([data-no-nav]) {
padding-bottom: 0;
}
body:has(.protected-layout-root) {
padding-bottom: 0;
}
body > * {
position: relative;
z-index: 1;
@ -270,10 +274,10 @@ img {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120' viewBox='0 0 120 120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)' opacity='0.6'/%3E%3C/svg%3E");
}
/* Кастомный нижний бар iOS 26 — первый ряд всегда, остальное по раскрытию */
/* Кастомный нижний бар iOS 26 — первый ряд всегда, остальное по раскрытию. Ноутбук и выше: fixed, bottom 20px */
.ios26-bottom-nav-container {
position: fixed;
bottom: 30px;
bottom: 20px;
left: 16px;
right: 16px;
z-index: 1000;
@ -294,6 +298,70 @@ img {
padding-bottom: env(safe-area-inset-bottom, 0);
}
/* Protected layout: контент скроллится сверху, снизу меню. На мобильном — меню в потоке; ноутбук+ — fixed */
.protected-layout-root {
display: flex;
flex-direction: column;
min-height: 100vh;
height: 100vh;
}
.protected-layout-root .protected-main {
flex: 1;
min-height: 0;
overflow: auto;
}
/* Ноутбук и выше (768px+): нижний бар fixed, bottom 20px, контенту отступ снизу */
@media (min-width: 768px) {
.protected-layout-root .ios26-bottom-nav-container {
position: fixed;
bottom: 20px;
left: 16px;
right: 16px;
margin: 0 auto;
max-width: min(900px, 100%);
}
.protected-layout-root .protected-main {
padding-bottom: 88px;
}
}
/* Мобильный: меню в потоке, на всю ширину, прижато к низу */
@media (max-width: 767px) {
.protected-layout-root .ios26-bottom-nav-container {
position: relative;
bottom: auto;
left: 0;
right: 0;
margin: 0;
max-width: 100%;
flex-shrink: 0;
border-radius: 0;
}
.protected-layout-root .ios26-bottom-nav {
border-radius: 0;
border-left: none;
border-right: none;
padding-bottom: calc(8px + env(safe-area-inset-bottom, 0));
}
/* Все строки навигации по 4 элемента на мобильном */
.protected-layout-root .ios26-bottom-nav-first-row {
grid-template-columns: repeat(4, 1fr);
}
.protected-layout-root .ios26-bottom-nav-first-row-buttons {
grid-template-columns: repeat(4, 1fr);
}
.protected-layout-root .ios26-bottom-nav-rest {
grid-template-columns: repeat(4, 1fr);
}
}
.ios26-bottom-nav-expand-trigger {
all: unset;
cursor: pointer;
@ -353,6 +421,15 @@ img {
grid-template-columns: unset;
}
/* Первый ряд: 4 колонки, когда есть слот уведомлений (мобильное меню) */
.ios26-bottom-nav-first-row--with-notifications {
grid-template-columns: repeat(4, 1fr);
}
.ios26-bottom-nav-first-row-buttons--with-notifications {
grid-template-columns: repeat(4, 1fr);
}
.ios26-bottom-nav-first-row-buttons {
flex: 1;
min-width: 0;
@ -1306,6 +1383,308 @@ img {
font-size: 15px !important;
}
/* ========== Адаптив: планшет и телефон (dashboard, chat, materials, homework, my-progress, request-mentor, profile, livekit, students, feedback, analytics, payment, referrals) ========== */
/* Планшет: 768px — 1024px */
@media (max-width: 1024px) {
.protected-main {
padding-left: 12px !important;
padding-right: 12px !important;
padding-top: 12px !important;
padding-bottom: 12px !important;
}
.protected-main[data-full-width] {
padding: 12px !important;
}
.ios26-dashboard {
padding: 12px;
}
.ios26-dashboard.ios26-dashboard-grid {
gap: 12px;
}
.ios26-stat-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.ios26-analytics-chart-row {
flex-direction: column;
}
.ios26-analytics-two-cols {
flex-direction: column;
}
.ios26-dashboard-analytics .ios26-stat-grid--aside {
width: 100%;
flex-direction: row;
flex-wrap: wrap;
gap: 8px;
}
.ios26-dashboard-analytics .ios26-stat-grid--aside .ios26-stat-tile {
flex: 1;
min-width: 140px;
}
.ios26-bottom-nav-container {
left: 8px;
right: 8px;
}
.ios26-panel {
padding: 12px;
}
}
/* Телефон: до 767px */
@media (max-width: 767px) {
body {
padding-bottom: calc(50px + 52px);
}
.protected-main {
padding-left: 10px !important;
padding-right: 10px !important;
padding-top: 10px !important;
padding-bottom: 10px !important;
}
.protected-main[data-full-width] {
padding: 10px !important;
}
.ios26-dashboard {
padding: 10px;
}
.ios26-dashboard.ios26-dashboard-grid {
grid-template-columns: 1fr;
gap: 10px;
}
.ios26-stat-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.ios26-stat-tile {
padding: 10px 12px;
}
.ios26-stat-label,
.ios26-stat-value {
font-size: 15px;
}
.ios26-section-title {
font-size: 18px;
margin-bottom: 12px;
}
.ios26-bottom-nav-container {
left: 6px;
right: 6px;
bottom: 20px;
}
.ios26-bottom-nav {
padding: 6px 8px;
}
.ios26-bottom-nav-first-row {
gap: 2px 4px;
}
.ios26-bottom-nav-rest--expanded {
max-height: 200px;
}
.ios26-panel {
padding: 10px;
border-radius: var(--ios26-radius-sm);
}
.ios26-dashboard-analytics .ios26-analytics-chart,
.ios26-dashboard-analytics .ios26-analytics-chart-placeholder {
min-height: 60vh;
}
.ios26-analytics-nav .ios26-analytics-nav-btn:first-child {
left: 8px;
}
.ios26-analytics-nav .ios26-analytics-nav-btn:last-child {
right: 8px;
}
.ios26-feedback-page {
padding: 10px;
}
.ios26-feedback-kanban {
gap: 12px;
}
.ios26-list-row {
font-size: 15px;
padding: 10px 0;
}
/* Страница Студенты: сетка карточек и боковая панель */
.page-students {
padding: 16px !important;
}
.students-cards-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
align-items: start;
}
.students-side-panel {
width: 100% !important;
max-width: 100vw;
right: 0 !important;
border-top-left-radius: 0 !important;
border-bottom-left-radius: 0 !important;
}
/* Страница Профиль: одна колонка, поля в 1 колонку */
.page-profile {
padding: 16px !important;
}
.page-profile-grid {
grid-template-columns: 1fr !important;
}
.page-profile-fields {
grid-template-columns: 1fr !important;
}
/* Аналитика: уже есть .ios26-analytics-chart-row column, .ios26-analytics-two-cols column */
}
/* Маленький телефон: до 480px */
@media (max-width: 480px) {
.protected-main {
padding-left: 8px !important;
padding-right: 8px !important;
padding-top: 8px !important;
padding-bottom: 8px !important;
}
.protected-main[data-full-width] {
padding: 8px !important;
}
.page-students {
padding: 12px !important;
}
.students-cards-grid {
grid-template-columns: 1fr;
gap: 10px;
}
.page-profile {
padding: 12px !important;
}
.ios26-dashboard {
padding: 8px;
}
.ios26-dashboard.ios26-dashboard-grid {
gap: 8px;
}
.ios26-stat-grid {
grid-template-columns: 1fr;
}
.ios26-stat-tile {
padding: 12px;
}
.ios26-bottom-nav-container {
left: 4px;
right: 4px;
bottom: 16px;
}
.ios26-bottom-nav-first-row {
grid-template-columns: repeat(5, 1fr);
}
.ios26-bottom-nav-button {
min-width: 0;
}
.ios26-bottom-nav-label {
font-size: 10px;
}
.ios26-panel {
padding: 8px;
}
.ios26-section-title {
font-size: 17px;
}
}
/* Страница Студенты: сетка карточек (десктоп) */
.students-cards-grid {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 16px;
align-items: start;
}
/* Страница календаря (расписание): планшет — правая часть сверху, календарь снизу; телефон — только правая часть */
@media (max-width: 1024px) {
.ios26-schedule-layout {
grid-template-columns: 1fr !important;
grid-template-rows: auto 1fr;
}
.ios26-schedule-calendar-wrap {
order: 2;
}
.ios26-schedule-right-wrap {
order: 1;
}
}
@media (max-width: 767px) {
.ios26-schedule-calendar-wrap {
display: none !important;
}
.ios26-schedule-layout {
grid-template-rows: 1fr !important;
min-height: auto !important;
}
}
/* Chat: список + окно чата — на планшете и телефоне одна колонка, список сверху */
@media (max-width: 900px) {
.ios26-chat-page {
padding: 10px !important;
}
.ios26-chat-layout {
grid-template-columns: 1fr !important;
grid-template-rows: auto 1fr;
height: calc(100vh - 120px) !important;
max-height: none !important;
}
.ios26-chat-layout > div:first-of-type {
max-height: 38vh;
min-height: 180px;
overflow: auto;
}
}
@media (max-width: 480px) {
.ios26-chat-page {
padding: 8px !important;
}
.ios26-chat-layout {
height: calc(100vh - 100px) !important;
}
.ios26-chat-layout > div:first-of-type {
max-height: 35vh;
min-height: 160px;
}
}
/* Materials / Homework / Students / Request-mentor / Profile: карточки и контент */
@media (max-width: 767px) {
.ios26-payment-tab,
.ios26-plan-card-grid {
gap: 10px;
}
.ios26-plan-card {
padding: 12px;
}
.protected-main md-elevated-card {
padding: 20px !important;
border-radius: 16px !important;
}
}
@media (max-width: 480px) {
.protected-main md-elevated-card {
padding: 14px !important;
border-radius: 14px !important;
}
}
/* LiveKit: полноэкранный контейнер на мобильных */
@media (max-width: 767px) {
[data-lk-theme] {
font-size: 14px;
}
.protected-main[data-no-nav] {
padding: 0 !important;
}
}
/* Flip-карточка эффект */
.flip-card {
position: relative;