{"id":"l-a11y-4-2","courseId":"web-accessibility","lang":"ar","kind":"lesson","summary":"حافظ على قابلية استخدام Ticketly عند تكبير 400% ومع تباعد نصي مخصّص، واضبط أحجام أهداف اللمس كما ينبغي، واحترم المستخدمين الذين يحتاجون إلى حركة أقل باستخدام prefers-reduced-motion وأزرار الإيقاف المؤقت.","html":"<p>تقرأ مها Ticketly بتكبير 400% على حاسوب محمول بشاشة 13 بوصة. عند هذا التكبير، يصبح عرض نافذة متصفّحها فعليًا 320 بكسل CSS. بطاقات المهرجان، المصفوفة في ثلاثة أعمدة ثابتة بعرض 320px، تجبرها على التمرير أفقيًا مع كل سطر نص، وهذا يعني عند ذلك التكبير أنها تفقد موضعها باستمرار. وفي الوقت نفسه يمرّر الشريط المتحرّك في الصفحة الرئيسية أسماء الفنانين إلى ما لا نهاية، فيصاب زميلها توم، الذي يعاني اضطرابًا دهليزيًا (vestibular disorder) يؤثّر في التوازن، بصداع خلال دقيقة.</p>\n<p>يتناول هذا الدرس ما يحدث حين يغيّر الناس حجم صفحتك، وحين تتحرّك صفحتك من تلقاء نفسها.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">التكبير وإعادة التدفّق</h2>\n<p>ثلاثة معايير تعمل معًا:</p>\n<ul>\n<li><strong>1.4.4 Resize Text ‏(AA):</strong> يمكن تكبير النص إلى 200% دون فقدان محتوى أو وظيفة.</li>\n<li><strong>1.4.10 Reflow ‏(AA):</strong> عند عرض <strong>320 بكسل CSS</strong>، يتّسع المحتوى دون تمرير أفقي. (نافذة بعرض 1280px عند تكبير 400% عرضها 320 بكسل CSS.) جداول البيانات والخرائط والمخطّطات مستثناة، لأنها تحتاج إلى بُعدين.</li>\n<li><strong>1.4.12 Text Spacing ‏(AA):</strong> لا ينكسر شيء حين يفرض المستخدمون ارتفاع سطر قدره 1.5، والمسافة بين الفقرات إلى 2em، والمسافة بين الحروف إلى 0.12em، والمسافة بين الكلمات إلى 0.16em، كما يفعل كثير من ذوي عُسر القراءة عبر إضافة للمتصفّح.</li>\n</ul>\n<p>إن كنت تبني بأسلوب متجاوب، فقد قطعت معظم الطريق. والمتّهمان المعتادان هما العروض الثابتة والارتفاعات الثابتة. شبكة البطاقات في Ticketly:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">before.css</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.cards</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  display</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">grid</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  grid-template-columns</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">repeat</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">3</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">320</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  gap</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">16</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.card</span><span style=\"color:#116329;--shiki-dark:#7EE787\"> h3</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  height</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">24</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;      </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">/* clips the title when text spacing grows */</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  font-size</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">18</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;   </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">/* doesn't follow the user's default font size */</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">after.css</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.cards</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  display</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">grid</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  grid-template-columns</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">repeat</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">auto-fit</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">minmax</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">min</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">100</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">%</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">16</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">rem</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">), </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">fr</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">));</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  gap</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">rem</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.card</span><span style=\"color:#116329;--shiki-dark:#7EE787\"> h3</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  font-size</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1.125</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">rem</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>الجزء <code dir=\"ltr\">min(100%, 16rem)</code> مهم: في viewport عرضه 320px قد يظلّ عمود بعرض 16rem ‏(256px يُضاف إليها الحشو) يفيض، و<code dir=\"ltr\">min()</code> تسمح للعمود بالانكماش إلى عرض الحاوية. وضبط النص بوحدة <code dir=\"ltr\">rem</code> يعني أنه يكبر حين يضبط المستخدم حجم خط افتراضيًا أكبر في متصفّحه، وهي حالة لا يغطّيها التكبير وحده.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">خطأ شائع</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>تعطيل التكبير باللمس</span></p>\n<p>نُسخ <code dir=\"ltr\">&lt;meta name=&quot;viewport&quot; content=&quot;…, maximum-scale=1, user-scalable=no&quot;&gt;</code> إلى مواقع لا تُحصى لمنع «التكبير غير المقصود». لكنه يمنع التكبير المقصود أيضًا، وهو الطريقة التي يستخدم بها ضعاف البصر الهاتف. استخدم <code dir=\"ltr\">width=device-width, initial-scale=1</code> ولا شيء غيرهما.</p>\n</aside>\n<p>لاختبار إعادة التدفّق، اضبط نافذة المتصفّح على عرض 1280px وكبّر إلى 400%، أو استخدم الوضع المتجاوب في أدوات المطوّر بعرض 320px. مرّر عموديًا عبر الصفحة كلها. أي شريط تمرير أفقي، أو نص متداخل، أو زر مقطوع، خطأ.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">حجم الهدف من جديد</h2>\n<p>قابلت معيار 2.5.8 Target Size (Minimum) في درس <a href=\"/ar/lesson/web-accessibility/l-a11y-1-3\">WCAG 2.2 ومبادئ POUR والقانون</a>: أهداف المؤشّر لا تقلّ عن 24 × 24 بكسل CSS، أو تكون حول الأهداف الأصغر مسافة كافية. كان عدّاد الكمية في Ticketly يستخدم أزرار أيقونات بحجم 16px متلاصقة. والإصلاح بالحشو، لا بتكبير الأيقونات:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">CSS</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.stepper</span><span style=\"color:#116329;--shiki-dark:#7EE787\"> button</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  min-inline-size</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2.75</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">rem</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">/* 44px, comfortably above the 24px minimum */</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  min-block-size</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2.75</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">rem</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>واطمح إلى ما هو أعلى من الحدّ الأدنى للأفعال الرئيسية على شاشات اللمس. إرشادات منصّتي Apple وGoogle تقترح نحو 44 إلى 48 بكسل، وسيشكرك المصابون بالرعاش.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">الحركة: أوقفها مؤقتًا، واحترم التفضيل</h2>\n<p>الحركة تسبّب مشكلتين مختلفتين. المحتوى الذي يتحرّك من تلقاء نفسه ينافس على الانتباه، وهذا عائق حقيقي لذوي اضطرابات الانتباه ولأي شخص يقرأ ببطء. والحركة الكبيرة، مثل الـ parallax أو الانتقالات المكبِّرة أو اللوحات المنزلقة، قد تثير الدوار والغثيان لدى المصابين باضطرابات دهليزية.</p>\n<p>تعالج WCAG المشكلة الأولى بمعيار <strong>2.2.2 Pause, Stop, Hide ‏(A)</strong>: كل ما يبدأ تلقائيًا، ويتحرّك (أو يومض أو يتمرّر) أكثر من خمس ثوانٍ، ويظهر إلى جانب محتوى آخر، يحتاج إلى طريقة لإيقافه مؤقتًا أو إيقافه أو إخفائه. ويمنع معيار <strong>2.3.1 Three Flashes or Below Threshold ‏(A)</strong> المحتوى الذي يومض أكثر من ثلاث مرات في أي ثانية واحدة، إلا إن كانت الومضات صغيرة وخافتة بما يكفي لتبقى دون عتبات الوميض، لأن الوميض قد يثير نوبات صرع.</p>\n<p>وللمشكلة الثانية، تتيح أنظمة التشغيل للمستخدمين طلب تقليل الحركة (في macOS: ‏Accessibility ثم Display ثم Reduce motion؛ وفي Windows: ‏Accessibility ثم Visual effects ثم إيقاف Animation effects)، ويستطيع CSS قراءة هذا الإعداد:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">ticketly/motion.css</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.marquee__track</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  animation</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: scroll-names </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">20</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">s</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> linear</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> infinite</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.marquee--paused</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> .marquee__track</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  animation-play-state</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">paused</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">@media</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (prefers-reduced-motion: reduce) {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  .marquee__track</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">    animation</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">none</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  .card</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">    transition</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: opacity </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.2</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">s</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">/* a fade instead of a slide */</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"html\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">HTML</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> type</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"button\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> class</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"marquee-toggle\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> aria-pressed</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"false\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Pause animation&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span></code></pre></div>\n<p>زر الإيقاف المؤقت ما زال ضروريًا: كثيرون ممّن تزعجهم الحركة لا يجدون إعداد النظام أبدًا، ومعيار 2.2.2 يشترط عنصر تحكّم في الصفحة. و«تقليل» الحركة لا يعني بالضرورة «انعدامها». التلاشي القصير أو تغيّر اللون نادرًا ما يسبّب مشكلة؛ الحركة الكبيرة عبر الشاشة هي التي تسبّبها.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 190\" role=\"img\" aria-labelledby=\"l-a11y-4-2-t1\"><defs><marker id=\"arrow-l-a11y-4-2-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-a11y-4-2-t1\">يمرّ إعداد تقليل الحركة في نظام التشغيل عبر الـ media query ‏prefers-reduced-motion ليحذف الحركات الكبيرة افتراضيًا، بينما يتيح زر إيقاف مؤقت في الصفحة لأي شخص إيقاف الشريط المتحرّك؛ ومعًا يغطّيان من ضبطوا التفضيل ومن لم يضبطوه.</title>\n  <rect class=\"d-box\" x=\"20\" y=\"30\" width=\"190\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"115\" y=\"63\" text-anchor=\"middle\">النظام: تقليل الحركة</text>\n  <path class=\"d-arrow\" d=\"M210 58 L270 58\" marker-end=\"url(#arrow-l-a11y-4-2-1)\"/>\n  <rect class=\"d-box-primary\" x=\"275\" y=\"30\" width=\"200\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"375\" y=\"63\" text-anchor=\"middle\">prefers-reduced-motion</text>\n  <rect class=\"d-box\" x=\"20\" y=\"110\" width=\"190\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"115\" y=\"143\" text-anchor=\"middle\">زر الإيقاف المؤقت</text>\n  <path class=\"d-arrow\" d=\"M210 138 L270 138\" marker-end=\"url(#arrow-l-a11y-4-2-1)\"/>\n  <rect class=\"d-box-primary\" x=\"275\" y=\"110\" width=\"200\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"375\" y=\"143\" text-anchor=\"middle\">animation-play-state</text>\n  <path class=\"d-arrow\" d=\"M475 58 L530 95\" marker-end=\"url(#arrow-l-a11y-4-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M475 138 L530 105\" marker-end=\"url(#arrow-l-a11y-4-2-1)\"/>\n  <rect class=\"d-box-success\" x=\"535\" y=\"72\" width=\"130\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"600\" y=\"105\" text-anchor=\"middle\">صفحة هادئة</text>\n</svg><figcaption>طبقتان للتحكّم في الحركة</figcaption></figure>\n<p>الحركات التي تديرها JavaScript تحتاج إلى الفحص نفسه. تخبرك <code dir=\"ltr\">window.matchMedia('(prefers-reduced-motion: reduce)').matches</code> بالتفضيل الحالي، ويمكنك الاستماع إلى حدث <code dir=\"ltr\">change</code> الخاص بها لتستجيب إن غيّره المستخدم والصفحة مفتوحة.</p>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">نصيحة</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>الفيديو والوسائط ذاتية التشغيل</span></p>\n<p>فيديوهات الخلفية تُحتسب محتوى متحرّكًا، وأي صوت يُشغَّل تلقائيًا لأكثر من ثلاث ثوانٍ يحتاج إلى عنصر تحكّم للإيقاف المؤقت أو لمستوى الصوت (1.4.2 Audio Control). وأبسط تصميم متوافق هو ألا تشغّل شيئًا تلقائيًا أصلًا.</p>\n</aside>\n<p>صار Ticketly متماسكًا حين يكبّره الناس، أو يباعدون نصّه، أو ينقرونه بيد مرتعشة، أو يطلبون منه أن يثبت في مكانه. ويعود الدرس التالي إلى شجرة إمكانية الوصول وينظر عن قرب في كيف يحصل كل عنصر تحكّم على الاسم الذي ينطقه قارئ الشاشة.</p>\n","toc":[{"id":"sec-1","text":"التكبير وإعادة التدفّق","level":2},{"id":"sec-2","text":"حجم الهدف من جديد","level":2},{"id":"sec-3","text":"الحركة: أوقفها مؤقتًا، واحترم التفضيل","level":2}],"takeaways":["يجب أن يُعاد تدفّق المحتوى داخل viewport عرضه 320 بكسل CSS، وهو عرض نافذة بعرض 1280px عند تكبير 400%، دون تمرير أفقي.","اضبط أحجام النصوص بوحدة rem، وتجنّب الارتفاعات الثابتة لحاويات النص، ولا تعطّل أبدًا التكبير باللمس (pinch zoom) في وسم viewport.","كل ما يتحرّك من تلقاء نفسه لأكثر من خمس ثوانٍ يحتاج إلى طريقة لإيقافه مؤقتًا أو إيقافه أو إخفائه (2.2.2).","ضع الحركة غير الضرورية داخل فحص prefers-reduced-motion، واستبدل الحركة الكبيرة بتلاشٍ أو بلا شيء."],"further":[{"title":"W3C: Understanding 1.4.10 Reflow","url":"https://www.w3.org/WAI/WCAG22/Understanding/reflow.html"},{"title":"MDN: prefers-reduced-motion","url":"https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion"},{"title":"W3C: Understanding 2.2.2 Pause, Stop, Hide","url":"https://www.w3.org/WAI/WCAG22/Understanding/pause-stop-hide.html"},{"title":"W3C: Understanding 1.4.12 Text Spacing","url":"https://www.w3.org/WAI/WCAG22/Understanding/text-spacing.html"}],"quiz":[{"q":"<p>لماذا يُختبر معيار 1.4.10 Reflow عند عرض 320 بكسل CSS؟</p>","options":[{"text":"لأنه عرض أصغر هاتف ما زال يُباع","why":"الهواتف فائدة جانبية. الرقم مصدره التكبير على أجهزة سطح المكتب، لا قائمة بالأجهزة."},{"text":"لأن نافذة متصفّح بعرض 1280px مكبّرة إلى 400% يكون الـ viewport فيها 320 بكسل CSS","why":"صحيح. ضعيف البصر الذي يستخدم تكبير 400% على حاسوب محمول يحصل على التخطيط نفسه الذي يحصل عليه هاتف ضيّق، فالتخطيط المتجاوب يخدم الاثنين."},{"text":"لأنه العرض الأدنى الافتراضي في CSS grid","why":"ليس في CSS grid قيمة افتراضية كهذه. القيمة مصدرها حساب التكبير في وثيقة Understanding."}],"answer":1},{"q":"<p>أيّ وسم viewport يخالف معيار WCAG ‏1.4.4 Resize Text؟</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"html\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">HTML</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">meta</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> name</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"viewport\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> content</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span></code></pre></div>\n","options":[{"text":"ينجح، لأنه يضبط مقياس البداية بشكل صحيح","why":"القيمتان الأوليان سليمتان. أما الأخيرتان فتمنعان الناس من التكبير باللمس على الهاتف، وهذا بالضبط ما يعتمد عليه ضعاف البصر."},{"text":"يفشل على متصفّحات سطح المكتب فقط","why":"متصفّحات سطح المكتب تتجاهل وسم viewport. مستخدمو الهواتف هم من يفقدون القدرة على التكبير."},{"text":"يفشل، لأن maximum-scale=1 وuser-scalable=no تمنعان التكبير باللمس","why":"صحيح. احذفهما كلتيهما. بعض متصفّحات الهاتف تتجاوزهما لأسباب تتعلّق بإمكانية الوصول، لكن لا يمكنك الاعتماد على ذلك."}],"answer":2},{"q":"<p>يمرّر الشريط الرئيسي في Ticketly أسماء الفنانين تلقائيًا في شريط متحرّك (marquee) إلى ما لا نهاية. ماذا تشترط WCAG 2.2 في المستوى A؟</p>","options":[{"text":"عنصر تحكّم مرئيًا لإيقافه مؤقتًا أو إيقافه أو إخفائه، لأنه يتحرّك أكثر من خمس ثوانٍ","why":"صحيح. ينطبق معيار 2.2.2 Pause, Stop, Hide على المحتوى المتحرّك الذي يبدأ تلقائيًا ويستمر أكثر من خمس ثوانٍ ويقع إلى جانب محتوى آخر."},{"text":"لا شيء، ما دام يتوقّف حين يكون prefers-reduced-motion مضبوطًا","why":"احترام هذا التفضيل ممارسة جيدة، لكن كثيرين ممّن تشتّتهم الحركة لا يضبطونه أبدًا. ومعيار 2.2.2 يشترط عنصر تحكّم في الصفحة."},{"text":"live region عبر aria-live لتُعلن قارئات الشاشة كل اسم جديد","why":"هذا سيُغرق مستخدمي قارئ الشاشة بالإعلانات. المشكلة في الحركة نفسها، لا في غياب الإعلانات."}],"answer":0},{"q":"<p>أيّ CSS يحترم إعداد تقليل الحركة لدى المستخدم في البطاقات المنزلقة في Ticketly؟</p>","options":[{"text":"<code dir=\"ltr\">@media (prefers-reduced-motion: no-preference) { .card { transition: none; } }</code>","why":"هذا يحذف الحركة عن المستخدمين الذين لم يطلبوا حركة أقل، ويُبقيها لمن طلبوا ذلك. الشرط معكوس."},{"text":"<code dir=\"ltr\">.card { transition: transform 0.4s; } @media (prefers-reduced-motion: reduce) { .card { transition: opacity 0.2s; } }</code>","why":"صحيح. الوضع الافتراضي يُبقي الانزلاق، والـ media query تستبدله بتلاشٍ قصير حين يطلب المستخدم تقليل الحركة."},{"text":"<code dir=\"ltr\">@media (motion: reduce) { .card { transition: none; } }</code>","why":"لا توجد media feature اسمها motion. اسمها prefers-reduced-motion."},{"text":"<code dir=\"ltr\">.card[aria-reduced-motion] { transition: none; }</code>","why":"لا توجد خاصية ARIA كهذه. يضبط المستخدمون التفضيل في نظام التشغيل، ويقرؤه CSS عبر prefers-reduced-motion."}],"answer":1}],"exercise":{"kind":"web","mode":"dom","starter":{"html":"<section class=\"marquee\" aria-label=\"Artists\">\n  <div class=\"marquee__track\">Nour Quartet · The Saltmarsh Band · Layla &amp; the Tides · Harbour Brass · DJ Marwa</div>\n</section>\n\n<div class=\"cards\">\n  <article class=\"card\">\n    <h3>Friday pass</h3>\n    <p>SAR 180</p>\n    <div class=\"stepper\">\n      <button type=\"button\" aria-label=\"Fewer Friday passes\">−</button>\n      <span>1</span>\n      <button type=\"button\" aria-label=\"More Friday passes\">+</button>\n    </div>\n  </article>\n  <article class=\"card\"><h3>Saturday pass</h3><p>SAR 180</p></article>\n  <article class=\"card\"><h3>Weekend pass</h3><p>SAR 320</p></article>\n</div>","css":".marquee { overflow: hidden; background: #f1eefb; padding: 8px 0; margin-block-end: 16px; }\n.marquee__track { white-space: nowrap; animation: scroll-names 12s linear infinite; }\n@keyframes scroll-names { from { transform: translateX(100%); } to { transform: translateX(-100%); } }\n\n.cards { display: grid; grid-template-columns: repeat(3, 320px); gap: 16px; }\n.card { border: 1px solid #c9c4dc; border-radius: 12px; padding: 12px 16px; }\n.card h3 { margin: 0; font-size: 18px; }\n.stepper { display: flex; align-items: center; gap: 0; }\n.stepper button { width: 16px; height: 16px; padding: 0; font-size: 12px; line-height: 1; border: 1px solid #5b3cc4; background: #fff; }\n.stepper span { padding-inline: 8px; }\n","js":""},"solution":{"html":"<section class=\"marquee\" aria-label=\"Artists\">\n  <button type=\"button\" class=\"marquee-toggle\" aria-pressed=\"false\">Pause animation</button>\n  <div class=\"marquee__track\">Nour Quartet · The Saltmarsh Band · Layla &amp; the Tides · Harbour Brass · DJ Marwa</div>\n</section>\n\n<div class=\"cards\">\n  <article class=\"card\">\n    <h3>Friday pass</h3>\n    <p>SAR 180</p>\n    <div class=\"stepper\">\n      <button type=\"button\" aria-label=\"Fewer Friday passes\">−</button>\n      <span>1</span>\n      <button type=\"button\" aria-label=\"More Friday passes\">+</button>\n    </div>\n  </article>\n  <article class=\"card\"><h3>Saturday pass</h3><p>SAR 180</p></article>\n  <article class=\"card\"><h3>Weekend pass</h3><p>SAR 320</p></article>\n</div>","css":".marquee { overflow: hidden; background: #f1eefb; padding: 8px 0; margin-block-end: 16px; }\n.marquee__track { white-space: nowrap; animation: scroll-names 12s linear infinite; }\n.marquee--paused .marquee__track { animation-play-state: paused; }\n@keyframes scroll-names { from { transform: translateX(100%); } to { transform: translateX(-100%); } }\n\n@media (prefers-reduced-motion: reduce) {\n  .marquee__track { animation: none; }\n}\n\n.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr)); gap: 1rem; }\n.card { border: 1px solid #c9c4dc; border-radius: 12px; padding: 12px 16px; }\n.card h3 { margin: 0; font-size: 1.125rem; }\n.stepper { display: flex; align-items: center; gap: 4px; }\n.stepper button { min-inline-size: 2.75rem; min-block-size: 2.75rem; padding: 0; font-size: 1.25rem; line-height: 1; border: 1px solid #5b3cc4; background: #fff; }\n.stepper span { padding-inline: 8px; }\n","js":"const marquee = document.querySelector('.marquee');\nconst toggle = document.querySelector('.marquee-toggle');\n\ntoggle.addEventListener('click', () => {\n  const paused = toggle.getAttribute('aria-pressed') === 'true';\n  toggle.setAttribute('aria-pressed', String(!paused));\n  marquee.classList.toggle('marquee--paused', !paused);\n});\n"},"checks":[{"id":"reduced-motion","test":"const track = $('.marquee__track'); const rules = []; const walk = (list) => { for (const r of list) { if (r.media && r.conditionText && /prefers-reduced-motion\\s*:\\s*reduce/.test(r.conditionText)) rules.push(...r.cssRules); else if (r.cssRules) walk(r.cssRules); } }; for (const s of document.styleSheets) walk(s.cssRules); return rules.some((r) => r.selectorText && track.matches(r.selectorText) && (r.style.animationName === 'none' || /(^|\\s)none(\\s|$)/.test(r.style.animation) || r.style.animationPlayState === 'paused' || /^0(s|ms)?$/.test(r.style.animationDuration) || r.style.animationIterationCount === '1'));"},{"id":"pause-control","test":"const btn = $$('button').find((b) => /pause|stop/i.test(b.textContent + ' ' + (b.getAttribute('aria-label') || ''))); if (!btn) return false; const track = $('.marquee__track'); if (getComputedStyle(track).animationPlayState === 'paused') return false; await click(btn); const cs = getComputedStyle(track); return cs.animationPlayState === 'paused' || cs.animationName === 'none';"},{"id":"reflow","test":"const doc = document.documentElement; const cards = $('.cards'); return doc.scrollWidth <= doc.clientWidth && cards.scrollWidth <= cards.clientWidth + 1;"},{"id":"target-size","test":"const bs = $$('.stepper button'); return bs.length === 2 && bs.every((b) => { const r = b.getBoundingClientRect(); return r.width >= 24 && r.height >= 24; });"}],"title":"هدّئ الصفحة الرئيسية وامنحها مرونة","prompt":"<p>تمرّر الصفحة الرئيسية في Ticketly أسماء الفنانين إلى ما لا نهاية، وتفرض شبكة من ثلاثة أعمدة لبطاقات ثابتة العرض 320px، وتستخدم أزرار عدّاد بحجم 16px. أصلحها:</p>\n<ol>\n<li>أضِف كتلة <code dir=\"ltr\">@media (prefers-reduced-motion: reduce)</code> توقف حركة <code dir=\"ltr\">.marquee__track</code>.</li>\n<li>أضِف <strong>زرًّا</strong> يحتوي نصه “Pause” يوقف الشريط المتحرّك مؤقتًا عند نقره (مثلًا باستخدام <code dir=\"ltr\">animation-play-state: paused</code>).</li>\n<li>اجعل شبكة <code dir=\"ltr\">.cards</code> يُعاد تدفّقها فلا يتمرّر شيء أفقيًا في viewport ضيّق (عرض المعاينة نحو 300px فقط).</li>\n<li>اجعل زرّي <code dir=\"ltr\">.stepper</code> كليهما لا يقلّان عن 24 × 24 بكسل CSS.</li>\n</ol>\n","hints":["داخل الـ media query: ‎.marquee__track { animation: none; }","بدّل كلاسًا مثل ‎.marquee–paused على القسم عند نقر الزر، واضبط animation-play-state: paused على المسار داخله. وaria-pressed تجعله زر تبديل حقيقيًا.","grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr)) تسمح للشبكة بالنزول إلى عمود واحد. ولزرّي العدّاد، min-inline-size وmin-block-size بقيمة 2.75rem تعطيان 44px مريحة."],"explanation":"<p>تحذف الـ media query حركة الشريط عن كل من طلب من نظام تشغيله تقليل الحركة، ويغطّي زر الإيقاف المؤقت كل من سواهم، وهذا ما يشترطه معيار 2.2.2 Pause, Stop, Hide. وتسمح <code dir=\"ltr\">auto-fit</code> مع <code dir=\"ltr\">minmax(min(100%, 16rem), 1fr)</code> للشبكة بالعودة إلى عمود واحد، فلا يحتاج تكبير 400% (أو هاتف) إلى تمرير أفقي أبدًا. ويكبر زرّا العدّاد إلى 44px، أعلى بكثير من حدّ 24px في معيار 2.5.8، دون تكبير الأيقونات نفسها.</p>\n","checkLabels":{"reduced-motion":"كتلة prefers-reduced-motion: reduce توقف حركة الشريط المتحرّك","pause-control":"زر Pause يوقف الشريط المتحرّك مؤقتًا","reflow":"لا تتمرّر الصفحة ولا شبكة البطاقات أفقيًا","target-size":"زرّا العدّاد كلاهما لا يقلّان عن 24 × 24 بكسل"}},"words":724,"readMinutes":4}