Compare commits

...

8 Commits

Author SHA1 Message Date
Anas Rashid
9cd8067e52 Merge branch 'feature/design-system' into main: role tokens, shared controls, mobile layout, arrow and editor icons, centred wrapped verses 2026-10-09 15:57:04 +02:00
Anas Rashid
1e3fd3066e Merge branch 'feature/icons-palette' into main: header icons, colour roles, palettes, display settings, Eastern/Western numerals 2026-10-09 15:57:04 +02:00
Anas Rashid
ada721fd6d Design system: colour tokens named by role (surfaces, text, link, accent, active, focus, ok, danger), each palette written once with light-dark(); radius, type and shadow scales; one base for every button, chip and tab and one for every text box; a /design reference page (dev only). Phones: the search box no longer overlaps the header icons or pushes the page sideways (its own row), دیوان clears it, poet cards keep whole names. Arrows instead of پچھلا/اگلا, an exit icon for full-screen PDFs, an icon toolbar in the editor. Long half-lines wrap centred
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 15:56:56 +02:00
Anas Rashid
3305a514ba Numerals: Eastern (۱۲۳) or Western (123) in the display settings; the server sends pages with the chosen digits (scripts and styles untouched) and script-drawn numbers follow. Settings button lines up with the other icons
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 15:34:38 +02:00
Anas Rashid
e04ed0d4af Colour palettes and a display settings panel: sepia, فیروزی، نیلگوں، سبز، گلابی (each light and dark); font, palette and light/dark/automatic in one header button
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 15:31:17 +02:00
Anas Rashid
a37ba82829 Header: font list in Naskh at a fixed height (Nastaliq spilled out in Safari); the ع is laid over the book icon as text (Safari drew the SVG text too large)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 15:29:27 +02:00
Anas Rashid
70e44f705d Header: the font is a list (نسخ / نستعلیق, each in its own font); the word book icon has ع, not a Latin A; log out lines up with the other icons
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 15:27:30 +02:00
Anas Rashid
c4bf52dd97 Icons and colour roles: header icons with tooltips and a red log out; buttons coloured by what they do; محفوظ شدہ and پسندیدہ لکھاری
- Icon component (Lucide shapes, ISC), currentColor, decorative (the control keeps its words in aria-label/title).
- Colour roles: --action (primary, filled), --ok (approve, publish), --danger (log out, delete, reject); .btn variants
  and .icon-btn; light and dark.
- Header: admin, moderation, saved, word book, account/sign in, font, theme as icons; log out in red.
- Review: save/send filled, approve/publish green, return plain, reject red, with icons. Account: save, log out, password,
  delete. Reading pages: edit, history, arrange, tag. E-books: add, download, covers; the reader's controls as icons.
- نشانات is now محفوظ شدہ; favourite writers have their own page, پسندیدہ لکھاری (/writers).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 15:23:06 +02:00
19 changed files with 611 additions and 292 deletions

View File

@ -0,0 +1,55 @@
---
// Icons (Lucide, ISC licence; https://lucide.dev): drawn with currentColor, so they take the colour of their button
// or link. Decorative: the button or link carries the words (aria-label/title, or its text).
const { name, size = 20, class: cls } = Astro.props as { name: keyof typeof PATHS; size?: number; class?: string };
const PATHS = {
admin: '<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/>',
edit: '<path d="M12 20h9"/><path d="M16.376 3.622a1 1 0 0 1 3.002 3.002L7.368 18.635a2 2 0 0 1-.855.506l-2.872.838a.5.5 0 0 1-.62-.62l.838-2.872a2 2 0 0 1 .506-.854z"/>',
bookmark: '<path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"/>',
// the word book: a book; the letter ع is laid over it as text (.glyph-icon), which every browser draws the same
words: '<path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"/>',
user: '<circle cx="12" cy="8" r="5"/><path d="M20 21a8 8 0 0 0-16 0"/>',
login: '<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><path d="m10 17 5-5-5-5"/><path d="M15 12H3"/>',
logout: '<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><path d="m16 17 5-5-5-5"/><path d="M21 12H9"/>',
theme: '<circle cx="12" cy="12" r="10"/><path d="M12 18a6 6 0 0 0 0-12v12z"/>',
font: '<path d="M4 7V4h16v3"/><path d="M9 20h6"/><path d="M12 4v16"/>',
tag: '<path d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z"/><circle cx="7.5" cy="7.5" r="1" fill="currentColor"/>',
arrange: '<path d="M10 12h11"/><path d="M10 18h11"/><path d="M10 6h11"/><path d="M4 10h2"/><path d="M4 6h1v4"/><path d="M6 18H4c0-1 2-2 2-3s-1-1.5-2-1"/>',
history: '<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l4 2"/>',
upload: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m17 8-5-5-5 5"/><path d="M12 3v12"/>',
download: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/><path d="M12 15V3"/>',
check: '<path d="M20 6 9 17l-5-5"/>',
close: '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
undo: '<path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11"/>',
send: '<path d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"/><path d="m21.854 2.147-10.94 10.939"/>',
save: '<path d="M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z"/><path d="M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7"/><path d="M7 3v4a1 1 0 0 0 1 1h7"/>',
exitfull: '<path d="M8 3v3a2 2 0 0 1-2 2H3"/><path d="M21 8h-3a2 2 0 0 1-2-2V3"/><path d="M3 16h3a2 2 0 0 1 2 2v3"/><path d="M16 21v-3a2 2 0 0 1 2-2h3"/>',
full: '<path d="M8 3H5a2 2 0 0 0-2 2v3"/><path d="M21 8V5a2 2 0 0 0-2-2h-3"/><path d="M3 16v3a2 2 0 0 0 2 2h3"/><path d="M16 21h3a2 2 0 0 0 2-2v-3"/>',
next: '<path d="m15 18-6-6 6-6"/>', // points left: forward in a right-to-left page
prev: '<path d="m9 18 6-6-6-6"/>',
trash: '<path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/>',
key: '<path d="m15.5 7.5 2.3 2.3a1 1 0 0 0 1.4 0l2.1-2.1a1 1 0 0 0 0-1.4L19 4"/><path d="m21 2-9.6 9.6"/><circle cx="7.5" cy="15.5" r="5.5"/>',
image: '<rect width="18" height="18" x="3" y="3" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.1-3.1a2 2 0 0 0-2.828 0L6 21"/>',
plus: '<path d="M5 12h14"/><path d="M12 5v14"/>',
settings: '<path d="M21 4h-7"/><path d="M10 4H3"/><path d="M21 12h-9"/><path d="M8 12H3"/><path d="M21 20h-5"/><path d="M12 20H3"/><path d="M14 2v4"/><path d="M8 10v4"/><path d="M16 18v4"/>',
sun: '<circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/>',
moon: '<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>',
external: '<path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>',
// editor toolbar
couplet: '<path d="M3 9h7"/><path d="M14 9h7"/><path d="M3 15h7"/><path d="M14 15h7"/>',
para: '<path d="M13 4v16"/><path d="M17 4v16"/><path d="M19 4H9.5a4.5 4.5 0 0 0 0 9H13"/>',
h2: '<path d="M4 12h8"/><path d="M4 18V6"/><path d="M12 18V6"/><path d="m17 12 3-2v8"/>',
h3: '<path d="M4 12h8"/><path d="M4 18V6"/><path d="M12 18V6"/><path d="M21 18h-4c0-4 4-3 4-6 0-1.5-2-2.5-4-1"/>',
dict: '<path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/>',
note: '<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>',
variant: '<circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><path d="M13 6h3a2 2 0 0 1 2 2v7"/><path d="M11 18H8a2 2 0 0 1-2-2V9"/>',
redo: '<path d="m15 14 5-5-5-5"/><path d="M20 9H9.5A5.5 5.5 0 0 0 4 14.5 5.5 5.5 0 0 0 9.5 20H13"/>',
keyboard: '<rect x="2" y="6" width="20" height="12" rx="2"/><path d="M6 10h.01M10 10h.01M14 10h.01M18 10h.01M8 14h8"/>',
code: '<path d="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/>',
up: '<path d="m5 12 7-7 7 7"/><path d="M12 19V5"/>',
down: '<path d="M12 5v14"/><path d="m19 12-7 7-7-7"/>',
top: '<path d="M5 3h14"/><path d="m18 13-6-6-6 6"/><path d="M12 7v14"/>',
bottom: '<path d="M12 17V3"/><path d="m6 11 6 6 6-6"/><path d="M19 21H5"/>',
};
---
<svg class:list={['icon', cls]} viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" set:html={PATHS[name]} />

View File

@ -1,9 +1,10 @@
---
// tabs between the reader's bookmarks and their word book (kept separate)
const { current } = Astro.props as { current: 'bookmarks' | 'words' };
// tabs of the reader's own space: saved texts, favourite writers, the word book (kept separate), profile
const { current } = Astro.props as { current: 'bookmarks' | 'writers' | 'words' };
---
<nav class="admin-nav">
<a href="/library" aria-current={current === 'bookmarks' ? 'page' : undefined}><span class="bm-icon on" aria-hidden="true"></span> نشانات</a>
<a href="/library" aria-current={current === 'bookmarks' ? 'page' : undefined}><span class="bm-icon on" aria-hidden="true"></span> محفوظ شدہ</a>
<a href="/writers" aria-current={current === 'writers' ? 'page' : undefined}>پسندیدہ لکھاری</a>
<a href="/words" aria-current={current === 'words' ? 'page' : undefined}>ذخیرۂ الفاظ</a>
<a href="/account">پروفائل</a>
</nav>

View File

@ -3,4 +3,4 @@
const { s } = Astro.props as { s: [string, string, string?][] };
---
{s.map(([t, k, d]) => (k === 'mark' ? <mark>{t}</mark> : k === 'pen' ? <span class="takhallus">{t}</span>
: k === 'bmk' ? <span class="bmk" data-save={d} data-confirm="یہ نشان ہٹائیں؟" title="نشان زدہ عبارت (ہٹانے کے لیے دبائیں)">{t}</span> : t))}
: k === 'bmk' ? <span class="bmk" data-save={d} data-confirm="یہ عبارت محفوظ شدہ سے ہٹائیں؟" title="محفوظ شدہ عبارت (ہٹانے کے لیے دبائیں)">{t}</span> : t))}

View File

@ -8,6 +8,7 @@ import '../styles/global.css';
interface Props { title?: string; description?: string; q?: string }
const { title, description = 'اردو کی کلاسیکی شاعری اور نثر', q = '' } = Astro.props;
const fullTitle = title ? `${title} · دیوان` : 'دیوان · اردو کی کلاسیکی شاعری اور نثر';
import Icon from '../components/Icon.astro';
---
<!doctype html>
<html lang="ur-PK" dir="rtl">
@ -18,7 +19,7 @@ const fullTitle = title ? `${title} · دیوان` : 'دیوان · اردو ک
<meta name="description" content={description} />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<script is:inline>
// font + theme choice applied before paint (Naskh and system theme by default)
// font, palette and light/dark applied before paint (Naskh, sepia and the system's light/dark by default)
try {
// ?font=nastaliq|naskh in the URL sets the choice (shareable links)
const f = new URLSearchParams(location.search).get('font');
@ -26,6 +27,9 @@ const fullTitle = title ? `${title} · دیوان` : 'دیوان · اردو ک
if (localStorage.getItem('divan-font') === 'nastaliq') document.documentElement.classList.add('nastaliq');
const t = localStorage.getItem('divan-theme');
if (t) document.documentElement.dataset.theme = t;
const pal = localStorage.getItem('divan-palette'); // sepia when unset
if (pal) document.documentElement.dataset.palette = pal;
if (document.cookie.includes('divan-digits=latn')) document.documentElement.dataset.digits = 'latn'; // Western numerals
} catch (e) {}
</script>
</head>
@ -43,17 +47,43 @@ const fullTitle = title ? `${title} · دیوان` : 'دیوان · اردو ک
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="10.5" cy="10.5" r="6.5"/><path d="m15.5 15.5 5 5"/></svg>
</button>
</form>
<div class="toggles">
{Astro.locals.user?.role === 'admin' && <a class="account-link" href="/admin">ایڈمن</a>}
{['mod-l2', 'mod-l1', 'admin'].includes(Astro.locals.user?.role ?? '') && <a class="account-link" href="/mod">موڈریشن</a>}
{Astro.locals.user && <a class="account-link" href="/library">نشانات</a>}
{Astro.locals.user && <a class="account-link" href="/words">ذخیرۂ الفاظ</a>}
<nav class="toggles" aria-label="سائٹ">
{Astro.locals.user?.role === 'admin' && <a class="icon-btn" href="/admin" title="ایڈمن" aria-label="ایڈمن"><Icon name="admin" /></a>}
{['mod-l2', 'mod-l1', 'admin'].includes(Astro.locals.user?.role ?? '') && <a class="icon-btn" href="/mod" title="موڈریشن" aria-label="موڈریشن"><Icon name="edit" /></a>}
{Astro.locals.user && <a class="icon-btn" href="/library" title="محفوظ شدہ" aria-label="محفوظ شدہ"><Icon name="bookmark" /></a>}
{Astro.locals.user && <a class="icon-btn" href="/words" title="ذخیرۂ الفاظ" aria-label="ذخیرۂ الفاظ"><span class="glyph-icon"><Icon name="words" /><span aria-hidden="true">ع</span></span></a>}
{Astro.locals.user
? <a class="account-link" href="/account" title={Astro.locals.user.email}>{Astro.locals.user.full_name.split(' ')[0] || 'اکاؤنٹ'}</a>
: <a class="account-link" href={`/signin?next=${encodeURIComponent(Astro.url.pathname)}`}>لاگ ان</a>}
<button type="button" id="font-toggle" aria-label="خط بدلیں">نسخ</button>
<button type="button" id="theme-toggle" aria-label="روشن یا تاریک">◐</button>
? <a class="icon-btn" href="/account" title={`اکاؤنٹ: ${Astro.locals.user.full_name || Astro.locals.user.email}`} aria-label="اکاؤنٹ"><Icon name="user" /></a>
: <a class="icon-btn" href={`/signin?next=${encodeURIComponent(Astro.url.pathname)}`} title="لاگ ان" aria-label="لاگ ان"><Icon name="login" /></a>}
<details class="display-menu">
<summary class="icon-btn" title="خط اور رنگ" aria-label="خط اور رنگ"><Icon name="settings" /></summary>
<div class="display-panel">
<p>خط</p>
<div class="seg"><button type="button" data-font="naskh">نسخ</button><button type="button" data-font="nastaliq" class="f-nastaliq">نستعلیق</button></div>
<p>رنگ</p>
<div class="swatches">
{[['', 'سپیا', '#fbf6ea', '#bf3f2a'], ['teal', 'فیروزی', '#e2f0ee', '#0b4f6c'], ['lapis', 'نیلگوں', '#e6ebf8', '#1d3b8b'],
['green', 'سبز', '#e5eede', '#1f5d3a'], ['rose', 'گلابی', '#f5e6ea', '#6b2c5e']].map(([v, name, a, b]) => (
<button type="button" data-palette={v} title={name} aria-label={name} style={`--a: ${a}; --b: ${b}`}><span></span>{name}</button>
))}
</div>
<p>ہندسے</p>
<div class="seg"><button type="button" data-digits="">۱۲۳ مشرقی</button><button type="button" data-digits="latn">123 مغربی</button></div>
<p>روشنی</p>
<div class="seg">
<button type="button" data-theme-pick="light"><Icon name="sun" size={16} /> روشن</button>
<button type="button" data-theme-pick="dark"><Icon name="moon" size={16} /> تاریک</button>
<button type="button" data-theme-pick=""><Icon name="theme" size={16} /> خودکار</button>
</div>
</div>
</details>
{Astro.locals.user && (
<form method="post" action="/account" class="inline-form">
<input type="hidden" name="act" value="signout" />
<button class="icon-btn danger" title="لاگ آؤٹ" aria-label="لاگ آؤٹ"><Icon name="logout" /></button>
</form>
)}
</nav>
</div>
<div id="kb" class="wrap kb" hidden></div>
</header>
@ -87,14 +117,41 @@ const fullTitle = title ? `${title} · دیوان` : 'دیوان · اردو ک
</footer>
<script is:inline>
const root = document.documentElement;
const fontBtn = document.getElementById('font-toggle');
const label = () => (fontBtn.textContent = root.classList.contains('nastaliq') ? 'نستعلیق' : 'نسخ');
label();
fontBtn.onclick = () => {
root.classList.toggle('nastaliq');
try { localStorage.setItem('divan-font', root.classList.contains('nastaliq') ? 'nastaliq' : 'naskh'); } catch (e) {}
label();
// display settings: reading font, colour palette, light/dark/automatic; each choice is remembered
const panel = document.querySelector('.display-menu');
const store = (k, v) => { try { v ? localStorage.setItem(k, v) : localStorage.removeItem(k); } catch (e) {} };
const marks = () => {
const on = { font: root.classList.contains('nastaliq') ? 'nastaliq' : 'naskh', palette: root.dataset.palette ?? '', theme: root.dataset.theme ?? '' };
panel.querySelectorAll('[data-font]').forEach((b) => b.setAttribute('aria-pressed', String(b.dataset.font === on.font)));
panel.querySelectorAll('[data-palette]').forEach((b) => b.setAttribute('aria-pressed', String(b.dataset.palette === on.palette)));
panel.querySelectorAll('[data-theme-pick]').forEach((b) => b.setAttribute('aria-pressed', String(b.dataset.themePick === on.theme)));
panel.querySelectorAll('[data-digits]').forEach((b) => b.setAttribute('aria-pressed', String(b.dataset.digits === (root.dataset.digits ?? ''))));
};
panel.addEventListener('click', (e) => {
const b = e.target.closest('button'); if (!b) return;
if (b.dataset.font) { root.classList.toggle('nastaliq', b.dataset.font === 'nastaliq'); store('divan-font', b.dataset.font); }
if (b.dataset.palette != null) { if (b.dataset.palette) root.dataset.palette = b.dataset.palette; else delete root.dataset.palette; store('divan-palette', b.dataset.palette); }
if (b.dataset.digits != null) {
// numerals: a cookie, so the server sends pages with them; switching back to Eastern reloads the page
document.cookie = `divan-digits=${b.dataset.digits}; path=/; max-age=${b.dataset.digits ? 31536000 : 0}; samesite=lax`;
if (!b.dataset.digits) { delete root.dataset.digits; location.reload(); return; }
root.dataset.digits = 'latn'; westernDigits(document.body);
}
if (b.dataset.themePick != null) { if (b.dataset.themePick) root.dataset.theme = b.dataset.themePick; else delete root.dataset.theme; store('divan-theme', b.dataset.themePick); }
marks();
});
document.addEventListener('click', (e) => { if (panel.open && !panel.contains(e.target)) panel.open = false; }); // close when clicking elsewhere
// Western numerals also in what scripts add later (word sidebar, page numbers, suggestions)
function westernDigits(node) {
const walk = document.createTreeWalker(node, NodeFilter.SHOW_TEXT);
for (let t = walk.nextNode(); t; t = walk.nextNode()) if (/[۰-۹]/.test(t.data)) t.data = t.data.replace(/[۰-۹]/g, (d) => String(d.charCodeAt(0) - 0x6f0));
}
new MutationObserver((list) => {
if (root.dataset.digits !== 'latn') return;
for (const m of list) for (const n of m.addedNodes) n.nodeType === 3 ? westernDigits(n.parentNode) : n.nodeType === 1 && westernDigits(n);
}).observe(document.body, { childList: true, subtree: true });
marks();
// Urdu on-screen keyboard, Gboard/Windows Urdu layout
const BASE = [[...'طصھدٹپتبجح'], [...'مورنلہاکی'], [...'قفےسشغع']];
const SHIFT = [['ظ','ض','ذ','ڈ','ث','ّ','ۃ','ـ','چ','خ'], ['ژ','ز','ڑ','ں','ۂ','ء','آ','گ',''], ['','','ۓ','','ؤ','ئ','']];
@ -396,11 +453,6 @@ const fullTitle = title ? `${title} · دیوان` : 'دیوان · اردو ک
if (!menu.hidden) hideMenu(); else if (!dict.hidden) closeDict();
});
document.getElementById('theme-toggle').onclick = () => {
const dark = root.dataset.theme ? root.dataset.theme === 'dark' : matchMedia('(prefers-color-scheme: dark)').matches;
root.dataset.theme = dark ? 'light' : 'dark';
try { localStorage.setItem('divan-theme', root.dataset.theme); } catch (e) {}
};
</script>
</body>
</html>

View File

@ -57,7 +57,7 @@ export async function pdfReader(root: HTMLElement) {
const full = root.querySelector<HTMLButtonElement>('.pdf-full')!;
const toggle = () => (document.fullscreenElement ? document.exitFullscreen() : root.requestFullscreen()).catch(() => {});
full.onclick = toggle;
document.addEventListener('fullscreenchange', () => { full.textContent = document.fullscreenElement === root ? 'واپس' : 'پوری اسکرین'; show(page); });
document.addEventListener('fullscreenchange', () => { const on = document.fullscreenElement === root; full.title = on ? 'پوری اسکرین سے واپس (F)' : 'پوری اسکرین (F)'; full.setAttribute('aria-label', on ? 'پوری اسکرین سے واپس' : 'پوری اسکرین'); show(page); });
// a click on the half the book turns towards goes forward
pagesBox.onclick = (e) => { const left = e.clientX < pagesBox.getBoundingClientRect().left + pagesBox.clientWidth / 2; step(left === rtl ? 1 : -1); };
document.addEventListener('keydown', (e) => {

View File

@ -1,4 +1,6 @@
// The signed-in reader (or null) for every page, from the session cookie
// The signed-in reader (or null) for every page, from the session cookie; and the reader's numerals: pages are
// written with Eastern Arabic (Urdu) digits, and a reader who chose Western digits (cookie divan-digits=latn, set in
// the display settings) gets them in the page as sent, so nothing flashes
import { defineMiddleware } from 'astro:middleware';
import { auth, COOKIE } from './lib/auth';
@ -10,5 +12,11 @@ export const onRequest = defineMiddleware(async (ctx, next) => {
if (r.ok) ctx.locals.user = r.data.user;
else if (r.status === 401) ctx.cookies.delete(COOKIE, { path: '/' }); // expired or signed out elsewhere
}
return next();
const res = await next();
if (ctx.cookies.get('divan-digits')?.value !== 'latn' || !res.headers.get('content-type')?.includes('text/html')) return res;
// text only: scripts and styles are left alone (the page's own scripts look for Eastern digits)
const html = (await res.text()).split(/(<script[\s\S]*?<\/script>|<style[\s\S]*?<\/style>)/)
.map((part, i) => (i % 2 ? part : part.replace(/[۰-۹]/g, (d) => String(d.charCodeAt(0) - 0x6f0)))).join('');
const headers = new Headers(res.headers); headers.delete('content-length');
return new Response(html, { status: res.status, statusText: res.statusText, headers });
});

View File

@ -82,6 +82,7 @@ const ebookCount = page.type === 'poet' ? ((await api<any[]>(`/api/ebooks?poet=$
const tagTarget = page.type === 'poem' ? `work/${poemId}` : `category/${page.category?.id}`;
const bookmark = (c: number) => JSON.stringify({ kind: 'couplet', poemId, couplet: c });
const title = page.type === 'poem' ? page.poem.title : page.type === 'poet' ? page.poet.nickname : page.category.title;
import Icon from '../components/Icon.astro';
---
<Base title={page.type === 'poem' ? `${page.poem.title} · ${page.poet.nickname}` : title} description={page.type === 'poet' ? page.poet.description?.slice(0, 160) : undefined}>
{page.type !== 'poet' && (
@ -95,8 +96,8 @@ const title = page.type === 'poem' ? page.poem.title : page.type === 'poet' ? pa
{page.type === 'poet' && (
<>
<h1>{page.poet.name}</h1>
<div class="center"><button type="button" class="save" data-save={JSON.stringify({ kind: 'poet', poetId })} aria-pressed={String(!!saved.poet)} title="یہ شاعر محفوظ کریں">{label(saved.poet)}</button>{arrange && <> <form method="post" action={`/mod/order/${page.category.id}`} class="inline-form"><button class="mod-edit-btn">ترتیب دیں</button></form>
<a class="mod-history" href={`/mod/order/${page.category.id}`}>ترتیب کا تاریخچہ</a></>}{tagCan && <> <form method="post" action={`/mod/tags/${tagTarget}`} class="inline-form"><button class="mod-edit-btn">ٹیگ</button></form></>}</div>
<div class="center"><button type="button" class="save" data-save={JSON.stringify({ kind: 'poet', poetId })} aria-pressed={String(!!saved.poet)} title="یہ شاعر محفوظ کریں">{label(saved.poet)}</button>{arrange && <> <form method="post" action={`/mod/order/${page.category.id}`} class="inline-form"><button class="mod-edit-btn"><Icon name="arrange" size={17} /> ترتیب دیں</button></form>
<a class="icon-btn small" href={`/mod/order/${page.category.id}`} title="ترتیب کا تاریخچہ" aria-label="ترتیب کا تاریخچہ"><Icon name="history" size={17} /></a></>}{tagCan && <> <form method="post" action={`/mod/tags/${tagTarget}`} class="inline-form"><button class="mod-edit-btn"><Icon name="tag" size={17} /> ٹیگ</button></form></>}</div>
<p class="muted">{years(page.poet)}</p>
<Tags tags={pageTags} />
{page.poet.description && <div class="intro">{page.poet.description}</div>}
@ -106,8 +107,8 @@ const title = page.type === 'poem' ? page.poem.title : page.type === 'poet' ? pa
<>
<h1>{page.category.title}</h1>
<Tags tags={pageTags} />
<div class="center"><button type="button" class="save" data-save={JSON.stringify({ kind: 'category', categoryId: page.category.id })} aria-pressed={String(!!saved.category)} title="یہ کتاب یا باب محفوظ کریں">{label(saved.category)}</button>{arrange && <> <form method="post" action={`/mod/order/${page.category.id}`} class="inline-form"><button class="mod-edit-btn">ترتیب دیں</button></form>
<a class="mod-history" href={`/mod/order/${page.category.id}`}>ترتیب کا تاریخچہ</a></>}{tagCan && <> <form method="post" action={`/mod/tags/${tagTarget}`} class="inline-form"><button class="mod-edit-btn">ٹیگ</button></form></>}</div>
<div class="center"><button type="button" class="save" data-save={JSON.stringify({ kind: 'category', categoryId: page.category.id })} aria-pressed={String(!!saved.category)} title="یہ کتاب یا باب محفوظ کریں">{label(saved.category)}</button>{arrange && <> <form method="post" action={`/mod/order/${page.category.id}`} class="inline-form"><button class="mod-edit-btn"><Icon name="arrange" size={17} /> ترتیب دیں</button></form>
<a class="icon-btn small" href={`/mod/order/${page.category.id}`} title="ترتیب کا تاریخچہ" aria-label="ترتیب کا تاریخچہ"><Icon name="history" size={17} /></a></>}{tagCan && <> <form method="post" action={`/mod/tags/${tagTarget}`} class="inline-form"><button class="mod-edit-btn"><Icon name="tag" size={17} /> ٹیگ</button></form></>}</div>
</>
)}
@ -135,8 +136,8 @@ const title = page.type === 'poem' ? page.poem.title : page.type === 'poet' ? pa
</p>
)}
<div class="center"><button type="button" class="save" data-save={JSON.stringify({ kind: 'poem', poemId })} aria-pressed={String(!!saved.poem)} title="یہ کلام محفوظ کریں">{label(saved.poem)}</button>
{modCan.edit && <> <form method="post" action={`/mod/work/${poemId}`} class="inline-form"><button class="mod-edit-btn">ترمیم کریں</button></form>
<a class="mod-history" href={`/mod/work/${poemId}`}>تاریخچہ</a></>}{tagCan && <> <form method="post" action={`/mod/tags/${tagTarget}`} class="inline-form"><button class="mod-edit-btn">ٹیگ</button></form></>}</div>
{modCan.edit && <> <form method="post" action={`/mod/work/${poemId}`} class="inline-form"><button class="mod-edit-btn"><Icon name="edit" size={17} /> ترمیم کریں</button></form>
<a class="icon-btn small" href={`/mod/work/${poemId}`} title="تاریخچہ" aria-label="تاریخچہ"><Icon name="history" size={17} /></a></>}{tagCan && <> <form method="post" action={`/mod/tags/${tagTarget}`} class="inline-form"><button class="mod-edit-btn"><Icon name="tag" size={17} /> ٹیگ</button></form></>}</div>
<Tags tags={pageTags} />
{page.poem.radif != null && <p class="muted radif">{page.poem.radif ? `ردیف: ${page.poem.radif}` : 'غیر مردف'}</p>}
<article class="poem" data-cite={`${page.poet.nickname}، ${page.poem.title}`} data-poem={poemId}>
@ -150,8 +151,8 @@ const title = page.type === 'poem' ? page.poem.title : page.type === 'poet' ? pa
</article>
<p class="muted hint">کسی لفظ کو منتخب کریں تو اس کے معنی اور تلفظ ساتھ نظر آئیں گے۔</p>
<nav class="poem-nav">
{page.prev && <a href={page.prev} rel="prev">‹ پچھلا</a>}
{page.next && <a href={page.next} rel="next">اگلا ›</a>}
{page.prev && <a class="icon-btn" href={page.prev} rel="prev" title="پچھلا" aria-label="پچھلا"><Icon name="prev" /></a>}
{page.next && <a class="icon-btn" href={page.next} rel="next" title="اگلا" aria-label="اگلا"><Icon name="next" /></a>}
</nav>
{page.divan && (
<p class="source divan-edit">

View File

@ -18,11 +18,12 @@ const cover = (b: any) => (b.has_cover ? `/ebooks/cover/${b.id}` : b.kind === 'a
const token = Astro.locals.user ? Astro.cookies.get(COOKIE)?.value : undefined;
const mayAdd = token && ['mod-l2', 'mod-l1', 'admin'].includes(Astro.locals.user!.role)
&& (await asUser(token, `/api/mod/can?category=${page.category.id}`)).data.ebooks;
import Icon from '../../components/Icon.astro';
---
<Base title={`ای بکس · ${page.poet.nickname}`}>
<nav class="crumbs"><a href={page.poet.url}>{page.poet.nickname}</a></nav>
<h1>ای بکس</h1>
{mayAdd && <p class="center"><a class="mod-edit-btn" href={`/mod/ebook/new?poet=${page.poet.id}`}>ای بک شامل کریں</a></p>}
{mayAdd && <p class="center"><a class="btn primary" href={`/mod/ebook/new?poet=${page.poet.id}`}><Icon name="plus" size={18} /> ای بک شامل کریں</a></p>}
<nav class="admin-nav ebook-tabs" aria-label="کتابوں کی قسم">
{tabs.map(([k, ur, en]) => {
const n = books.filter((b) => b.kind === k).length;

View File

@ -36,6 +36,7 @@ if (Astro.request.method === 'POST') {
}
}
const since = new Date(user.created_at);
import Icon from '../components/Icon.astro';
---
<Base title="میرا اکاؤنٹ">
<h1>{user.full_name || 'میرا اکاؤنٹ'}</h1>
@ -49,12 +50,12 @@ const since = new Date(user.created_at);
<input type="hidden" name="act" value="profile" />
<label>پورا نام<input name="full_name" value={user.full_name} maxlength="100" dir="auto" autocomplete="name" /></label>
<label>تعارف<textarea name="bio" maxlength="1000" rows="4" dir="auto">{user.bio}</textarea></label>
<button type="submit">محفوظ کریں</button>
<button type="submit" class="btn primary"><Icon name="save" size={18} /> محفوظ کریں</button>
</form>
<form method="post" class="account-form">
<input type="hidden" name="act" value="signout" />
<button type="submit">لاگ آؤٹ</button>
<button type="submit" class="btn danger"><Icon name="logout" size={18} /> لاگ آؤٹ</button>
</form>
<form method="post" class="account-form">
@ -63,7 +64,7 @@ const since = new Date(user.created_at);
<label>موجودہ پاس ورڈ<input type="password" name="current" required autocomplete="current-password" dir="ltr" /></label>
<label>نیا پاس ورڈ<input type="password" name="next" required minlength="8" autocomplete="new-password" dir="ltr" /></label>
<label>نیا پاس ورڈ دوبارہ<input type="password" name="next2" required minlength="8" autocomplete="new-password" dir="ltr" /></label>
<button type="submit">پاس ورڈ بدلیں</button>
<button type="submit" class="btn"><Icon name="key" size={18} /> پاس ورڈ بدلیں</button>
</form>
<form method="post" class="account-form danger">
@ -71,6 +72,6 @@ const since = new Date(user.created_at);
<p class="muted">اکاؤنٹ اور اس کا تمام ڈیٹا مستقل طور پر حذف ہو جائے گا۔</p>
<input type="hidden" name="act" value="delete" />
<label>پاس ورڈ<input type="password" name="password" required autocomplete="current-password" dir="ltr" /></label>
<button type="submit">اکاؤنٹ مستقل طور پر ختم کریں</button>
<button type="submit" class="btn danger"><Icon name="trash" size={18} /> اکاؤنٹ مستقل طور پر ختم کریں</button>
</form>
</Base>

View File

@ -1,6 +1,7 @@
---
// Admin: audit log of admin actions
import Base from '../../layouts/Base.astro';
import Icon from '../../components/Icon.astro';
import AdminNav from '../../components/AdminNav.astro';
import { asUser, COOKIE } from '../../lib/auth';
import { ud } from '../../lib/urdu';
@ -38,9 +39,9 @@ const when = (d: string) => ud(new Date(d).toISOString().slice(0, 16).replace('T
</table>
{pages > 1 && (
<nav class="pager">
{page > 1 && <a href={`/admin/audit?page=${page - 1}`}>‹ پچھلا</a>}
{page > 1 && <a class="icon-btn" href={`/admin/audit?page=${page - 1}`} rel="prev" title="پچھلا" aria-label="پچھلا"><Icon name="prev" /></a>}
<span class="muted">صفحہ {ud(page)} از {ud(pages)}</span>
{page < pages && <a href={`/admin/audit?page=${page + 1}`}>اگلا ›</a>}
{page < pages && <a class="icon-btn" href={`/admin/audit?page=${page + 1}`} rel="next" title="اگلا" aria-label="اگلا"><Icon name="next" /></a>}
</nav>
)}
</Base>

View File

@ -1,6 +1,7 @@
---
// Admin: users. Password resets happen here on a reader's request (no email server yet).
import Base from '../../layouts/Base.astro';
import Icon from '../../components/Icon.astro';
import AdminNav from '../../components/AdminNav.astro';
import { asUser, COOKIE } from '../../lib/auth';
import { ud } from '../../lib/urdu';
@ -92,9 +93,9 @@ const link = (n: number) => `/admin?q=${encodeURIComponent(q)}&page=${n}`;
</table>
{pages > 1 && (
<nav class="pager">
{page > 1 && <a href={link(page - 1)}>‹ پچھلا</a>}
{page > 1 && <a class="icon-btn" href={link(page - 1)} rel="prev" title="پچھلا" aria-label="پچھلا"><Icon name="prev" /></a>}
<span class="muted">صفحہ {ud(page)} از {ud(pages)}</span>
{page < pages && <a href={link(page + 1)}>اگلا ›</a>}
{page < pages && <a class="icon-btn" href={link(page + 1)} rel="next" title="اگلا" aria-label="اگلا"><Icon name="next" /></a>}
</nav>
)}
</Base>

105
web/src/pages/design.astro Normal file
View File

@ -0,0 +1,105 @@
---
// Design system reference (dev only): the real tokens and components from global.css, so it cannot drift.
// Switch palette, light/dark and font from the header's display settings.
import Base from '../layouts/Base.astro';
import Icon from '../components/Icon.astro';
if (import.meta.env.PROD) return new Response(null, { status: 404 });
const colours = [
['surfaces', ['surface-page', 'surface-raised', 'surface-poem', 'surface-sunken']],
['text and lines', ['text', 'text-muted', 'link', 'heading', 'border', 'line']],
['brand and accent', ['brand', 'accent', 'accent-soft', 'active', 'focus']],
['roles', ['action', 'action-ink', 'ok', 'danger']],
] as const;
const sizes = ['xs', 'sm', 'md', 'lg', 'xl', '2xl'];
const radii = ['sm', 'md', 'lg', 'pill'];
---
<Base title="ڈیزائن">
<h1>ڈیزائن سسٹم</h1>
<p class="muted">رنگ، خط اور روشنی اوپر کی ترتیبات سے بدلیں</p>
<section class="ds">
<h2>رنگ</h2>
{colours.map(([group, names]) => (
<div class="ds-row">
<span class="ds-label" lang="en">{group}</span>
{names.map((n) => <span class="ds-swatch" style={`--c: var(--${n})`}><i></i><code lang="en">--{n}</code></span>)}
</div>
))}
<h2>خط</h2>
{sizes.map((s) => <p class="ds-type" style={`font-size: var(--fs-${s})`}><code lang="en">--fs-{s}</code> دل ہی تو ہے نہ سنگ و خشت درد سے بھر نہ آئے کیوں</p>)}
<h2>گولائی</h2>
<div class="ds-row">{radii.map((r) => <span class="ds-radius" style={`border-radius: var(--r-${r})`}><code lang="en">--r-{r}</code></span>)}</div>
<h2>بٹن</h2>
<div class="ds-row">
<button class="btn">عام</button>
<button class="btn primary">محفوظ کریں</button>
<button class="btn ok"><Icon name="check" size={16} /> منظور</button>
<button class="btn danger"><Icon name="trash" size={16} /> حذف</button>
<button class="btn" disabled>بند</button>
<a class="icon-btn" href="#" title="ترتیبات" aria-label="ترتیبات"><Icon name="settings" /></a>
<a class="icon-btn small" href="#" title="ترمیم" aria-label="ترمیم"><Icon name="edit" size={16} /></a>
<a class="icon-btn danger" href="#" title="لاگ آؤٹ" aria-label="لاگ آؤٹ"><Icon name="logout" /></a>
</div>
<div class="ds-row">
<button class="save">محفوظ کریں</button>
<button class="save" aria-pressed="true">محفوظ شدہ</button>
<button class="wordbook">ذخیرۂ الفاظ</button>
<button class="wordbook" aria-pressed="true">ذخیرۂ الفاظ</button>
<button class="tool">عام</button>
<button class="tool" aria-pressed="true">چنا ہوا</button>
</div>
<h2>ایڈیٹر</h2>
<div class="toolbar" role="toolbar" aria-label="ایڈیٹر"><button type="button" class="tool" title="نیا شعر" aria-label="نیا شعر"><Icon name="couplet" size={18} /></button><button type="button" class="tool" title="پیراگراف" aria-label="پیراگراف"><Icon name="para" size={18} /></button><button type="button" class="tool" title="باب کا عنوان" aria-label="باب کا عنوان"><Icon name="h2" size={18} /></button><button type="button" class="tool" title="ذیلی عنوان" aria-label="ذیلی عنوان"><Icon name="h3" size={18} /></button><span class="sep"></span><button type="button" class="tool" title="لغت کا ربط" aria-label="لغت کا ربط"><Icon name="dict" size={18} /></button><button type="button" class="tool" title="حاشیہ" aria-label="حاشیہ"><Icon name="note" size={18} /></button><button type="button" class="tool" title="نسخہ" aria-label="نسخہ"><Icon name="variant" size={18} /></button><span class="sep"></span><button type="button" class="tool" title="واپس" aria-label="واپس"><Icon name="undo" size={18} /></button><button type="button" class="tool" title="دوبارہ" aria-label="دوبارہ"><Icon name="redo" size={18} /></button><button type="button" class="tool" title="اردو کی بورڈ" aria-label="اردو کی بورڈ"><Icon name="keyboard" size={18} /></button><span class="sep"></span><button type="button" class="tool" title="کوڈ" aria-label="کوڈ"><Icon name="code" size={18} /></button><span class="sep"></span><button type="button" class="tool" title="اوپر" aria-label="اوپر"><Icon name="up" size={18} /></button><button type="button" class="tool" title="نیچے" aria-label="نیچے"><Icon name="down" size={18} /></button><button type="button" class="tool" title="ہٹائیں" aria-label="ہٹائیں"><Icon name="close" size={18} /></button><span class="sep"></span><button type="button" class="tool">مطلع</button><button type="button" class="tool" aria-pressed="true">مقطع</button></div>
<h2>چپس اور نشان</h2>
<p class="tag-chips"><a class="tag-chip" href="#">عشق <small>۱۲</small></a><a class="tag-chip" href="#">فراق <small>۴</small></a></p>
<div class="author-chips">
<label class="chip"><input type="checkbox" checked /> غالب <small>۲۳۴</small></label>
<label class="chip"><input type="checkbox" /> میر <small>۱۲۰</small></label>
</div>
<p class="center">
<span class="status s-submitted">جمع شدہ</span><span class="status s-approved">منظور شدہ</span>
<span class="status s-published">شائع شدہ</span><span class="status s-returned">واپس</span>
</p>
<h2>خانے</h2>
<form class="account-form" onsubmit="return false">
<label>نام <input value="مرزا غالب" /></label>
<label>تعارف <textarea rows="2">دل ہی تو ہے</textarea></label>
<p class="form-error">غلط پاس ورڈ</p>
<p class="form-done">محفوظ ہو گیا</p>
</form>
<h2>کارڈ</h2>
<div class="cards"><a class="card" href="#">دیوانِ غالب <small>۲۳۴</small></a><a class="card" href="#">خطوط</a></div>
<div class="poets"><a class="poet-card" href="#"><span class="name">مرزا غالب</span><small>۱۷۹۷ – ۱۸۶۹</small></a></div>
<div class="century"><h2>بارہویں صدی ہجری</h2></div>
<h2>شعر</h2>
<div class="poem">
<div class="couplet"><p>دل ہی تو ہے نہ سنگ و خشت درد سے بھر نہ آئے کیوں</p><p>روئیں گے ہم ہزار بار کوئی ہمیں ستائے کیوں</p></div>
<div class="couplet" data-mark="مطلع"><p>دیر نہیں حرم نہیں در نہیں آستاں نہیں</p><p>بیٹھے ہیں رہ گزر پہ ہم <span class="takhallus">غالب</span> ہمیں اٹھائے کیوں</p></div>
</div>
<div class="poem-nav"><a class="icon-btn" href="#" title="پچھلا" aria-label="پچھلا"><Icon name="prev" /></a><a class="icon-btn" href="#" title="اگلا" aria-label="اگلا"><Icon name="next" /></a></div>
<h2>تلاش</h2>
<div class="result"><a href="#">غزل ۱۲</a><span class="snippet"><p>دل ہی تو ہے نہ <mark>سنگ</mark> و خشت</p></span></div>
<div class="pager"><span class="pages"><a href="#">۱</a><span class="current">۲</span><a href="#">۳</a></span></div>
</section>
</Base>
<style is:global>
.ds h2 { margin-top: 32px; }
.ds-row { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin: 10px 0; }
.ds-label { width: 9em; color: var(--text-muted); font: var(--fs-sm) system-ui, sans-serif; }
.ds-swatch { display: inline-flex; flex-direction: column; align-items: center; gap: 4px; }
.ds-swatch i { width: 64px; height: 40px; border-radius: var(--r-sm); background: var(--c); box-shadow: 0 0 0 1px var(--border); }
.ds code, .ds-type code { font: var(--fs-xs) ui-monospace, monospace; color: var(--text-muted); direction: ltr; }
.ds-type { margin: 4px 0; display: flex; gap: 16px; align-items: baseline; }
.ds-type code { width: 6em; flex: none; }
.ds-radius { width: 90px; height: 56px; display: grid; place-items: center; border: 1.5px solid var(--border); background: var(--surface-raised); }
</style>

View File

@ -18,6 +18,7 @@ const mayCover = token && poetPage && b.kind !== 'archive' && ['mod-l2', 'mod-l1
// the reader's bookmarked pages of this book
const marks = token && b.published && b.kind === 'pdf' ? (await asUser(token, `/api/library/state?ebook=${b.id}`)).data.pages ?? [] : [];
const mb = !b.size ? '' : b.size < 1048576 ? `${ud(Math.max(1, Math.round(b.size / 1024)))} KB` : `${ud((b.size / 1048576).toFixed(1))} MB`;
import Icon from '../../components/Icon.astro';
---
<Base title={`${b.title} · ${b.poet}`}>
<nav class="crumbs"><a href={b.poet_url}>{b.poet}</a> » <a href={`${b.poet_url}/ebooks?kind=${b.kind}`}>ای بکس</a></nav>
@ -25,7 +26,7 @@ const mb = !b.size ? '' : b.size < 1048576 ? `${ud(Math.max(1, Math.round(b.size
{!b.published && <p class="form-error center">یہ کتاب ابھی جائزے میں ہے؛ صرف موڈریٹر دیکھ سکتے ہیں۔</p>}
<p class="center muted">
{KIND[b.kind]}{mb && <> · {mb}</>}{b.source && <> · ماخذ: {b.source}</>}{b.licence && <> · {b.licence}</>}
{b.file && <> · <a href={file} download>ڈاؤن لوڈ</a></>}
{b.file && <> · <a class="btn" href={file} download><Icon name="download" size={17} /> ڈاؤن لوڈ</a></>}
{b.kind === 'archive' && <> · <a href={`https://archive.org/details/${b.archive_id}`}>archive.org پر</a></>}
</p>
<p class="center eb-people">
@ -38,8 +39,8 @@ const mb = !b.size ? '' : b.size < 1048576 ? `${ud(Math.max(1, Math.round(b.size
{mayCover && (
<p class="center cover-tools" data-id={b.id} data-kind={b.kind} data-src={file}>
{b.cover && <img class="cover-small" src={`/ebooks/cover/${b.id}`} alt="سرورق" />}
{(b.kind === 'pdf' || b.kind === 'epub') && <button type="button" class="mod-edit-btn" data-make>{b.kind === 'pdf' ? 'پہلے صفحے سے سرورق بنائیں' : 'کتاب کا سرورق لیں'}</button>}
<label class="mod-edit-btn">اپنی تصویر سرورق بنائیں<input type="file" accept="image/jpeg,image/png,image/webp" hidden data-own /></label>
{(b.kind === 'pdf' || b.kind === 'epub') && <button type="button" class="mod-edit-btn" data-make><Icon name="image" size={17} /> {b.kind === 'pdf' ? 'پہلے صفحے سے سرورق بنائیں' : 'کتاب کا سرورق لیں'}</button>}
<label class="mod-edit-btn"><Icon name="upload" size={17} /> اپنی تصویر سرورق بنائیں<input type="file" accept="image/jpeg,image/png,image/webp" hidden data-own /></label>
<span class="muted" data-cover-status></span>
</p>
)}
@ -47,15 +48,15 @@ const mb = !b.size ? '' : b.size < 1048576 ? `${ud(Math.max(1, Math.round(b.size
{b.kind === 'pdf' && (
<div class="pdf-reader" data-src={file} data-ebook={b.id} data-marks={JSON.stringify(marks)} dir={b.direction === 'ltr' ? 'ltr' : 'rtl'}>
<nav class="pdf-bar">
<button type="button" class="mod-edit-btn pdf-prev">‹ پچھلا</button>
<button type="button" class="icon-btn pdf-prev" title="پچھلا صفحہ" aria-label="پچھلا صفحہ"><Icon name={b.direction === 'ltr' ? 'next' : 'prev'} /></button>
<span>صفحہ <input class="pdf-page" type="number" min="1" value="1" aria-label="صفحہ نمبر" /> از <span class="pdf-total">…</span></span>
<button type="button" class="mod-edit-btn pdf-next">اگلا ›</button>
<button type="button" class="mod-edit-btn pdf-full" title="پوری اسکرین (F)">پوری اسکرین</button>
<button type="button" class="icon-btn pdf-next" title="اگلا صفحہ" aria-label="اگلا صفحہ"><Icon name={b.direction === 'ltr' ? 'prev' : 'next'} /></button>
<button type="button" class="icon-btn pdf-full" title="پوری اسکرین (F)" aria-label="پوری اسکرین"><Icon name="full" class="i-enter" /><Icon name="exitfull" class="i-exit" /></button>
{b.published && <button type="button" class="save pdf-mark" aria-pressed="false">صفحہ محفوظ کریں</button>}
</nav>
<p class="tag-chips pdf-marks" hidden></p>
<div class="pdf-pages" title="بائیں طرف کلک: اگلا صفحہ"><p class="muted">کتاب کھل رہی ہے…</p></div>
<p class="muted center small-line" dir="rtl">{b.direction === 'ltr' ? '→ اگلا صفحہ · ← پچھلا صفحہ' : '← اگلا صفحہ · → پچھلا صفحہ'} · <a href={file} target="_blank" rel="noopener">نئی ٹیب میں کھولیں</a></p>
<p class="muted center small-line" dir="rtl">{b.direction === 'ltr' ? '→ اگلا صفحہ · ← پچھلا صفحہ' : '← اگلا صفحہ · → پچھلا صفحہ'} · <a href={file} target="_blank" rel="noopener"><Icon name="external" size={15} /> نئی ٹیب میں کھولیں</a></p>
</div>
)}
{b.kind === 'archive' && <iframe class="ebook-frame" src={`https://archive.org/embed/${b.archive_id}`} title={b.title} allowfullscreen></iframe>}
@ -63,7 +64,7 @@ const mb = !b.size ? '' : b.size < 1048576 ? `${ud(Math.max(1, Math.round(b.size
{b.kind === 'epub' && (
<>
<div id="epub-view" class="ebook-frame" data-src={file}></div>
<nav class="poem-nav"><button type="button" class="mod-edit-btn" id="epub-prev">‹ پچھلا صفحہ</button><button type="button" class="mod-edit-btn" id="epub-next">اگلا صفحہ ›</button></nav>
<nav class="poem-nav"><button type="button" class="icon-btn" id="epub-prev" title="پچھلا صفحہ" aria-label="پچھلا صفحہ"><Icon name="prev" /></button><button type="button" class="icon-btn" id="epub-next" title="اگلا صفحہ" aria-label="اگلا صفحہ"><Icon name="next" /></button></nav>
</>
)}
<script>

View File

@ -1,5 +1,5 @@
---
// نشانات: the reader's bookmarks (poets, books/chapters, works, couplets, phrases), each with its full path in
// محفوظ شدہ: the reader's saved books/chapters, works, couplets, phrases and e-book pages, each with its full path in
// the site (poet » book » chapter » work) and a link to the couplet. The word book is separate: /words.
import Base from '../layouts/Base.astro';
import { asUser, COOKIE } from '../lib/auth';
@ -17,7 +17,7 @@ if (Astro.request.method === 'POST') {
return Astro.redirect(`/library#i${id}`, 303);
}
const lib = (await asUser(token, '/api/library')).data;
const total = lib.poets.length + lib.categories.length + lib.poems.length + lib.couplets.length + lib.phrases.length + (lib.pages?.length ?? 0);
const total = lib.categories.length + lib.poems.length + lib.couplets.length + lib.phrases.length + (lib.pages?.length ?? 0);
// a bookmarked phrase shown inside its couplet: [before, phrase, after] per line
const marked = (lines: string[], phrase: string) => phraseParts(lines, phrase).map((part, i) => {
if (!part) return [lines[i], '', ''];
@ -26,28 +26,19 @@ const marked = (lines: string[], phrase: string) => phraseParts(lines, phrase).m
});
const at = (item: any) => (item.poem ? `${item.poem.url}${item.poem.couplet != null ? `#c${item.poem.couplet}` : ''}` : '#');
---
<Base title="نشانات">
<h1>{me.full_name || 'نشانات'}</h1>
<Base title="محفوظ شدہ">
<h1>{me.full_name || 'محفوظ شدہ'}</h1>
{me.bio && <p class="profile-bio">{me.bio}</p>}
<LibraryNav current="bookmarks" />
<p class="muted center">{ud(total)} نشانات</p>
<p class="muted center">{ud(total)} محفوظ شدہ</p>
{total === 0 && (
<p class="muted center">ابھی کچھ محفوظ نہیں۔ کسی شاعر، کتاب، باب یا کلام پر "محفوظ کریں" دبائیں، کسی شعر کے ساتھ والا نشان دبائیں، یا کوئی عبارت منتخب کر کے "محفوظ کریں" دبائیں۔</p>
)}
{lib.poets.length > 0 && (
<section class="lib-section">
<h2>نشان زد شعرا</h2>
<div class="poets">
{lib.poets.map((p: any) => p.poet && <a class="poet-card" id={`i${p.id}`} href={p.poet.url}>{p.poet.name}</a>)}
</div>
</section>
<p class="muted center">ابھی کچھ محفوظ نہیں۔ کسی کتاب، باب یا کلام پر "محفوظ کریں" دبائیں، کسی شعر کے ساتھ والا نشان دبائیں، یا کوئی عبارت منتخب کر کے "محفوظ کریں" دبائیں۔</p>
)}
{lib.categories.length > 0 && (
<section class="lib-section">
<h2>نشان زد کتابیں اور ابواب</h2>
<h2>محفوظ کتابیں اور ابواب</h2>
<ul class="lib-list">
{lib.categories.map((c: any) => (
<li id={`i${c.id}`}>
@ -64,7 +55,7 @@ const at = (item: any) => (item.poem ? `${item.poem.url}${item.poem.couplet != n
{lib.poems.length > 0 && (
<section class="lib-section">
<h2>نشان زد کلام</h2>
<h2>محفوظ کلام</h2>
<ul class="lib-list">
{lib.poems.map((p: any) => (
<li id={`i${p.id}`}>
@ -81,7 +72,7 @@ const at = (item: any) => (item.poem ? `${item.poem.url}${item.poem.couplet != n
{lib.couplets.length > 0 && (
<section class="lib-section">
<h2>نشان زد اشعار</h2>
<h2>محفوظ اشعار</h2>
<ul class="lib-list">
{lib.couplets.map((c: any) => (
<li id={`i${c.id}`}>
@ -103,7 +94,7 @@ const at = (item: any) => (item.poem ? `${item.poem.url}${item.poem.couplet != n
{lib.phrases.length > 0 && (
<section class="lib-section">
<h2>نشان زد عبارتیں</h2>
<h2>محفوظ عبارتیں</h2>
<ul class="lib-list">
{lib.phrases.map((p: any) => (
<li id={`i${p.id}`}>

View File

@ -42,7 +42,10 @@ const ebook = rev.entity === 'ebook';
const EB: [string, string][] = [['title', 'عنوان'], ['writer', 'مصنف'], ['coauthors', 'شریک مصنفین'], ['language', 'زبان'], ['direction', 'سمت'],
['tags', 'ٹیگ'], ['source', 'ماخذ'], ['licence', 'اجازت'], ['note', 'تفصیل']];
const ebMeta = ebook ? Object.fromEntries(EB.map(([k, label]) => [k, (rev.content.split('\n').find((l: string) => l.startsWith(label + ':')) ?? '').slice(label.length + 1).trim()])) : {};
const acts = ([['approve', 'منظور کریں'], ['return', 'واپس بھیجیں'], ['reject', 'مسترد کریں'], ['publish', 'شائع کریں']] as const).filter(([a]) => may[a]);
// each action in its colour and with its icon: approve and publish confirm, return goes back, reject is destructive
const acts = ([['approve', 'منظور کریں', 'ok', 'check'], ['return', 'واپس بھیجیں', '', 'undo'], ['reject', 'مسترد کریں', 'danger', 'close'],
['publish', 'شائع کریں', 'ok', 'upload']] as const).filter(([a]) => may[a]);
import Icon from '../../../components/Icon.astro';
---
<Base title={`مسودہ: ${rev.work_title}`}>
<h1>{order && 'ترتیب: '}{tagging && 'ٹیگ: '}{ebook && 'ای بک: '}{rev.work_title}</h1>
@ -62,8 +65,8 @@ const acts = ([['approve', 'منظور کریں'], ['return', 'واپس بھیج
<EbookFields v={ebMeta} />
<textarea name="content" id="eb-content" hidden>{rev.content}</textarea>
<div class="filter-actions">
<button name="act" value="save">محفوظ کریں</button>
<button name="act" value="submit">{me.role === 'admin' ? 'محفوظ کر کے شائع کریں' : me.role === 'mod-l1' ? 'محفوظ کر کے اشاعت کے لیے بھیجیں' : 'محفوظ کر کے جائزے کے لیے بھیجیں'}</button>
<button name="act" value="save" class="btn primary"><Icon name="save" size={18} /> محفوظ کریں</button>
<button name="act" value="submit" class="btn primary"><Icon name={me.role === 'admin' ? 'upload' : 'send'} size={18} /> {me.role === 'admin' ? 'محفوظ کر کے شائع کریں' : me.role === 'mod-l1' ? 'محفوظ کر کے اشاعت کے لیے بھیجیں' : 'محفوظ کر کے جائزے کے لیے بھیجیں'}</button>
</div>
</form>
) : may.save && tagging ? (
@ -77,7 +80,7 @@ const acts = ([['approve', 'منظور کریں'], ['return', 'واپس بھیج
<span class="in-box-field tag-name-box"><input class="tag-name" id={`tag-name-${i}`} list="tag-names" value={t.name} maxlength="60" data-urdu aria-label="ٹیگ کا نام" placeholder="ٹیگ کا نام" />
<button type="button" class="in-box rom" data-urdu-rom={`tag-name-${i}`} aria-pressed="false" title="رومن حروف سے اردو">اب</button>
<button type="button" class="in-box" data-urdu-kb={`tag-name-${i}`} aria-pressed="false" title="اردو کی بورڈ" aria-label="اردو کی بورڈ"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><rect x="2" y="6" width="20" height="12" rx="2"/><path d="M6 10h.01M10 10h.01M14 10h.01M18 10h.01M7 14h10" stroke-linecap="round" stroke-width="2"/></svg></button></span>
<button type="button" class="tool" data-remove title="ہٹائیں">✕</button>
<button type="button" class="tool" data-remove title="ہٹائیں" aria-label="ہٹائیں"><Icon name="close" size={18} /></button>
</div>)}
</div>
<template id="tag-row">{[{ couplet: 0, type: 'موضوع', name: '' }].map((t, i) => <div class="tag-row">
@ -86,14 +89,14 @@ const acts = ([['approve', 'منظور کریں'], ['return', 'واپس بھیج
<span class="in-box-field tag-name-box"><input class="tag-name" id={`tag-name-${i}`} list="tag-names" value={t.name} maxlength="60" data-urdu aria-label="ٹیگ کا نام" placeholder="ٹیگ کا نام" />
<button type="button" class="in-box rom" data-urdu-rom={`tag-name-${i}`} aria-pressed="false" title="رومن حروف سے اردو">اب</button>
<button type="button" class="in-box" data-urdu-kb={`tag-name-${i}`} aria-pressed="false" title="اردو کی بورڈ" aria-label="اردو کی بورڈ"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><rect x="2" y="6" width="20" height="12" rx="2"/><path d="M6 10h.01M10 10h.01M14 10h.01M18 10h.01M7 14h10" stroke-linecap="round" stroke-width="2"/></svg></button></span>
<button type="button" class="tool" data-remove title="ہٹائیں">✕</button>
<button type="button" class="tool" data-remove title="ہٹائیں" aria-label="ہٹائیں"><Icon name="close" size={18} /></button>
</div>)}</template>
<datalist id="tag-names">{knownTags.map((n) => <option value={n} />)}</datalist>
<p><button type="button" class="tool" id="tag-add">+ ٹیگ</button></p>
<textarea name="content" id="tag-content" hidden>{rev.content}</textarea>
<div class="filter-actions">
<button name="act" value="save">محفوظ کریں</button>
<button name="act" value="submit">{me.role === 'admin' ? 'محفوظ کر کے شائع کریں' : me.role === 'mod-l1' ? 'محفوظ کر کے اشاعت کے لیے بھیجیں' : 'محفوظ کر کے جائزے کے لیے بھیجیں'}</button>
<button name="act" value="save" class="btn primary"><Icon name="save" size={18} /> محفوظ کریں</button>
<button name="act" value="submit" class="btn primary"><Icon name={me.role === 'admin' ? 'upload' : 'send'} size={18} /> {me.role === 'admin' ? 'محفوظ کر کے شائع کریں' : me.role === 'mod-l1' ? 'محفوظ کر کے اشاعت کے لیے بھیجیں' : 'محفوظ کر کے جائزے کے لیے بھیجیں'}</button>
</div>
</form>
) : may.save && order ? (
@ -105,18 +108,18 @@ const acts = ([['approve', 'منظور کریں'], ['return', 'واپس بھیج
<li draggable="true" data-line={it.line}>
<span class="order-title">{it.title}</span>
<span class="order-tools">
<button type="button" class="tool" data-move="top" title="سب سے اوپر">اول</button>
<button type="button" class="tool" data-move="up" title="اوپر">↑</button>
<button type="button" class="tool" data-move="down" title="نیچے">↓</button>
<button type="button" class="tool" data-move="bottom" title="سب سے نیچے">آخر</button>
<button type="button" class="tool" data-move="top" title="سب سے اوپر" aria-label="سب سے اوپر"><Icon name="top" size={18} /></button>
<button type="button" class="tool" data-move="up" title="اوپر" aria-label="اوپر"><Icon name="up" size={18} /></button>
<button type="button" class="tool" data-move="down" title="نیچے" aria-label="نیچے"><Icon name="down" size={18} /></button>
<button type="button" class="tool" data-move="bottom" title="سب سے نیچے" aria-label="سب سے نیچے"><Icon name="bottom" size={18} /></button>
</span>
</li>
))}
</ol>
<textarea name="content" id="order-content" hidden>{rev.content}</textarea>
<div class="filter-actions">
<button name="act" value="save">محفوظ کریں</button>
<button name="act" value="submit">{me.role === 'admin' ? 'محفوظ کر کے شائع کریں' : me.role === 'mod-l1' ? 'محفوظ کر کے اشاعت کے لیے بھیجیں' : 'محفوظ کر کے جائزے کے لیے بھیجیں'}</button>
<button name="act" value="save" class="btn primary"><Icon name="save" size={18} /> محفوظ کریں</button>
<button name="act" value="submit" class="btn primary"><Icon name={me.role === 'admin' ? 'upload' : 'send'} size={18} /> {me.role === 'admin' ? 'محفوظ کر کے شائع کریں' : me.role === 'mod-l1' ? 'محفوظ کر کے اشاعت کے لیے بھیجیں' : 'محفوظ کر کے جائزے کے لیے بھیجیں'}</button>
</div>
</form>
) : may.save ? (
@ -130,25 +133,25 @@ const acts = ([['approve', 'منظور کریں'], ['return', 'واپس بھیج
<fieldset class="meta-fields"><legend>کلام کا خاکہ</legend><div id="meta-rows"></div>
<button type="button" id="meta-add" class="tool">+ معلومات</button></fieldset>
<div class="toolbar" role="toolbar" aria-label="ایڈیٹر">
<button type="button" class="tool" data-cmd="couplet" title="نیا شعر (دو مصرعے)">شعر</button>
<button type="button" class="tool" data-cmd="para" title="نثر کا نیا پیراگراف">پیراگراف</button>
<button type="button" class="tool" data-cmd="h2" title="باب کا عنوان">باب کا عنوان</button>
<button type="button" class="tool" data-cmd="h3" title="ذیلی عنوان">ذیلی عنوان</button>
<button type="button" class="tool" data-cmd="couplet" title="نیا شعر (دو مصرعے)" aria-label="نیا شعر (دو مصرعے)"><Icon name="couplet" size={18} /></button>
<button type="button" class="tool" data-cmd="para" title="نثر کا نیا پیراگراف" aria-label="نثر کا نیا پیراگراف"><Icon name="para" size={18} /></button>
<button type="button" class="tool" data-cmd="h2" title="باب کا عنوان" aria-label="باب کا عنوان"><Icon name="h2" size={18} /></button>
<button type="button" class="tool" data-cmd="h3" title="ذیلی عنوان" aria-label="ذیلی عنوان"><Icon name="h3" size={18} /></button>
<span class="sep"></span>
<button type="button" class="tool" data-cmd="label" data-label="مطلع">مطلع</button>
<button type="button" class="tool" data-cmd="label" data-label="حسن مطلع">حسن مطلع</button>
<button type="button" class="tool" data-cmd="label" data-label="مقطع">مقطع</button>
<span class="sep"></span>
<button type="button" class="tool" data-cmd="dict" title="منتخب لفظ کو لغت سے جوڑیں">لغت کا ربط</button>
<button type="button" class="tool" data-cmd="note" title="حاشیہ لگائیں">حاشیہ</button>
<button type="button" class="tool" data-cmd="variant" title="نسخوں کا فرق">نسخہ</button>
<button type="button" class="tool" data-cmd="dict" title="منتخب لفظ کو لغت سے جوڑیں" aria-label="منتخب لفظ کو لغت سے جوڑیں"><Icon name="dict" size={18} /></button>
<button type="button" class="tool" data-cmd="note" title="حاشیہ لگائیں" aria-label="حاشیہ لگائیں"><Icon name="note" size={18} /></button>
<button type="button" class="tool" data-cmd="variant" title="نسخوں کا فرق" aria-label="نسخوں کا فرق"><Icon name="variant" size={18} /></button>
<span class="sep"></span>
<button type="button" class="tool" data-cmd="undo" title="پچھلی تبدیلی واپس">واپس</button>
<button type="button" class="tool" data-cmd="redo" title="تبدیلی دوبارہ">دوبارہ</button>
<button type="button" class="tool" data-urdu-rom="dv-editor-host" title="رومن حروف سے اردو">اب</button>
<button type="button" class="tool" data-urdu-kb="dv-editor-host" title="اردو کی بورڈ">کی بورڈ</button>
<button type="button" class="tool" data-cmd="undo" title="پچھلی تبدیلی واپس" aria-label="پچھلی تبدیلی واپس"><Icon name="undo" size={18} /></button>
<button type="button" class="tool" data-cmd="redo" title="تبدیلی دوبارہ" aria-label="تبدیلی دوبارہ"><Icon name="redo" size={18} /></button>
<button type="button" class="tool" data-urdu-rom="dv-editor-host" title="رومن حروف سے اردو" aria-label="رومن حروف سے اردو">اب</button>
<button type="button" class="tool" data-urdu-kb="dv-editor-host" title="اردو کی بورڈ" aria-label="اردو کی بورڈ"><Icon name="keyboard" size={18} /></button>
<span class="sep"></span>
<button type="button" class="tool" data-cmd="code" aria-pressed="false" title="ساتھ ساتھ دیوان متن (کوڈ) دیکھیں">کوڈ</button>
<button type="button" class="tool" data-cmd="code" aria-pressed="false" title="ساتھ ساتھ دیوان متن (کوڈ) دیکھیں" aria-label="ساتھ ساتھ دیوان متن (کوڈ) دیکھیں"><Icon name="code" size={18} /></button>
</div>
<div class="editor-split">
<div id="dv-editor-host"></div>
@ -160,8 +163,8 @@ const acts = ([['approve', 'منظور کریں'], ['return', 'واپس بھیج
<textarea name="content" id="mod-content" data-urdu rows="22" dir="rtl" spellcheck="false">{rev.content}</textarea>
</label>
<div class="filter-actions">
<button name="act" value="save">محفوظ کریں</button>
<button name="act" value="submit">{me.role === 'admin' ? 'محفوظ کر کے شائع کریں' : me.role === 'mod-l1' ? 'محفوظ کر کے اشاعت کے لیے بھیجیں' : 'محفوظ کر کے جائزے کے لیے بھیجیں'}</button>
<button name="act" value="save" class="btn primary"><Icon name="save" size={18} /> محفوظ کریں</button>
<button name="act" value="submit" class="btn primary"><Icon name={me.role === 'admin' ? 'upload' : 'send'} size={18} /> {me.role === 'admin' ? 'محفوظ کر کے شائع کریں' : me.role === 'mod-l1' ? 'محفوظ کر کے اشاعت کے لیے بھیجیں' : 'محفوظ کر کے جائزے کے لیے بھیجیں'}</button>
</div>
</form>
) : rev.summary && <p class="center">خلاصہ: {rev.summary}</p>}
@ -186,7 +189,7 @@ const acts = ([['approve', 'منظور کریں'], ['return', 'واپس بھیج
{acts.length > 0 && (
<form method="post" class="account-form mod-actions">
<label>تبصرہ (واپس بھیجنے یا مسترد کرنے کے لیے ضروری)<textarea name="comment" rows="2" data-urdu maxlength="2000"></textarea></label>
<div class="filter-actions">{acts.map(([a, label]) => <button name="act" value={a} class={`act-${a}`}>{label}</button>)}</div>
<div class="filter-actions">{acts.map(([a, label, role, icon]) => <button name="act" value={a} class={`btn ${role} act-${a}`}><Icon name={icon} size={18} /> {label}</button>)}</div>
</form>
)}
@ -270,7 +273,7 @@ const acts = ([['approve', 'منظور کریں'], ['return', 'واپس بھیج
const KNOWN = ['عنوان', 'شاعر', 'مصنف', 'صنف', 'ردیف', 'قافیہ', 'بحر', 'ماخذ', 'ماخذ_ربط'];
const row = (k = '', v = '') => {
const r = document.createElement('div'); r.className = 'meta-row';
r.innerHTML = '<input class="meta-k" list="meta-keys" aria-label="معلومات کا نام"><input class="meta-v" data-urdu aria-label="قدر"><button type="button" class="tool" aria-label="ہٹائیں">✕</button>';
r.innerHTML = '<input class="meta-k" list="meta-keys" aria-label="معلومات کا نام"><input class="meta-v" data-urdu aria-label="قدر"><button type="button" class="tool" title="ہٹائیں" aria-label="ہٹائیں"><svg class="icon" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>';
(r.children[0] as HTMLInputElement).value = k; (r.children[1] as HTMLInputElement).value = v;
(r.children[2] as HTMLButtonElement).onclick = () => r.remove();
rows.append(r);

View File

@ -55,8 +55,12 @@ const centuries = Object.fromEntries(Array.from({ length: 16 }, (_, c) => [c, ce
const state: any[] = data.sections.map((s: any) => ({ title: s.title ?? '', century: s.century, sort: s.sort, poets: [...s.poets] }));
const host = document.getElementById('sections')!;
const norm = (s: string) => s.normalize('NFC').replace(/[ً-ْٰٔ]/g, '').replace(/[يى]/g, 'ی').replace(/ك/g, 'ک').replace(/[هۂ]/g, 'ہ');
// arrows, ✕ and ہٹائیں are drawn as icons (the same Lucide paths as Icon.astro); the words stay as tooltip and label
const ICON: Record<string, string> = { '↑': '<path d="m5 12 7-7 7 7"/><path d="M12 19V5"/>', '↓': '<path d="M12 5v14"/><path d="m19 12-7 7-7-7"/>', '✕': '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>', 'ہٹائیں': '<path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/>' };
const btn = (label: string, title: string, fn: () => void) => {
const b = document.createElement('button'); b.type = 'button'; b.className = 'tool'; b.textContent = label; b.title = title;
const b = document.createElement('button'); b.type = 'button'; b.className = 'tool'; b.title = title;
if (ICON[label]) { b.innerHTML = `<svg class="icon" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${ICON[label]}</svg>`; b.setAttribute('aria-label', title); }
else b.textContent = label;
b.onclick = () => { fn(); draw(); }; return b;
};
// a box with the Urdu keyboard and Roman typing buttons inside it, as in search (Base.astro handles them)

View File

@ -1,6 +1,7 @@
---
// Search: poets/writers by name and books/chapters by title (first page), then the content results, paged
import Base from '../layouts/Base.astro';
import Icon from '../components/Icon.astro';
import Seg from '../components/Seg.astro';
import Tags from '../components/Tags.astro';
import { api } from '../lib/api';
@ -42,7 +43,7 @@ const from = res ? (pageNo - 1) * res.pageSize + 1 : 0, to = res ? Math.min(res.
<div class="poets">
{res.poets.map((p: any) => (
<div class="poet-result">
<a class="poet-card" href={p.url}><Seg s={hl(p.nickname)} /><small>{years(p)}</small></a>
<a class="poet-card" href={p.url}><span class="name"><Seg s={hl(p.nickname)} /></span><small>{years(p)}</small></a>
<a class="muted small" href={link(1, [p.id])}>{ud(p.works)} کلام میں «{q}»</a>
</div>
))}
@ -122,13 +123,13 @@ const from = res ? (pageNo - 1) * res.pageSize + 1 : 0, to = res ? Math.min(res.
))}
{pages > 1 && (
<nav class="pager" aria-label="صفحات">
{pageNo > 1 ? <a href={link(pageNo - 1)} rel="prev">‹ پچھلا</a> : <span />}
{pageNo > 1 ? <a class="icon-btn" href={link(pageNo - 1)} rel="prev" title="پچھلا" aria-label="پچھلا"><Icon name="prev" /></a> : <span />}
<span class="pages">
{numbers.map((n) => n === null ? <span class="gap">…</span>
: n === pageNo ? <span class="current" aria-current="page">{ud(n)}</span>
: <a href={link(n)}>{ud(n)}</a>)}
</span>
{pageNo < pages ? <a href={link(pageNo + 1)} rel="next">اگلا ›</a> : <span />}
{pageNo < pages ? <a class="icon-btn" href={link(pageNo + 1)} rel="next" title="اگلا" aria-label="اگلا"><Icon name="next" /></a> : <span />}
</nav>
)}
</section>

View File

@ -0,0 +1,22 @@
---
// پسندیدہ لکھاری: the writers (poets and prose writers) the reader saved, apart from their saved texts
import Base from '../layouts/Base.astro';
import LibraryNav from '../components/LibraryNav.astro';
import { asUser, COOKIE } from '../lib/auth';
import { ud } from '../lib/urdu';
const me = Astro.locals.user;
if (!me) return Astro.redirect('/signin?next=/writers');
const lib = (await asUser(Astro.cookies.get(COOKIE)!.value, '/api/library')).data;
const writers = lib.poets.filter((p: any) => p.poet);
---
<Base title="پسندیدہ لکھاری">
<h1>پسندیدہ لکھاری</h1>
<LibraryNav current="writers" />
{writers.length === 0
? <p class="muted center">ابھی کوئی لکھاری محفوظ نہیں۔ کسی شاعر یا ادیب کے صفحے پر "محفوظ کریں" دبائیں۔</p>
: <>
<p class="muted center">{ud(writers.length)} لکھاری</p>
<div class="poets">{writers.map((p: any) => <a class="poet-card" href={p.poet.url} title={p.poet.name}><span class="name">{p.poet.name}</span></a>)}</div>
</>}
</Base>

View File

@ -1,61 +1,104 @@
/* Divan styles: Ganjoor's look (palette, gold pills, centred couplets with ✤), RTL, Naskh default. */
[hidden] { display: none !important; } /* layout rules (e.g. .site-header .wrap flex) must not unhide */
/* Design tokens. Palettes set colours only, each as light-dark(light, dark); roles below derive from them.
color-scheme picks the half: the system's by default, or the reader's choice (data-theme on <html>). */
:root {
--bg: #fbf6ea; --paper: #fffdf7; --poem: #f6edd6; --inner: #f6efe3; --nested: #eee3d0;
--ink: #17130f; --muted: #635543; --lapis: #0d1d30; --gold: #b88828; --gold-light: #dfcd9f;
--gold-subtle: #f5edd8; --border: #b8a78c; --brand: #bf3f2a;
--font: 'Noto Naskh Arabic', 'Noto Nastaliq Urdu', serif; --verse-lh: 2.1;
color-scheme: light;
color-scheme: light dark;
/* surfaces, back to front */
--surface-page: light-dark(#fbf6ea, #14120f); --surface-raised: light-dark(#fffdf7, #181510);
--surface-poem: light-dark(#f6edd6, #1c1916); --surface-sunken: light-dark(#f6efe3, #242019); /* sunken: control fills */
/* text and lines */
--text: light-dark(#17130f, #e7ded0); --text-muted: light-dark(#635543, #9e9185); --link: light-dark(#0d1d30, #f0f6fc);
--border: light-dark(#b8a78c, #9b7332); --line: light-dark(#dfcd9f, #3a322b); /* line: soft dividers */
/* brand: ornaments (wordmark, ✤, takhallus); accent: control outlines, highlights */
--brand: light-dark(#bf3f2a, #d4553a); --accent: light-dark(#b88828, #c9a050); --accent-soft: light-dark(#f5edd8, #2a2418);
/* roles: primary actions (filled), confirming (approve, publish), destructive (log out, delete, reject) */
--action: light-dark(#1f4e79, #7db3e8); --action-ink: light-dark(#fff, #0d1520);
--ok: light-dark(#2e7d32, #7cc47f); --danger: light-dark(#b3261e, #f07a6e);
/* derived roles: change these to separate a job from its colour */
--heading: var(--link); --active: var(--brand); --focus: var(--accent);
/* scales */
--r-sm: 8px; --r-md: 12px; --r-lg: 16px; --r-pill: 999px;
--fs-xs: .75rem; --fs-sm: .85rem; --fs-md: .95rem; --fs-lg: 1.1rem; --fs-xl: 1.25rem; --fs-2xl: 1.7rem;
--shadow-sm: 0 2px 8px rgb(0 0 0 / .08); --shadow-lg: 0 8px 24px rgb(0 0 0 / .18);
--font: 'Noto Naskh Arabic', 'Noto Nastaliq Urdu', serif; --font-ui: 'Noto Naskh Arabic', serif; --verse-lh: 2.1;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme='light']) {
--bg: #14120f; --paper: #181510; --poem: #1c1916; --inner: #242019; --nested: #2d2620;
--ink: #e7ded0; --muted: #9e9185; --lapis: #f0f6fc; --gold: #c9a050; --gold-light: #3a322b;
--gold-subtle: #2a2418; --border: #9b7332; --brand: #d4553a; color-scheme: dark;
:root[data-theme='light'] { color-scheme: light; }
:root[data-theme='dark'] { color-scheme: dark; }
/* colour palettes (owner request): sepia (above) and four calmer colourful ones; chosen in the display settings (data-palette) */
:root[data-palette='teal'] {
--surface-page: light-dark(#f2f8f7, #0f1716); --surface-raised: light-dark(#fcfefe, #121c1b); --surface-poem: light-dark(#e2f0ee, #152220); --surface-sunken: light-dark(#e9f3f1, #1a2927);
--text: light-dark(#0f1f1d, #dcebe8); --text-muted: light-dark(#4a615e, #8fa8a4); --link: light-dark(#0b4f6c, #e6f4f2); --border: light-dark(#8db8b1, #2f7d72); --line: light-dark(#b6dad4, #23413c);
--brand: light-dark(#c8553d, #e07a5f); --accent: light-dark(#2a9d8f, #3fb5a5); --accent-soft: light-dark(#e4f2ef, #172b28);
--action: light-dark(#0b4f6c, #5cc2b3); --action-ink: light-dark(#fff, #0c1514);
}
:root[data-palette='lapis'] {
--surface-page: light-dark(#f3f5fb, #10131c); --surface-raised: light-dark(#fdfdff, #131724); --surface-poem: light-dark(#e6ebf8, #171c2c); --surface-sunken: light-dark(#ecf0f9, #1c2235);
--text: light-dark(#131a2e, #e1e5f2); --text-muted: light-dark(#4e5772, #98a0bb); --link: light-dark(#1d3b8b, #eef1fb); --border: light-dark(#98a6c8, #4b5c9a); --line: light-dark(#d8d0ad, #2c3350);
--brand: light-dark(#b83254, #e0607e); --accent: light-dark(#c49a2c, #d4b25a); --accent-soft: light-dark(#eef0f8, #1b2034);
--action: light-dark(#1d3b8b, #8ea9ee); --action-ink: light-dark(#fff, #0e1220);
}
:root[data-theme='dark'] {
--bg: #14120f; --paper: #181510; --poem: #1c1916; --inner: #242019; --nested: #2d2620;
--ink: #e7ded0; --muted: #9e9185; --lapis: #f0f6fc; --gold: #c9a050; --gold-light: #3a322b;
--gold-subtle: #2a2418; --border: #9b7332; --brand: #d4553a; color-scheme: dark;
:root[data-palette='green'] {
--surface-page: light-dark(#f3f7f0, #111611); --surface-raised: light-dark(#fdfefb, #141a13); --surface-poem: light-dark(#e5eede, #182017); --surface-sunken: light-dark(#ecf3e7, #1d271b);
--text: light-dark(#151f0f, #e0e8d8); --text-muted: light-dark(#54614a, #9aa88e); --link: light-dark(#1f5d3a, #eef5e8); --border: light-dark(#9db38c, #4f7a45); --line: light-dark(#d7cea2, #2c3a27);
--brand: light-dark(#b5452f, #e0705a); --accent: light-dark(#a8872e, #c9a94e); --accent-soft: light-dark(#eff1e1, #1c251a);
--action: light-dark(#1f5d3a, #84c79a); --action-ink: light-dark(#fff, #0d150f);
}
:root.nastaliq { --font: 'Noto Nastaliq Urdu', 'Noto Naskh Arabic', serif; --verse-lh: 2.7; }
:root[data-palette='rose'] {
--surface-page: light-dark(#fbf4f6, #181214); --surface-raised: light-dark(#fffcfd, #1c1518); --surface-poem: light-dark(#f5e6ea, #22191c); --surface-sunken: light-dark(#f8edf0, #291e22);
--text: light-dark(#29141a, #efe0e4); --text-muted: light-dark(#6c5057, #b39aa1); --link: light-dark(#6b2c5e, #fbeef2); --border: light-dark(#c8a1a9, #8a5563); --line: light-dark(#e7c8ce, #3e2a30);
--brand: light-dark(#b0304a, #ef6f86); --accent: light-dark(#c27c88, #d792a0); --accent-soft: light-dark(#f7e8ec, #2a1d21);
--action: light-dark(#6b2c5e, #e3a3c3); --action-ink: light-dark(#fff, #1d1216);
}
:root.nastaliq { --font: 'Noto Nastaliq Urdu', var(--font-ui); --verse-lh: 2.7; }
* { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--ink); font-family: var(--font); line-height: 1.9; }
a { color: var(--lapis); text-decoration: none; }
a:hover { color: var(--brand); }
body { margin: 0; background: var(--surface-page); color: var(--text); font-family: var(--font); line-height: 1.9; }
:where(a) { color: var(--link); text-decoration: none; }
:where(a):hover { color: var(--active); }
.wrap { max-width: 960px; margin: 0 auto; padding: 0 16px; }
.muted { color: var(--muted); }
.muted { color: var(--text-muted); }
/* primitives. control: every button, chip, tab and pager link; a pill unless a component says otherwise; pressed, selected,
current and checked all look the same (--active). field: every text box, text area and select. Both are :where() (no
specificity), so component rules below only add size, shape and layout. */
:where(.btn, .mod-edit-btn, .save-like, .save, .wordbook, .tool, .chip, .tag-chip, .account-link, .poem-nav a, .admin-nav a, .pager .pages a, .pager .pages .current, .kb button, .dict .similar button, .dict .play, .account-form button, .admin-search button, .admin-table button, .lib-note button, .lib-remove button, .author-suggest button, .filter-actions button, .editor-tabs button, .display-panel button) {
display: inline-flex; align-items: center; justify-content: center; gap: 6px; font: inherit; font-family: var(--font-ui);
font-size: var(--fs-sm); line-height: 1.8; padding: 1px 14px; border: 1.5px solid var(--border); border-radius: var(--r-pill);
background: var(--surface-sunken); color: var(--text); cursor: pointer; text-decoration: none; transition: border-color .15s, color .15s, background .15s; }
:where(.btn, .mod-edit-btn, .save-like, .save, .wordbook, .tool, .chip, .tag-chip, .account-link, .poem-nav a, .admin-nav a, .pager .pages a, .pager .pages .current, .kb button, .dict .similar button, .dict .play, .account-form button, .admin-search button, .admin-table button, .lib-note button, .lib-remove button, .author-suggest button, .filter-actions button, .editor-tabs button, .display-panel button):hover { border-color: var(--active); color: var(--active); }
:where(.btn, .mod-edit-btn, .save-like, .save, .wordbook, .tool, .chip, .tag-chip, .account-link, .poem-nav a, .admin-nav a, .pager .pages a, .pager .pages .current, .kb button, .dict .similar button, .dict .play, .account-form button, .admin-search button, .admin-table button, .lib-note button, .lib-remove button, .author-suggest button, .filter-actions button, .editor-tabs button, .display-panel button):is([aria-pressed='true'], [aria-selected='true'], [aria-current='page'], .current, .on, :has(input:checked)) {
border-color: var(--active); color: var(--active); background: color-mix(in srgb, var(--active) 10%, var(--surface-sunken)); font-weight: 700; }
:where(.btn, .mod-edit-btn, .save-like, .save, .wordbook, .tool, .chip, .tag-chip, .account-link, .poem-nav a, .admin-nav a, .pager .pages a, .pager .pages .current, .kb button, .dict .similar button, .dict .play, .account-form button, .admin-search button, .admin-table button, .lib-note button, .lib-remove button, .author-suggest button, .filter-actions button, .editor-tabs button, .display-panel button):disabled { opacity: .5; cursor: not-allowed; }
:where(input:not([type='checkbox'], [type='radio'], [type='file'], [type='range'], [type='color']), textarea, select) {
font: inherit; padding: 4px 12px; border: 1.5px solid var(--border); border-radius: var(--r-md); background: var(--surface-raised); color: var(--text); }
:where(input:not([type='checkbox'], [type='radio'], [type='file'], [type='range'], [type='color']), textarea, select):focus { outline: none; border-color: var(--focus); box-shadow: 0 0 0 3px color-mix(in srgb, var(--focus) 25%, transparent); }
:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
/* header: logo + search + toggles in a gold-bordered bar */
.site-header .wrap { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; padding-top: 12px; }
.wordmark { font-family: 'Noto Nastaliq Urdu', serif; font-size: 2rem; line-height: 1.5; color: var(--brand); font-weight: 700; }
.search-form { flex: 1; min-width: 200px; display: flex; position: relative; }
.search-form input, .toggles button {
font: inherit; border: 1.5px solid var(--border); background: var(--inner); color: var(--ink); border-radius: 10px;
}
.search-form input { flex: 1; padding: 4px 14px; padding-inline-end: 106px; }
.search-form input:focus { outline: none; border-color: var(--gold); }
.toggles { display: flex; gap: 6px; }
.toggles button { padding: 2px 12px; cursor: pointer; font-size: .85rem; }
.search-form { flex: 1; min-width: 0; display: flex; position: relative; }
.search-form input { background: var(--surface-sunken); }
.search-form input { flex: 1; min-width: 0; padding: 4px 14px; padding-inline-end: 106px; } /* min-width 0: shrinks instead of pushing past the icons */
.toggles { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; } /* icon buttons with tooltips */
.toggles form { display: flex; margin: 0; } /* the log-out button sits in a form: no line box of its own */
/* controls keep Naskh at normal height whatever reading font is chosen */
.search-form input, .toggles button { font-family: 'Noto Naskh Arabic', serif; line-height: 1.8; }
.toggles button:hover { border-color: var(--brand); }
.search-form input { font-family: var(--font-ui); line-height: 1.8; }
main { padding: 16px 0 48px; }
h1, h2 { line-height: 1.6; margin: .2em 0 .4em; font-weight: 700; text-align: center; color: var(--lapis); }
h1 { font-size: 1.7rem; }
h1, h2 { line-height: 1.6; margin: .2em 0 .4em; font-weight: 700; text-align: center; color: var(--heading); }
h1 { font-size: clamp(1.3rem, 5vw, var(--fs-2xl)); }
h1::after { content: ''; display: block; width: 140px; height: 2px; background: var(--brand); margin: 8px auto 0; }
/* Ganjoor's gold pill headings and links (.century / .part-title-block) */
.pill, .century h2, .card {
display: block; text-align: center; color: var(--lapis); background: var(--gold-subtle);
display: block; text-align: center; color: var(--link); background: var(--accent-soft);
border: 1.5px solid var(--border); border-radius: 10px; padding: 4px 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, .08); transition: transform .2s, background .2s, border-color .2s;
}
.card:hover { background: var(--gold-light); border-color: var(--brand); transform: translateY(-1px); color: var(--lapis); }
.card small { color: var(--muted); margin-inline-start: 6px; }
.card:hover { background: var(--line); border-color: var(--active); transform: translateY(-1px); color: var(--link); }
.card small { color: var(--text-muted); margin-inline-start: 6px; }
.century { margin-top: 28px; }
.century h2 { font-size: 1rem; margin: 0 0 12px; }
.century h2::after { display: none; }
@ -65,71 +108,80 @@ h1::after { content: ''; display: block; width: 140px; height: 2px; background:
.poet-card {
/* one size for every card: long names end in … (full name on hover) */
width: 210px; max-width: calc(50% - 4px); min-height: 5.125rem; text-align: center;
display: flex; flex-direction: column; justify-content: center; background: var(--paper); border: 1.5px solid var(--border);
border-radius: 999px; padding: 6px 36px 8px; color: var(--ink); transition: transform .25s; position: relative;
display: flex; flex-direction: column; justify-content: center; background: var(--surface-raised); border: 1.5px solid var(--border);
border-radius: 999px; padding: 6px 36px 8px; color: var(--text); transition: transform .25s; position: relative;
}
/* the bookmark sits in the top-left corner, inside the side padding (36px), so it never covers the name and a saved card keeps its size */
.poet-card.mark-bm::after { position: absolute; top: 12px; left: 16px; margin: 0; }
.poet-card:hover { transform: translateY(-3px); border-color: var(--brand); color: var(--ink); }
.poet-card:hover { transform: translateY(-3px); border-color: var(--active); color: var(--text); }
.poet-card .name { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* years in Naskh: compact lines whatever the reading font (Nastaliq's tall line boxes made cards grow in some browsers) */
.poet-card small { display: block; color: var(--muted); font-family: 'Noto Naskh Arabic', serif; font-size: .75rem; line-height: 1.6; white-space: nowrap; }
.poet-card small { display: block; color: var(--text-muted); font-family: var(--font-ui); font-size: .75rem; line-height: 1.6; white-space: nowrap; }
/* narrow phones: a rounded box instead of a pill (the pill's curve ate the name) and names wrap to two lines */
@media (max-width: 480px) {
.poet-card { padding: 6px 12px 8px; border-radius: var(--r-lg); min-height: 0; }
.poet-card .name { white-space: normal; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; line-height: 1.7; }
.poet-card.mark-bm { padding-left: 28px; }
.poet-card small { white-space: normal; }
.poet-card.mark-bm::after { top: 10px; left: 8px; }
}
.hero { text-align: center; }
.hero p { margin: 0; }
/* poet / category */
.intro { background: var(--paper); border: 1.5px solid var(--border); border-radius: 12px; padding: 10px 18px; white-space: pre-line; text-align: justify; }
.intro { background: var(--surface-raised); border: 1.5px solid var(--border); border-radius: 12px; padding: 10px 18px; white-space: pre-line; text-align: justify; }
.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px; margin: 16px 0; }
.crumbs { text-align: center; font-size: .9rem; color: var(--muted); }
.crumbs a { color: var(--muted); }
.crumbs { text-align: center; font-size: .9rem; color: var(--text-muted); }
.crumbs a { color: var(--text-muted); }
.crumbs span + span::before { content: '»'; margin: 0 8px; }
.toc { list-style: none; padding: 0; margin: 16px auto; max-width: 700px; }
.toc li { border-bottom: 1px dashed var(--gold-light); }
.toc li { border-bottom: 1px dashed var(--line); }
.toc li a { display: block; padding: 6px 4px; text-align: center; }
/* poem: Ganjoor couplets (half-lines meet at the centre, ✤ after each) */
.poem { background: var(--poem); border: 1.5px solid var(--border); border-radius: 12px; padding: 20px clamp(12px, 3vw, 36px); margin: 16px 0; }
.poem { background: var(--surface-poem); border: 1.5px solid var(--border); border-radius: 12px; padding: 20px clamp(12px, 3vw, 36px); margin: 16px 0; }
.couplet { display: grid; grid-template-columns: 1fr 1fr; column-gap: 10%; margin: 0 0 6px; border-radius: 6px; font-size: 1.1rem; line-height: var(--verse-lh); }
.couplet:hover { background: linear-gradient(90deg, transparent, var(--gold-subtle) 15%, var(--gold-subtle) 85%, transparent); }
.couplet p { margin: 0; text-align: left; }
.couplet[data-mark]::before { content: attr(data-mark); grid-column: 1 / -1; justify-self: center; font-size: .72rem; line-height: 1.6; color: var(--brand); border: 1px solid var(--gold-light); border-radius: 999px; padding: 0 10px; margin-bottom: 2px; }
.couplet:hover { background: linear-gradient(90deg, transparent, var(--accent-soft) 15%, var(--accent-soft) 85%, transparent); }
/* each half-line is a box as wide as its text, pinned to the centre line (end of the right column, start of the left one in
RTL), with its text centred: short ones meet at the centre as before, long ones fill the column and wrap centred */
.couplet p { margin: 0; width: fit-content; max-width: 100%; justify-self: end; text-align: center; }
.couplet[data-mark]::before { content: attr(data-mark); grid-column: 1 / -1; justify-self: center; font-size: .72rem; line-height: 1.6; color: var(--brand); border: 1px solid var(--line); border-radius: 999px; padding: 0 10px; margin-bottom: 2px; }
.takhallus { color: var(--brand); }
.radif { text-align: center; margin-top: -6px; }
.radif-head { text-align: center; font-size: 1.05rem; color: var(--brand); margin: 22px 0 4px; }
.couplet p + p { text-align: right; }
.couplet p + p { justify-self: start; }
.couplet::after { content: '✤'; grid-column: 1 / -1; text-align: center; color: var(--brand); font-size: .72rem; opacity: .75; line-height: 1.2; }
.single { text-align: center; margin: 0 0 6px; font-size: 1.1rem; line-height: var(--verse-lh); }
.para { text-align: justify; margin: 0 0 12px; line-height: var(--verse-lh); }
@media (max-width: 760px) {
.couplet { grid-template-columns: 1fr; padding: 6px 0; border-bottom: 1px dashed var(--gold-light); }
.couplet p, .couplet p + p { text-align: center; }
.couplet { grid-template-columns: 1fr; padding: 6px 0; border-bottom: 1px dashed var(--line); }
.couplet p, .couplet p + p { justify-self: center; }
.couplet::after { display: none; }
}
.poem-nav { display: flex; justify-content: center; gap: 12px; } /* RTL: پچھلا on the right, اگلا on the left */
.poem-nav a { border: 1.5px solid var(--border); background: var(--inner); border-radius: 10px; padding: 0 14px; }
.source { text-align: center; font-size: .85rem; color: var(--muted); }
.source { text-align: center; font-size: .85rem; color: var(--text-muted); }
/* search */
.result { border-bottom: 1px dashed var(--gold-light); padding: 8px 0; }
.result .snippet { display: block; color: var(--ink); margin-top: 4px; padding: 4px 12px; border-inline-start: 2px solid var(--gold-light); }
.result { border-bottom: 1px dashed var(--line); padding: 8px 0; }
.result .snippet { display: block; color: var(--text); margin-top: 4px; padding: 4px 12px; border-inline-start: 2px solid var(--line); }
.result .snippet p { margin: 0; }
mark { background: var(--gold-subtle); color: inherit; border-radius: 3px; padding: 0 2px; box-shadow: inset 0 -2px var(--gold); }
mark { background: var(--accent-soft); color: inherit; border-radius: 3px; padding: 0 2px; box-shadow: inset 0 -2px var(--accent); }
.hl-note { text-align: center; font-size: .85rem; }
.hint { text-align: center; font-size: .8rem; margin: 0; }
.pager { display: flex; gap: 12px; justify-content: center; margin-top: 20px; }
/* made by a Pakistani for Pakistan: the flag beside the line (an SVG: the emoji is "PK" on Windows) */
.made-in { margin: 10px 0 0; display: flex; align-items: center; justify-content: center; gap: 10px;
font-family: 'Noto Nastaliq Urdu', serif; font-size: 1.05rem; line-height: 2; color: var(--ink); } /* a tagline */
font-family: 'Noto Nastaliq Urdu', serif; font-size: 1.05rem; line-height: 2; color: var(--text); } /* a tagline */
.made-in .flag { flex: none; border-radius: 2px; box-shadow: 0 0 0 1px rgb(0 0 0 / .15); }
.site-footer { border-top: 1.5px solid var(--border); padding: 16px 0; font-size: .85rem; color: var(--muted); text-align: center; }
.site-footer { border-top: 1.5px solid var(--border); padding: 16px 0; font-size: .85rem; color: var(--text-muted); text-align: center; }
h1 + .muted { text-align: center; margin-top: 0; }
/* Urdu keyboard */
.in-box { position: absolute; inset-inline-end: 8px; top: 50%; transform: translateY(-50%); display: flex; padding: 4px; border: 0; background: none; color: var(--muted, currentColor); opacity: .7; cursor: pointer; border-radius: 6px; }
.in-box { position: absolute; inset-inline-end: 8px; top: 50%; transform: translateY(-50%); display: flex; padding: 4px; border: 0; background: none; color: var(--text-muted); opacity: .7; cursor: pointer; border-radius: 6px; }
/* icons inside a text box, from the far left: search (top box only), keyboard, Roman-to-Urdu */
.in-box.rom { font: 600 .95rem/1 'Noto Naskh Arabic', serif; }
.in-box.rom { font: 600 .95rem/1 var(--font-ui); }
.search-form .in-box.go { inset-inline-end: 8px; }
.search-form #kb-toggle { inset-inline-end: 38px; }
.search-form #rom-toggle { inset-inline-end: 68px; }
@ -137,46 +189,46 @@ h1 + .muted { text-align: center; margin-top: 0; }
.in-box-field input { padding-inline-end: 76px !important; }
.in-box-field .in-box.rom { inset-inline-end: 38px; }
.in-box:hover { opacity: 1; }
.in-box[aria-pressed='true'] { color: var(--brand); opacity: 1; }
.in-box[aria-pressed='true'] { color: var(--active); opacity: 1; }
/* phones: wordmark and icons on one row, the search box on its own full-width row below */
@media (max-width: 640px) {
.site-header .wrap { gap: 8px; }
.wordmark { font-size: 1.7rem; padding-bottom: .45em; margin-bottom: -.2em; } /* Nastaliq's descenders hang below the line box: room so they clear the search row */
.toggles { margin-inline-start: auto; gap: 4px; }
.search-form { order: 3; flex-basis: 100%; }
}
.site-header .kb:not([hidden]) { display: block; padding-bottom: 10px; } /* not the header's flex row */
.kb div { display: flex; flex-wrap: wrap; justify-content: center; gap: 4px; margin-top: 4px; }
.kb button {
font-family: 'Noto Naskh Arabic', serif; font-size: 1.1rem; min-width: 2.4em; padding: 2px 6px; cursor: pointer;
border: 1.5px solid var(--border); background: var(--paper); color: var(--ink); border-radius: 8px;
}
.kb button:hover { border-color: var(--brand); }
.kb button { font-size: var(--fs-lg); min-width: 2.4em; padding: 2px 6px; background: var(--surface-raised); border-radius: var(--r-sm); }
.kb .wide { min-width: 12em; }
.kb .fn { min-width: 3.4em; background: var(--inner); }
.kb .fn.on { border-color: var(--brand); color: var(--brand); }
.kb .fn { min-width: 3.4em; background: var(--surface-sunken); }
/* word sidebar (Wiktionary): left side on wide screens, bottom sheet on phones */
.dict { position: fixed; top: 0; left: 0; bottom: 0; width: min(340px, 92vw); z-index: 20; overflow-y: auto; padding: 14px 18px 24px;
background: var(--paper); border-right: 1.5px solid var(--border); box-shadow: 4px 0 18px rgb(0 0 0 / .18); }
background: var(--surface-raised); border-right: 1.5px solid var(--border); box-shadow: 4px 0 18px rgb(0 0 0 / .18); }
.dict h2 { font-size: 1.6rem; margin: 0 0 4px; color: var(--brand); text-align: start; }
.dict h2::after { display: none; }
.dict h3 { font-size: 1rem; margin: 14px 0 4px; padding-bottom: 2px; border-bottom: 1px dashed var(--gold-light); }
.dict h3 { font-size: 1rem; margin: 14px 0 4px; padding-bottom: 2px; border-bottom: 1px dashed var(--line); }
.dict ol { margin: 4px 0; padding-inline-start: 22px; }
.dict .en { font-family: system-ui, sans-serif; font-size: .88rem; line-height: 1.5; }
.dict ol.en { text-align: left; }
.dict .ipa { font-family: system-ui, sans-serif; font-size: .88rem; color: var(--muted); }
.dict .ipa { font-family: system-ui, sans-serif; font-size: .88rem; color: var(--text-muted); }
.dict .pron { margin: 2px 0; }
.dict .pos { font-size: .8rem; color: var(--muted); margin: 8px 0 0; }
.dict .pos { font-size: .8rem; color: var(--text-muted); margin: 8px 0 0; }
.dict .note { font-size: .8rem; margin: 0; }
.dict .via { margin: 2px 0; }
.dict details.more { margin: 4px 0; }
.dict details.more summary { cursor: pointer; font-size: .8rem; color: var(--brand); }
.dict .example { font-size: .8rem; color: var(--muted); margin: 4px 0; border-left: 2px solid var(--gold-light); padding-left: 8px; }
.dict .example .ex-text { font-family: 'Noto Naskh Arabic', serif; font-size: .95rem; color: var(--ink); }
.dict .reading { border-inline-start: 2px solid var(--gold-light); padding-inline-start: 10px; margin: 8px 0; }
.dict .form { font-size: 1.15rem; color: var(--lapis); }
.dict .example { font-size: .8rem; color: var(--text-muted); margin: 4px 0; border-left: 2px solid var(--line); padding-left: 8px; }
.dict .example .ex-text { font-family: var(--font-ui); font-size: .95rem; color: var(--text); }
.dict .reading { border-inline-start: 2px solid var(--line); padding-inline-start: 10px; margin: 8px 0; }
.dict .form { font-size: 1.15rem; color: var(--link); }
.dict .via .en { font-size: .78rem; }
.dict .src { font-size: .78rem; margin-top: 18px; }
.dict .ety { text-align: left; color: var(--muted); font-size: .8rem; }
.dict-close { position: sticky; top: 0; float: left; border: 0; background: none; color: var(--muted); font-size: 1rem; cursor: pointer; }
.dict .ety { text-align: left; color: var(--text-muted); font-size: .8rem; }
.dict-close { position: sticky; top: 0; float: left; border: 0; background: none; color: var(--text-muted); font-size: 1rem; cursor: pointer; }
.dict .similar { display: flex; flex-wrap: wrap; gap: 6px; }
.dict .similar button { font: inherit; border: 1px solid var(--border); background: var(--inner); color: var(--ink); border-radius: 999px; padding: 0 12px; cursor: pointer; }
.dict .similar button:hover { border-color: var(--brand); color: var(--brand); }
.dict .play { border: 1px solid var(--border); background: var(--inner); border-radius: 8px; cursor: pointer; padding: 0 6px; }
.dict .play { padding: 0 6px; border-radius: var(--r-sm); }
/* wide screens: the page moves over instead of being covered */
@media (min-width: 1100px) { body.dict-open { padding-left: min(340px, 92vw); } }
.dict ol:not(.en) { list-style-type: urdu; }
@ -186,24 +238,20 @@ h1 + .muted { text-align: center; margin-top: 0; }
/* accounts */
.center { text-align: center; }
.account-link { font: inherit; font-size: .9rem; padding: 2px 12px; border: 1.5px solid var(--border); border-radius: 10px; background: var(--inner); color: var(--ink); text-decoration: none; }
.account-form { max-width: 420px; margin: 18px auto; display: flex; flex-direction: column; gap: 10px; }
.account-form h2 { font-size: 1.1rem; margin: 6px 0 0; }
.account-form label { display: flex; flex-direction: column; gap: 4px; font-size: .9rem; }
.account-form input { font: inherit; padding: 6px 12px; border: 1.5px solid var(--border); border-radius: 10px; background: var(--paper); color: var(--ink); }
.account-form textarea { font: inherit; line-height: 1.9; padding: 6px 12px; border: 1.5px solid var(--border); border-radius: 10px; background: var(--paper); color: var(--ink); resize: vertical; }
.account-form input:focus, .account-form textarea:focus { outline: none; border-color: var(--gold); }
.account-form input, .account-form textarea { padding: 6px 12px; }
.account-form textarea { line-height: 1.9; resize: vertical; }
.profile-bio { max-width: 520px; margin: 0 auto 12px; text-align: center; white-space: pre-line; }
.account-form button { font: inherit; padding: 6px 14px; border: 1.5px solid var(--gold); border-radius: 10px; background: var(--inner); color: var(--ink); cursor: pointer; }
.account-form button:hover { border-color: var(--brand); color: var(--brand); }
.account-form > button { font-size: inherit; padding: 4px 14px; }
/* boxes with the keyboard buttons inside them: full width, and the small in-box buttons keep their own look */
.account-form .in-box-field { display: block; position: relative; }
.account-form .in-box-field input { width: 100%; box-sizing: border-box; }
.account-form .in-box { padding: 4px; border: 0; border-radius: 6px; background: none; color: var(--muted, currentColor); }
.account-form .author-suggest button, .account-form .tag-chip { padding: 0 10px; border-radius: 999px; }
.account-form.danger button { border-color: var(--brand); color: var(--brand); }
.form-error { color: var(--brand); text-align: center; }
.form-done { color: var(--gold); text-align: center; }
.account-form .in-box { padding: 4px; border: 0; border-radius: 6px; background: none; color: var(--text-muted); }
.account-form.danger button { border-color: var(--danger); color: var(--danger); }
.form-error { color: var(--danger); text-align: center; }
.form-done { color: var(--accent); text-align: center; }
/* selection menu (Ganjoor's, by the selected text) */
.selmenu { position: absolute; z-index: 30; display: flex; gap: 2px; padding: 4px; border-radius: 10px;
background: rgb(14 17 17 / .92); box-shadow: 0 4px 14px rgb(0 0 0 / .25); }
@ -214,25 +262,22 @@ h1 + .muted { text-align: center; margin-top: 0; }
/* admin panel */
.admin-nav { display: flex; justify-content: center; gap: 8px; margin: 0 0 16px; }
.admin-nav a { padding: 2px 14px; border: 1.5px solid var(--border); border-radius: 999px; text-decoration: none; color: var(--ink); }
.admin-nav a[aria-current='page'] { border-color: var(--brand); color: var(--brand); }
.admin-search { display: flex; gap: 6px; max-width: 420px; margin: 0 auto 8px; }
.admin-search input { flex: 1; font: inherit; padding: 4px 12px; border: 1.5px solid var(--border); border-radius: 10px; background: var(--paper); color: var(--ink); }
.admin-search button, .admin-table button { font: inherit; font-size: .8rem; padding: 1px 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--inner); color: var(--ink); cursor: pointer; }
.admin-table button:hover { border-color: var(--brand); color: var(--brand); }
.admin-table button.danger { color: var(--brand); }
.admin-search input { flex: 1; }
.admin-table button { font-size: var(--fs-xs); padding: 0 10px; }
.admin-table button.danger { color: var(--danger); }
.admin-table { width: 100%; border-collapse: collapse; font-size: .88rem; margin: 8px 0; }
.admin-table th, .admin-table td { padding: 6px 8px; border-bottom: 1px dashed var(--gold-light); text-align: start; vertical-align: middle; }
.admin-table th, .admin-table td { padding: 6px 8px; border-bottom: 1px dashed var(--line); text-align: start; vertical-align: middle; }
.admin-table tr.disabled td { opacity: .55; }
.admin-table .actions { display: flex; flex-wrap: wrap; gap: 4px; }
.admin-table .actions form { margin: 0; }
.temp-password { max-width: 520px; margin: 10px auto; padding: 10px 16px; border: 1.5px solid var(--gold); border-radius: 12px; background: var(--inner); text-align: center; }
.temp-password { max-width: 520px; margin: 10px auto; padding: 10px 16px; border: 1.5px solid var(--accent); border-radius: 12px; background: var(--surface-sunken); text-align: center; }
.temp-password code { font-size: 1.2rem; letter-spacing: .08em; user-select: all; }
@media (max-width: 700px) { .admin-table thead { display: none; } .admin-table tr { display: block; border-bottom: 1px dashed var(--gold-light); padding: 6px 0; } .admin-table td { display: inline-block; border: 0; padding: 2px 6px; } }
@media (max-width: 700px) { .admin-table thead { display: none; } .admin-table tr { display: block; border-bottom: 1px dashed var(--line); padding: 6px 0; } .admin-table td { display: inline-block; border: 0; padding: 2px 6px; } }
.role-form { display: flex; gap: 4px; align-items: center; }
.role-form select, .grant-form select { font: inherit; font-size: .85rem; padding: 2px 8px; border: 1px solid var(--border); border-radius: 8px; background: var(--paper); color: var(--ink); }
.grant-form fieldset { border: 1px dashed var(--gold-light); border-radius: 10px; display: flex; flex-wrap: wrap; gap: 4px 14px; padding: 6px 12px; }
.role-form select, .grant-form select { font-size: var(--fs-sm); padding: 2px 8px; }
.grant-form fieldset { border: 1px dashed var(--line); border-radius: 10px; display: flex; flex-wrap: wrap; gap: 4px 14px; padding: 6px 12px; }
.grant-form legend { font-size: .85rem; padding: 0 6px; }
.grant-form label.check { flex-direction: row; align-items: center; gap: 6px; }
@ -241,19 +286,15 @@ h1 + .muted { text-align: center; margin-top: 0; }
separate (+). */
:root { --bm-outline: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M7.5 3h9A2.5 2.5 0 0 1 19 5.5v14.6a1 1 0 0 1-1.55.83L12 17.3l-5.45 3.63A1 1 0 0 1 5 20.1V5.5A2.5 2.5 0 0 1 7.5 3z' fill='none' stroke='black' stroke-width='2' stroke-linejoin='round'/%3E%3C/svg%3E"); --bm-filled: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M7.5 3h9A2.5 2.5 0 0 1 19 5.5v14.6a1 1 0 0 1-1.55.83L12 17.3l-5.45 3.63A1 1 0 0 1 5 20.1V5.5A2.5 2.5 0 0 1 7.5 3z' stroke='black' stroke-width='2' stroke-linejoin='round'/%3E%3C/svg%3E"); }
.bm-icon, .save::before, .bm::before, .bmk::after, .mark-fav::after, .mark-bm::after {
content: ''; display: inline-block; width: 1.05em; height: 1.05em; vertical-align: -.18em; background-color: var(--muted);
content: ''; display: inline-block; width: 1.05em; height: 1.05em; vertical-align: -.18em; background-color: var(--text-muted);
-webkit-mask: var(--bm-outline) center / contain no-repeat; mask: var(--bm-outline) center / contain no-repeat; user-select: none; }
.save[aria-pressed='true']::before, .bm[aria-pressed='true']::before, .bmk::after, .mark-fav::after, .mark-bm::after, .bm-icon.on {
background-color: var(--brand); -webkit-mask-image: var(--bm-filled); mask-image: var(--bm-filled); }
.save { font: inherit; font-size: .85rem; padding: 1px 14px; border: 1.5px solid var(--border); border-radius: 999px; background: var(--inner); color: var(--ink); cursor: pointer; }
.save::before { margin-inline-end: 4px; }
.save[aria-pressed='true'] { border-color: var(--brand); color: var(--brand); font-weight: 700;
background: color-mix(in srgb, var(--brand) 12%, var(--inner)); } /* محفوظ شدہ: accent tint, filled glyph */
background-color: var(--active); -webkit-mask-image: var(--bm-filled); mask-image: var(--bm-filled); }
.save::before { margin: 0; } /* محفوظ شدہ: the control's pressed look, and the glyph fills */
/* word book (ذخیرۂ الفاظ): + to add, ✓ when added (hearts are for poets and works) */
.wordbook { font: inherit; font-size: .85rem; padding: 1px 14px; margin-bottom: 6px; border: 1.5px solid var(--border); border-radius: 999px; background: var(--inner); color: var(--ink); cursor: pointer; }
.wordbook::before { content: '+ '; font-weight: 700; color: var(--gold); }
.wordbook[aria-pressed='true'] { border-color: var(--gold); color: var(--gold); }
.wordbook[aria-pressed='true']::before { content: '✓ '; }
.wordbook { margin-bottom: 6px; }
.wordbook::before { content: '+'; font-weight: 700; color: var(--accent); }
.wordbook[aria-pressed='true']::before { content: '✓'; color: inherit; }
.couplet, .poem .para, .poem .single { position: relative; }
.bm { position: absolute; inset-inline-end: -26px; top: .5em; width: 20px; height: 22px; padding: 0; border: 0; background: none; cursor: pointer;
opacity: 0; transition: opacity .15s; user-select: none; }
@ -262,26 +303,26 @@ h1 + .muted { text-align: center; margin-top: 0; }
.couplet:hover .bm, .poem .para:hover .bm, .poem .single:hover .bm, .bm:focus-visible { opacity: 1; }
@media (hover: none) { .bm { opacity: 1; } }
@media (max-width: 700px) { .bm { inset-inline-end: -4px; top: 0; } }
:target { background: var(--gold-subtle); border-radius: 6px; }
:target { background: var(--accent-soft); border-radius: 6px; }
.lib-section { margin: 22px 0; }
.lib-section h2 { font-size: 1.1rem; text-align: center; color: var(--brand); }
.lib-list { list-style: none; padding: 0; max-width: 760px; margin: 0 auto; }
.lib-list li { position: relative; padding: 10px 14px 10px 40px; margin: 8px 0; border: 1px dashed var(--gold-light); border-radius: 12px; }
.lib-list li { position: relative; padding: 10px 14px 10px 40px; margin: 8px 0; border: 1px dashed var(--line); border-radius: 12px; }
.lib-list .crumbs { text-align: start; margin: 0 0 4px; font-size: .8rem; }
.lib-title { font-weight: 700; }
.lib-couplet { display: block; color: var(--ink); text-decoration: none; text-align: center; line-height: var(--verse-lh, 2.2); }
.lib-couplet { display: block; color: var(--text); text-decoration: none; text-align: center; line-height: var(--verse-lh, 2.2); }
.lib-couplet p { margin: 0; }
.lib-couplet.small { font-size: .9rem; color: var(--muted); }
.lib-word { font: inherit; font-size: 1.25rem; font-weight: 700; color: var(--lapis); background: none; border: 0; padding: 0; cursor: pointer; }
.lib-couplet.small { font-size: .9rem; color: var(--text-muted); }
.lib-word { font: inherit; font-size: 1.25rem; font-weight: 700; color: var(--link); background: none; border: 0; padding: 0; cursor: pointer; }
.lib-note { display: flex; gap: 6px; margin-top: 6px; }
.lib-note input { flex: 1; font: inherit; font-size: .85rem; padding: 2px 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--paper); color: var(--ink); }
.lib-note button, .lib-remove button { font: inherit; font-size: .8rem; padding: 0 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--inner); color: var(--ink); cursor: pointer; }
.lib-note input { flex: 1; font-size: var(--fs-sm); padding: 2px 10px; }
.lib-note button, .lib-remove button { font-size: var(--fs-xs); padding: 0 10px; }
.lib-remove { position: absolute; left: 8px; top: 8px; margin: 0; }
.lib-remove button { color: var(--brand); }
.lib-remove button { color: var(--danger); }
/* bookmarked phrases, and the bookmark icon where the reader's bookmarks are */
.bmk { text-decoration: underline dotted var(--brand) 2px; text-underline-offset: 6px; cursor: pointer; background: var(--gold-subtle); border-radius: 4px; }
.bmk { text-decoration: underline dotted var(--brand) 2px; text-underline-offset: 6px; cursor: pointer; background: var(--accent-soft); border-radius: 4px; }
.bmk::after { width: .7em; height: .7em; vertical-align: super; margin-inline-start: 2px; }
.mark-fav::after, .mark-bm::after { width: .85em; height: .85em; margin-inline-start: 6px; vertical-align: -.1em; }
@ -290,151 +331,141 @@ h1 + .muted { text-align: center; margin-top: 0; }
.search-group h2 { font-size: 1.1rem; text-align: center; color: var(--brand); }
.search-group h2 small { font-size: .8rem; font-weight: 400; }
.poet-result { display: flex; flex-direction: column; align-items: center; gap: 2px; }
.poet-result { width: 210px; max-width: calc(50% - 4px); } /* the wrapper takes the card's size (a card's 50% of a shrink-wrapped box was tiny) */
.poet-result .poet-card { width: 100%; max-width: none; }
.poet-result .small { font-size: .75rem; }
.book-results { list-style: none; padding: 0; max-width: 700px; margin: 0 auto; }
.book-results li { padding: 5px 4px; border-bottom: 1px dashed var(--gold-light); text-align: center; }
.book-results li { padding: 5px 4px; border-bottom: 1px dashed var(--line); text-align: center; }
.pager .pages { display: flex; gap: 4px; flex-wrap: wrap; justify-content: center; }
.pager .pages a, .pager .pages .current { min-width: 2em; text-align: center; padding: 0 8px; border: 1px solid var(--border); border-radius: 8px; text-decoration: none; }
.pager .pages .current { border-color: var(--brand); color: var(--brand); font-weight: 700; }
.pager .gap { padding: 0 2px; color: var(--muted); }
.pager .pages a, .pager .pages .current { min-width: 2.2em; padding: 0 8px; }
.pager .gap { padding: 0 2px; color: var(--text-muted); }
/* author filter on search */
.author-filter { max-width: 760px; margin: 0 auto 14px; padding: 10px 14px; border: 1px dashed var(--gold-light); border-radius: 12px; text-align: center; }
.author-filter { max-width: 760px; margin: 0 auto 14px; padding: 10px 14px; border: 1px dashed var(--line); border-radius: 12px; text-align: center; }
.author-filter p { margin: 2px 0 8px; }
.author-chips { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; }
.author-chips .chip { display: inline-flex; align-items: center; gap: 4px; padding: 1px 12px; border: 1px solid var(--border); border-radius: 999px; background: var(--inner); cursor: pointer; font: inherit; font-size: .88rem; color: var(--ink); }
.author-chips .chip small { color: var(--muted); }
.author-chips .chip input { accent-color: var(--brand); margin: 0; }
.author-chips .chip:has(input:checked) { border-color: var(--brand); color: var(--brand); font-weight: 700; }
.author-chips .chip small { color: var(--text-muted); }
.author-chips .chip input { accent-color: var(--active); margin: 0; }
.author-chips .chip.extra { display: none; }
.author-chips.all .chip.extra { display: inline-flex; }
.author-find { position: relative; display: flex; gap: 4px; max-width: 420px; margin: 10px auto 0; }
.author-find input { flex: 1; font: inherit; padding: 3px 12px; border: 1.5px solid var(--border); border-radius: 10px; background: var(--paper); color: var(--ink); }
.author-suggest { position: absolute; top: 100%; inset-inline: 0; z-index: 10; display: flex; flex-wrap: wrap; gap: 4px; padding: 6px; margin-top: 4px; background: var(--paper); border: 1px solid var(--border); border-radius: 10px; box-shadow: 0 4px 14px rgb(0 0 0 / .15); }
.author-suggest button { font: inherit; font-size: .88rem; padding: 0 10px; border: 1px solid var(--gold-light); border-radius: 999px; background: var(--inner); color: var(--ink); cursor: pointer; }
.author-suggest button:hover { border-color: var(--brand); color: var(--brand); }
.author-find input { flex: 1; }
.author-suggest { position: absolute; top: 100%; inset-inline: 0; z-index: 10; display: flex; flex-wrap: wrap; gap: 4px; padding: 6px; margin-top: 4px; background: var(--surface-raised); border: 1px solid var(--border); border-radius: 10px; box-shadow: 0 4px 14px rgb(0 0 0 / .15); }
.author-suggest button { padding: 0 10px; }
.filter-actions { display: flex; gap: 12px; justify-content: center; align-items: center; margin-top: 10px !important; }
.filter-actions button { font: inherit; font-size: .85rem; padding: 1px 14px; border: 1.5px solid var(--gold); border-radius: 10px; background: var(--inner); color: var(--ink); cursor: pointer; }
/* moderation */
.status { display: inline-block; font-size: .78rem; padding: 0 10px; border-radius: 999px; border: 1px solid var(--border); margin-inline-start: 6px; }
.s-submitted { border-color: var(--gold); color: var(--gold); }
.s-approved { border-color: var(--lapis); color: var(--lapis); }
.s-published { border-color: #3d8b4f; color: #3d8b4f; }
.s-returned, .s-rejected { border-color: var(--brand); color: var(--brand); }
.s-submitted { border-color: var(--accent); color: var(--accent); }
.s-approved { border-color: var(--link); color: var(--link); }
.s-published { border-color: var(--ok); color: var(--ok); }
.s-returned, .s-rejected { border-color: var(--danger); color: var(--danger); }
.small-line { font-size: .8rem; margin: 2px 0 0; }
.mod-edit { max-width: 900px; margin: 12px auto; display: flex; flex-direction: column; gap: 8px; }
.mod-edit label { display: flex; flex-direction: column; gap: 4px; font-size: .9rem; }
.mod-edit input, .mod-edit textarea { font: inherit; padding: 6px 12px; border: 1.5px solid var(--border); border-radius: 10px; background: var(--paper); color: var(--ink); }
.mod-edit input, .mod-edit textarea { padding: 6px 12px; }
.mod-edit textarea { font-size: 1.05rem; line-height: 2; min-height: 26em; resize: vertical; }
.mod-edit textarea:focus, .mod-edit input:focus { outline: none; border-color: var(--gold); }
.mod-actions { max-width: 700px; }
.mod-actions .act-publish { border-color: #3d8b4f; color: #3d8b4f; }
.mod-actions .act-reject, .mod-actions .act-return { border-color: var(--brand); color: var(--brand); }
.diff { max-width: 900px; margin: 0 auto; border: 1px solid var(--border); border-radius: 10px; overflow: hidden; }
.diff div { padding: 2px 12px; white-space: pre-wrap; line-height: 1.9; }
.diff div span { display: inline-block; width: 1.2em; font-weight: 700; }
.diff .add { background: rgb(61 139 79 / .14); }
.diff .del { background: rgb(191 63 42 / .12); text-decoration: line-through; text-decoration-color: rgb(191 63 42 / .5); }
.diff .add { background: color-mix(in srgb, var(--ok) 14%, transparent); }
.diff .del { background: color-mix(in srgb, var(--danger) 12%, transparent); text-decoration: line-through; text-decoration-color: color-mix(in srgb, var(--danger) 50%, transparent); }
.timeline { max-width: 760px; margin: 0 auto; padding-inline-start: 20px; font-size: .88rem; }
.timeline li { margin: 4px 0; }
.mod-h { text-align: center; color: var(--brand); }
.inline-form { display: inline; }
.mod-edit-btn, .save-like { font: inherit; font-size: .85rem; padding: 1px 14px; border: 1.5px solid var(--gold); border-radius: 999px; background: var(--inner); color: var(--ink); cursor: pointer; }
.mod-history { font-size: .85rem; margin-inline-start: 6px; }
/* moderators' guide */
.guide { max-width: 900px; margin: 0 auto; line-height: 2; }
.guide h2 { font-size: 1.15rem; color: var(--brand); text-align: start; margin-top: 28px; }
.guide code { font-family: inherit; background: var(--inner); border: 1px solid var(--gold-light); border-radius: 6px; padding: 0 6px; unicode-bidi: plaintext; display: inline-block; } /* Latin tags read left to right, Urdu right to left */
.guide code { font-family: inherit; background: var(--surface-sunken); border: 1px solid var(--line); border-radius: 6px; padding: 0 6px; unicode-bidi: plaintext; display: inline-block; } /* Latin tags read left to right, Urdu right to left */
.guide-table td:first-child { white-space: nowrap; }
.example { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; align-items: start; margin: 10px 0; }
.example pre { unicode-bidi: plaintext; margin: 0; padding: 12px 14px; background: var(--inner); border: 1px solid var(--border); border-radius: 10px; white-space: pre-wrap;
font-family: 'Noto Naskh Arabic', serif; font-size: .95rem; line-height: 1.9; }
.example pre { unicode-bidi: plaintext; margin: 0; padding: 12px 14px; background: var(--surface-sunken); border: 1px solid var(--border); border-radius: 10px; white-space: pre-wrap;
font-family: var(--font-ui); font-size: .95rem; line-height: 1.9; }
.example:has(> pre:only-child) { grid-template-columns: 1fr; }
.preview { margin: 0; padding: 12px 16px; }
.preview .couplet { font-size: 1rem; grid-template-columns: 1fr; }
.preview .couplet p, .preview .couplet p + p { text-align: center; }
.preview .couplet p, .preview .couplet p + p { justify-self: center; }
.preview .stanza { margin: 6px 0 12px; }
.preview .stanza p, .preview .single { text-align: center; margin: 0; }
.preview .mod-h.sub { font-size: 1rem; text-align: center; color: var(--lapis); }
.preview .mod-h.sub { font-size: 1rem; text-align: center; color: var(--link); }
.preview sup { color: var(--brand); font-size: .7em; }
.preview-notes { font-size: .85rem; border-top: 1px dashed var(--gold-light); margin-top: 8px; padding-top: 6px; }
.preview-notes { font-size: .85rem; border-top: 1px dashed var(--line); margin-top: 8px; padding-top: 6px; }
@media (max-width: 760px) { .example { grid-template-columns: 1fr; } }
/* the structured content editor (Tiptap) */
.editor-tabs button { font: inherit; padding: 2px 14px; border: 1.5px solid var(--border); border-radius: 999px; background: var(--inner); color: var(--ink); cursor: pointer; }
.editor-tabs button[aria-selected='true'] { border-color: var(--brand); color: var(--brand); }
.meta-fields { border: 1px dashed var(--gold-light); border-radius: 10px; padding: 6px 12px; }
.meta-fields { border: 1px dashed var(--line); border-radius: 10px; padding: 6px 12px; }
.meta-fields legend { font-size: .85rem; padding: 0 6px; }
.meta-row { display: grid; grid-template-columns: 9em 1fr auto; gap: 6px; margin: 4px 0; }
.meta-row input { font: inherit; font-size: .9rem; padding: 2px 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--paper); color: var(--ink); }
.toolbar { position: sticky; top: 0; z-index: 5; display: flex; flex-wrap: wrap; gap: 4px; padding: 6px; background: var(--paper); border: 1px solid var(--border); border-radius: 10px; }
.toolbar .sep { width: 1px; background: var(--gold-light); margin: 0 4px; }
.tool { font: inherit; font-size: .85rem; padding: 0 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--inner); color: var(--ink); cursor: pointer; }
.tool:hover, .tool[aria-pressed='true'] { border-color: var(--brand); color: var(--brand); }
.meta-row input { font-size: var(--fs-md); padding: 2px 10px; }
.toolbar { position: sticky; top: 0; z-index: 5; display: flex; flex-wrap: wrap; gap: 4px; padding: 6px; background: var(--surface-raised); border: 1px solid var(--border); border-radius: 10px; }
.toolbar .sep { width: 1px; background: var(--line); margin: 0 4px; }
.tool { padding: 0 10px; border-radius: var(--r-sm); } /* editor toolbar: squarer, denser */
.tool:has(> .icon:only-child) { min-width: 32px; height: 32px; padding: 0; } /* icon-only tools: square targets; the word is the tooltip */
.dv-editor { min-height: 20em; outline: none; margin: 8px 0; }
/* arranging (#41): one row per item, moved by dragging or the ↑ ↓ buttons */
.order-list { list-style: urdu; padding-inline-start: 2.5em; margin: 8px 0; }
.order-list li { display: flex; align-items: center; gap: 8px; padding: 2px 8px; margin: 3px 0; background: var(--paper); border: 1px solid var(--border); border-radius: 8px; cursor: grab; display: list-item; }
.order-list li { display: flex; align-items: center; gap: 8px; padding: 2px 8px; margin: 3px 0; background: var(--surface-raised); border: 1px solid var(--border); border-radius: 8px; cursor: grab; display: list-item; }
.order-list li .order-tools { float: left; display: inline-flex; gap: 2px; }
.order-list li .tool { min-width: 1.8em; padding: 0 4px; font-size: .9rem; }
.order-list li.dragging { opacity: .5; }
.order-list li.moved { border-color: var(--brand); }
.order-list li.moved { border-color: var(--active); }
/* کوڈ: Divan text beside the editor (stacked on phones) */
.editor-split.with-code { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 12px; align-items: start; }
@media (max-width: 800px) { .editor-split.with-code { grid-template-columns: minmax(0, 1fr); } }
.live-code { position: sticky; top: 56px; max-height: 80vh; overflow: auto; margin: 8px 0; padding: 10px; white-space: pre-wrap; word-break: break-word;
font-family: 'Noto Naskh Arabic', ui-monospace, monospace; font-size: .9rem; line-height: 1.9; background: var(--inner); border: 1px solid var(--border); border-radius: 10px; unicode-bidi: plaintext; }
font-family: 'Noto Naskh Arabic', ui-monospace, monospace; font-size: .9rem; line-height: 1.9; background: var(--surface-sunken); border: 1px solid var(--border); border-radius: 10px; unicode-bidi: plaintext; }
/* Tiptap's own CSS turns ligatures off, which breaks Nastaliq (and Naskh's لا); keep the font's shaping */
.ProseMirror.dv-editor { font-variant-ligatures: normal; font-feature-settings: normal; }
.dv-editor:focus-within { border-color: var(--gold); }
.dv-editor:focus-within { border-color: var(--accent); }
.dv-editor .unit { position: relative; margin: 0 0 10px; padding: 4px 8px; border-radius: 8px; border: 1px dashed transparent; }
.dv-editor .unit:hover { border-color: var(--gold-light); }
.dv-editor .unit:hover { border-color: var(--line); }
.dv-editor .unit:has(> .misra:nth-child(2):last-child) { display: grid; grid-template-columns: 1fr 1fr; column-gap: 10%; }
.dv-editor .unit .misra { margin: 0; text-align: center; min-height: 1.8em; }
.dv-editor .unit:has(> .misra:nth-child(2):last-child) .misra:first-child { text-align: left; }
.dv-editor .unit:has(> .misra:nth-child(2):last-child) .misra:last-child { text-align: right; }
.dv-editor .unit:has(> .misra:nth-child(3)) { border-inline-start: 2px solid var(--gold-light); }
.dv-editor .unit[data-mark]::before { content: attr(data-mark); grid-column: 1 / -1; justify-self: center; font-size: .72rem; color: var(--brand); border: 1px solid var(--gold-light); border-radius: 999px; padding: 0 10px; }
.dv-editor .unit:has(> .misra:nth-child(3)) { border-inline-start: 2px solid var(--line); }
.dv-editor .unit[data-mark]::before { content: attr(data-mark); grid-column: 1 / -1; justify-self: center; font-size: .72rem; color: var(--brand); border: 1px solid var(--line); border-radius: 999px; padding: 0 10px; }
.dv-editor .para { min-height: 1.8em; }
.dv-editor .dv-dict { text-decoration: underline dotted var(--lapis) 2px; text-underline-offset: 5px; }
.dv-editor .dv-dict { text-decoration: underline dotted var(--link) 2px; text-underline-offset: 5px; }
.dv-editor sup.note { color: var(--brand); cursor: pointer; padding: 0 2px; }
.dv-editor .variant { border-bottom: 2px dashed var(--gold); cursor: pointer; }
.dv-editor .is-empty::before { content: attr(data-placeholder); color: var(--muted); opacity: .6; pointer-events: none; float: inline-start; height: 0; }
.dv-editor .variant { border-bottom: 2px dashed var(--accent); cursor: pointer; }
.dv-editor .is-empty::before { content: attr(data-placeholder); color: var(--text-muted); opacity: .6; pointer-events: none; float: inline-start; height: 0; }
@media (max-width: 700px) { .dv-editor .unit:has(> .misra:nth-child(2):last-child) { grid-template-columns: 1fr; } .dv-editor .unit .misra { text-align: center !important; } .meta-row { grid-template-columns: 1fr; } }
/* home page sections editor (/mod/site) */
.site-section { border: 1px solid var(--border); border-radius: 10px; background: var(--paper); margin: 10px 0; padding: 8px 12px; }
.site-section { border: 1px solid var(--border); border-radius: 10px; background: var(--surface-raised); margin: 10px 0; padding: 8px 12px; }
.site-head { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.site-head .site-title { flex: 1 1 14em; }
.site-head input { width: 100%; font: inherit; padding: 2px 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--inner); color: var(--ink); }
.site-head input { width: 100%; padding: 2px 10px; }
.site-poets { list-style: urdu; margin: 8px 0; padding-inline-start: 2.5em; columns: 2 16em; }
.site-poets li { break-inside: avoid; }
.site-poets .tool { min-width: 1.6em; padding: 0 4px; margin-inline-start: 4px; font-size: .85rem; }
.site-section .author-find { position: relative; max-width: 26em; }
.site-section .author-find input { width: 100%; font: inherit; padding: 2px 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--inner); color: var(--ink); }
.site-section .author-find input { width: 100%; padding: 2px 10px; }
/* tags (#52) */
.tag-chips { display: flex; flex-wrap: wrap; justify-content: center; gap: 6px; margin: 6px 0; }
.tag-chip { font-size: .85rem; padding: 0 10px; border: 1px solid var(--gold-light); border-radius: 999px; background: var(--inner); color: var(--ink); text-decoration: none; }
.tag-chip small { color: var(--muted); }
.tag-chip:hover { border-color: var(--brand); color: var(--brand); }
.tag-chip { padding: 0 10px; border-color: var(--line); }
.tag-chip small { color: var(--text-muted); }
.couplet .tag-chips { grid-column: 1 / -1; justify-content: center; margin: 2px 0 0; }
.couplet .tag-chip { font-size: .75rem; }
.couplet .tag-chip { font-size: var(--fs-xs); }
.tag-row { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin: 6px 0; }
.tag-row .tag-name-box { flex: 1 1 12em; }
.tag-row .tag-name { width: 100%; font: inherit; padding: 2px 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--inner); color: var(--ink); }
.tag-row .tag-name { width: 100%; font: inherit; padding: 2px 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface-sunken); color: var(--text); }
.tag-preview { display: flex; align-items: center; justify-content: center; gap: 6px; }
/* e-books */
.ebook-frame { display: block; width: 100%; height: 82vh; border: 1px solid var(--border); border-radius: 10px; background: #fff; }
.ebook-text { white-space: pre-wrap; line-height: 2.2; max-width: 900px; margin: 0 auto; padding: 16px; background: var(--inner); border: 1px solid var(--border); border-radius: 10px; }
.ebook-text { white-space: pre-wrap; line-height: 2.2; max-width: 900px; margin: 0 auto; padding: 16px; background: var(--surface-sunken); border: 1px solid var(--border); border-radius: 10px; }
.eb-meta { display: grid; grid-template-columns: max-content 1fr; gap: 4px 16px; max-width: 700px; margin: 0 auto; }
.eb-meta dt { color: var(--muted); }
.eb-meta dt { color: var(--text-muted); }
.eb-meta dd { margin: 0; }
/* e-books: tabs, cover grid, right-to-left PDF reader */
.ebook-tabs a span[lang] { font-size: .8em; }
.ebook-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 18px; margin: 16px 0; }
.ebook-card { display: flex; flex-direction: column; gap: 6px; text-decoration: none; color: var(--ink); text-align: center; }
.ebook-card img, .ebook-plain { width: 100%; aspect-ratio: 2 / 3; object-fit: cover; border-radius: 6px; border: 1px solid var(--border); box-shadow: 0 2px 8px rgb(0 0 0 / .12); background: var(--inner); transition: transform .2s; }
.ebook-card { display: flex; flex-direction: column; gap: 6px; text-decoration: none; color: var(--text); text-align: center; }
.ebook-card img, .ebook-plain { width: 100%; aspect-ratio: 2 / 3; object-fit: cover; border-radius: 6px; border: 1px solid var(--border); box-shadow: 0 2px 8px rgb(0 0 0 / .12); background: var(--surface-sunken); transition: transform .2s; }
.ebook-card:hover img, .ebook-card:hover .ebook-plain { transform: translateY(-3px); }
.ebook-plain { display: flex; flex-direction: column; justify-content: center; gap: 8px; padding: 12px; background: linear-gradient(160deg, var(--brand), #5a1e14); color: #fff; font-family: 'Noto Nastaliq Urdu', serif; line-height: 2; }
.ebook-plain small { opacity: .8; }
@ -443,10 +474,10 @@ h1 + .muted { text-align: center; margin-top: 0; }
.cover-tools { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 8px; }
.pdf-reader { margin: 12px 0; }
.pdf-bar { display: flex; justify-content: center; align-items: center; gap: 16px; margin-bottom: 8px; }
.pdf-page { width: 4.5em; font: inherit; text-align: center; border: 1px solid var(--border); border-radius: 6px; background: var(--inner); color: var(--ink); }
.pdf-page { width: 4.5em; padding: 0 4px; text-align: center; border-radius: var(--r-sm); }
.pdf-pages { display: flex; justify-content: center; gap: 8px; min-height: 60vh; cursor: pointer; }
.pdf-pages canvas { box-shadow: 0 2px 10px rgb(0 0 0 / .2); background: #fff; }
.pdf-reader:fullscreen { background: var(--bg); padding: 10px; overflow: auto; }
.pdf-reader:fullscreen { background: var(--surface-page); padding: 10px; overflow: auto; }
.pdf-reader:fullscreen .pdf-pages { min-height: calc(100vh - 70px); align-items: center; }
.eb-row { display: flex; flex-wrap: wrap; gap: 16px; align-items: flex-end; }
.eb-dir { border: 1px solid var(--border); border-radius: 8px; padding: 2px 12px; display: flex; gap: 14px; }
@ -455,5 +486,45 @@ h1 + .muted { text-align: center; margin-top: 0; }
.eb-co .tag-chips { justify-content: flex-start; }
.eb-co .author-find { position: relative; display: block; }
.eb-co .author-find input { width: 100%; }
.eb-people { color: var(--muted); }
.eb-people { color: var(--text-muted); }
.account-form.ebook-form, .mod-edit.account-form { max-width: 560px; }
/* buttons by role (owner request): one shape, colour says what it does
.btn secondary (outline) .btn.primary main action (filled)
.btn.ok confirms (approve, publish) .btn.danger destructive (log out, delete, reject)
.icon-btn icon only, round, with a tooltip (title) and aria-label */
.btn.primary { background: var(--action); border-color: var(--action); color: var(--action-ink); }
.btn.primary:hover { filter: brightness(1.12); border-color: var(--action); color: var(--action-ink); }
.btn.ok { border-color: var(--ok); color: var(--ok); }
.btn.ok:hover { background: var(--ok); color: var(--surface-page); }
.btn.danger { border-color: var(--danger); color: var(--danger); }
.btn.danger:hover { background: var(--danger); color: #fff; }
.icon-btn { display: inline-flex; align-items: center; justify-content: center; width: 36px; height: 36px; padding: 0; border: 1.5px solid var(--border);
border-radius: 50%; background: var(--surface-sunken); color: var(--text); cursor: pointer; text-decoration: none; }
.icon-btn:hover, .icon-btn[aria-current='page'] { border-color: var(--active); color: var(--active); }
.icon-btn.danger { color: var(--danger); border-color: var(--danger); }
.icon-btn.danger:hover { background: var(--danger); color: #fff; }
.icon { flex: none; }
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
.icon-btn.small { width: 30px; height: 30px; }
/* PDF full screen: the button shows enter or exit */
.pdf-full .i-exit, .pdf-reader:fullscreen .pdf-full .i-enter { display: none; }
.pdf-reader:fullscreen .pdf-full .i-exit { display: block; }
/* display settings in the header: a button and its panel (font, palette swatches, light/dark/automatic) */
.display-menu { position: relative; display: flex; } /* no line box: lines up with the other header icons */
.display-menu > summary { list-style: none; }
.display-menu > summary::-webkit-details-marker { display: none; }
.display-menu[open] > summary { border-color: var(--active); color: var(--active); }
.display-panel { position: absolute; top: calc(100% + 8px); left: 0; z-index: 30; width: 260px; padding: 10px 14px; border: 1.5px solid var(--border);
border-radius: 14px; background: var(--surface-raised); box-shadow: 0 8px 24px rgb(0 0 0 / .18); font: 400 .9rem/1.8 var(--font-ui); color: var(--text); }
.display-panel p { margin: 6px 0 4px; color: var(--text-muted); font-size: .8rem; }
.display-panel .seg { display: flex; gap: 4px; }
.display-panel button { flex: 1; gap: 4px; padding: 2px 6px; border-radius: var(--r-md); }
.display-panel .f-nastaliq { font-family: 'Noto Nastaliq Urdu', serif; line-height: 2.2; }
.display-panel .swatches { display: grid; grid-template-columns: repeat(5, 1fr); gap: 4px; }
.display-panel .swatches button { flex-direction: column; font-size: .7rem; padding: 4px 2px; }
.display-panel .swatches span { width: 22px; height: 22px; border-radius: 50%; background: linear-gradient(135deg, var(--a) 50%, var(--b) 50%);
box-shadow: 0 0 0 1px rgb(0 0 0 / .2); }
.glyph-icon { position: relative; display: inline-flex; }
.glyph-icon > span { position: absolute; inset: 0 0 4px 2px; display: flex; align-items: center; justify-content: center;
font: 700 11px/1 var(--font-ui); }