{"id":"l-css-4-1","courseId":"css-mastery","lang":"ar","kind":"lesson","summary":"أضف transitions وحركات keyframes تبدو مقصودة، وحرّك العناصر عند ظهورها من display: none باستخدام @starting-style، واحترم prefers-reduced-motion دون أن تكسر الواجهة.","html":"<p>يطلب موجز التصميم في Waypoint ثلاث لمسات من الحركة: بطاقات الجلسات ترتفع قليلًا عند المرور فوقها، والجلسة الجارية الآن لها نقطة «Live» نابضة، وإشعار منبثق (toast) ينزلق إلى الداخل حين تحفظ جلسة. الثلاث صغيرة. والثلاث يمكن أن تسوء بطرق تجعل الصفحة تبدو رخيصة، أو تُشعر بعض الناس بتعب جسدي حقيقي.</p>\n<p>يغطّي هذا الدرس الأدوات (الـ transitions والـ keyframes و<code dir=\"ltr\">@starting-style</code>) والمسؤولية (<code dir=\"ltr\">prefers-reduced-motion</code>).</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">الـ transitions: بين حالتين</h2>\n<p>الـ transition (الانتقال) يحرّك خاصية حين تتغيّر قيمتها المحسوبة، لأيّ سبب: مرور المؤشّر، أو تبديل class، أو media query. أنت تختار الخصائص والمدة ومنحنى التسارع (easing):</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">session-card.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\">.session-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\">:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    translate </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">200</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ms</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> ease-out</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    box-shadow </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">200</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ms</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> ease-out</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  &#x26;</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">:hover</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  &#x26;</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">:focus-within</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">    translate</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> -2</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\">    box-shadow</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 6</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 16</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> rgb</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">15</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 23</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 42</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> / </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.12</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:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>ثلاثة اختيارات تؤدّي العمل هنا. <strong>خصائص محدّدة</strong>: ترتفع البطاقة ويكبر ظلّها، ولا يتحرّك شيء آخر. <strong>مدة قصيرة</strong>: من 150 إلى 250ms تبدو مستجيبة لتغييرات الواجهة الصغيرة؛ وما يزيد على 400ms يبدو متثاقلًا حين يكون المستخدم في انتظاره. <strong>التسارع</strong>: <code dir=\"ltr\">ease-out</code> تبدأ سريعة ثم تستقرّ، وهذا يناسب ما يستجيب للمستخدم.</p>\n<p>وخصائص التحويل المنفردة (<code dir=\"ltr\">translate</code> و<code dir=\"ltr\">scale</code> و<code dir=\"ltr\">rotate</code>) تستحق أن تفضّلها على <code dir=\"ltr\">transform</code>: يمكن تحريك كلٍّ منها وتجاوزه باستقلال، فلا يمسح ارتفاع المرور قيمة scale ضُبطت في مكان آخر.</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>transition: all</span></p>\n<p><code dir=\"ltr\">transition: all 300ms</code> تحرّك كل خاصية تتغيّر، بما فيها خصائص لم تفكّر فيها قطّ. انتقل إلى الوضع الداكن فيتلاشى كل لون في البطاقة ببطء؛ وغيّر الـ padding عند breakpoint فتتمطّط البطاقة أمام العين. اذكر الخصائص التي تقصدها. كتابة أكثر بقليل، ومفاجآت أقل بكثير.</p>\n</aside>\n<h2 id=\"sec-2\" tabindex=\"-1\">الـ keyframes: حركة متعدّدة الخطوات ومتكرّرة</h2>\n<p>حين يكون للحركة أكثر من خطوتين، أو تتكرّر من تلقاء نفسها، تحتاج إلى <code dir=\"ltr\">@keyframes</code>:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"1\"><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:#CF222E;--shiki-dark:#FF7B72\">@keyframes</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> pulse</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  0%</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">   { </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">scale</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;   </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">opacity</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; }</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  70%</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  { </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">scale</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2.2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">opacity</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; }</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  100%</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">scale</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2.2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">opacity</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</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:#0550AE;--shiki-dark:#79C0FF\">.live-dot::after</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  content</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  position</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">absolute</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  inset</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  border-radius</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">50</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">%</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  background</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">currentColor</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\">: pulse </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1.6</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">s</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> ease-out</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></code></pre></div>\n<p>هالة النقطة تكبر وتتلاشى، ثم تتوقّف برهة (الثبات من 70% إلى 100%) قبل أن تتكرّر. وتحريك <code dir=\"ltr\">scale</code> و<code dir=\"ltr\">opacity</code> مقصود: هاتان الخاصيتان تستطيعان العمل على الـ compositor دون إعادة تشغيل التخطيط، وهذا ما يشرحه آخر درس في هذه الدورة.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">حركات الدخول بـ @starting-style</h2>\n<p>الإشعار المنبثق للحفظ يكون <code dir=\"ltr\">display: none</code> إلى أن يُظهره class. لا تستطيع الـ transitions أن تعمل عند ظهوره الأول، لأن العنصر المرسوم حديثًا ليس له نمط سابق يبدأ منه. و<code dir=\"ltr\">@starting-style</code> توفّر هذا النمط:</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\">.toast</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  opacity</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  translate</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0</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\">:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    opacity </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">200</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ms</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> ease-out</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    translate </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">200</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ms</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> ease-out</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    display </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">200</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ms</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> allow-discrete;</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\">@starting-style</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  .toast</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">    opacity</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">    translate</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 8</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:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.toast</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">hidden</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\">none</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  opacity</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</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\">hidden</code>، يبدأ المتصفّح الإشعار من قيم <code dir=\"ltr\">@starting-style</code> وينتقل إلى القيم العادية. و<code dir=\"ltr\">transition-behavior: allow-discrete</code> (المكتوبة هنا <code dir=\"ltr\">allow-discrete</code> في الاختصار) تتيح لـ <code dir=\"ltr\">display</code> أن تنقلب في اللحظة المناسبة عند الخروج، فيكون التلاشي ظاهرًا قبل أن يختفي العنصر. <code dir=\"ltr\">@starting-style</code> و<code dir=\"ltr\">transition-behavior</code> ضمن Baseline 2024، لذا تعمل حركة الدخول في كل المتصفّحات الحالية. أما تحريك <code dir=\"ltr\">display</code> نفسها عند الخروج فليس ضمن Baseline بعد (يدعمه Chrome وSafari دون Firefox)، فيختفي الإشعار هناك فورًا بدل أن يتلاشى؛ وفي المتصفّحات الأقدم يظهر ببساطة دون حركة. وكلاهما بديل احتياطي جيد تمامًا.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">احترم تقليل الحركة</h2>\n<p>بعض الناس يصابون بالدوار أو الغثيان أو التشتّت من الحركة على الشاشة، خصوصًا الحركة عبر مسافات طويلة، والـ parallax، والتكبير والتصغير. لأنظمة التشغيل إعداد «تقليل الحركة»، وتقرؤه CSS بـ <code dir=\"ltr\">prefers-reduced-motion</code>.</p>\n<p>النمط الأمتن يجعل الحركة اختيارية:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"3\"><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:#CF222E;--shiki-dark:#FF7B72\">@media</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (prefers-reduced-motion: no-preference) {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  .session-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\">: translate </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">200</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ms</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> ease-out</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, box-shadow </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">200</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ms</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> ease-out</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\">  .live-dot::after</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\">: pulse </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1.6</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">s</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> ease-out</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:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>المستخدمون الذين لم يطلبوا تقليل الحركة يحصلون على كل شيء. ومن طلبوه يحصلون على بطاقة ثابتة ونقطة ثابتة، وتظل الواجهة تعمل، لأن أيّ معلومة لا تعتمد على الحركة.</p>\n<aside class=\"callout callout-why\" 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\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/></svg><span><span class=\"callout-kind\">لماذا يهمّ هذا</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>لماذا التقليل لا الإزالة</span></p>\n<p>تقليل الحركة لا يعني غياب أيّ استجابة بصرية. تغيّر الحالة الذي ينزلق 200px عبر الشاشة يمكن أن يصبح تلاشيًا سريعًا؛ وارتفاع المرور يمكن أن يصبح تغيّرًا في لون الحدّ. احتفظ بالمعنى، وتخلَّ عن الحركة. ولا تجعل أيّ معلومة تعتمد على الحركة وحدها أبدًا: نقطة Live بجوارها النص «Live now» أيضًا.</p>\n</aside>\n<p>وسترى أيضًا شبكة أمان عامة تضبط مدة كل حركة وكل transition على ما يقارب الصفر داخل <code dir=\"ltr\">@media (prefers-reduced-motion: reduce)</code>. إنها حاجز احتياطي معقول للأدوات الخارجية (widgets)، لكنها كاستراتيجية وحيدة فظّة: قد تربك السكربتات التي تعتمد على توقيت الحركة (وهي تستخدم مدة صغيرة غير صفرية تحديدًا لأن transition بمدة <code dir=\"ltr\">0s</code> لا يُطلق <code dir=\"ltr\">transitionend</code> أبدًا)، وتزيل التلاشيات غير المؤذية مع الحركة المؤذية.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">دورك الآن</h2>\n<p>بطاقة التمرين فيها مرور فوري، ونقطة ثابتة، ولا تعامل مع تقليل الحركة. أضف transition للمرور على خصائص محدّدة، ونبضًا متكرّرًا على النقطة، وغلّف الحركة الزخرفية لتعمل فقط عند المستخدمين الذين لم يطلبوا تقليلها. وبعدها ستحرّك الانتقال بين حالات الصفحة كاملة باستخدام الـ view transitions.</p>\n","toc":[{"id":"sec-1","text":"الـ transitions: بين حالتين","level":2},{"id":"sec-2","text":"الـ keyframes: حركة متعدّدة الخطوات ومتكرّرة","level":2},{"id":"sec-3","text":"حركات الدخول بـ @starting-style","level":2},{"id":"sec-4","text":"احترم تقليل الحركة","level":2},{"id":"sec-5","text":"دورك الآن","level":2}],"takeaways":["اذكر الخصائص التي تحرّكها بالتحديد (<code dir=\"ltr\">transition: translate 200ms ease-out</code>) بدل <code dir=\"ltr\">all</code>، حتى لا تتحرّك تغييرات الثيم وخصائص التخطيط بالصدفة.","استخدم الـ transitions للتغيّر بين حالتين، و<code dir=\"ltr\">@keyframes</code> للحركة متعدّدة الخطوات أو المتكرّرة.","<code dir=\"ltr\">@starting-style</code> تحدّد القيم التي ينطلق منها العنصر في الـ transition حين يظهر أول مرة، ما يجعل حركات الدخول من <code dir=\"ltr\">display: none</code> ممكنة في CSS.","غلّف الحركة غير الضرورية في <code dir=\"ltr\">@media (prefers-reduced-motion: no-preference)</code>، فيحصل من طلبوا حركة أقل على واجهة هادئة افتراضيًا."],"further":[{"title":"Using CSS transitions (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Transitions/Using"},{"title":"@starting-style (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@starting-style"},{"title":"prefers-reduced-motion (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@media/prefers-reduced-motion"}],"quiz":[{"q":"<p>لبطاقة <code dir=\"ltr\">transition: all 300ms</code>. حين يتحوّل المستخدم إلى الوضع الداكن، تتلاشى ألوان الصفحة كلها ببطء. ما أنظف إصلاح؟</p>","options":[{"text":"أزل الـ transition في الوضع الداكن بـ media query.","why":"هذا يعالج العَرَض في حالة واحدة؛ وأيّ تغيير آخر في خاصية (تعديل padding عند breakpoint) سيظل يتحرّك."},{"text":"اضبط <code dir=\"ltr\">transition-duration: 0s</code> على <code dir=\"ltr\">:root</code>.","why":"الـ transitions غير موروثة، فلن يصل ذلك إلى البطاقة، وسيعطّل تأثير المرور أيضًا."},{"text":"حرّك الخصائص التي تقصدها فقط، مثل <code dir=\"ltr\">transition: translate 200ms, box-shadow 200ms</code>.","why":"صحيح. ذكر الخصائص يجعل الحركة مقصودة؛ وتتغيّر الألوان والتخطيط فورًا ما لم تطلب غير ذلك."}],"answer":2},{"q":"<p>يظهر إشعار منبثق (toast) بالتبديل من <code dir=\"ltr\">display: none</code> إلى <code dir=\"ltr\">display: block</code>، والـ transition الخاص بـ <code dir=\"ltr\">opacity</code> لا يعمل أبدًا. لماذا؟</p>","options":[{"text":"لا يمكن تحريك opacity على العناصر التي تستخدم display.","why":"تحريك opacity يعمل جيدًا؛ المشكلة أن العنصر ليس له نمط سابق ينطلق منه."},{"text":"حين يُرسم العنصر أول مرة لا توجد حالة سابقة، فلا شيء ينطلق منه الـ transition؛ و<code dir=\"ltr\">@starting-style</code> توفّر هذه الحالة.","why":"صحيح. <code dir=\"ltr\">@starting-style { .toast { opacity: 0; } }</code> تعطي المتصفّح قيم البداية لأول تحديث للنمط."},{"text":"الـ transitions لا تعمل إلا عند المرور بالمؤشّر.","why":"الـ transitions تعمل مع أيّ تغيّر في النمط المحسوب، أيًّا كان سببه."},{"text":"يجب ألّا تقل المدة عن 500ms ليلاحظها المتصفّح.","why":"لا يوجد حدّ أدنى للمدة؛ حتى الـ transitions بمدة 100ms تعمل."}],"answer":1},{"q":"<p>أيّ نهج يخدم على أفضل وجه المستخدمين الذين فعّلوا «تقليل الحركة» في نظام التشغيل؟</p>","options":[{"text":"ضع الحركة الزخرفية داخل <code dir=\"ltr\">@media (prefers-reduced-motion: no-preference)</code>، واجعل التلاشي البسيط أو التغيّر الفوري هو الافتراضي.","why":"صحيح. تصبح الحركة اختيارية، وتظل الواجهة تعبّر عن تغيّرات الحالة للجميع."},{"text":"أخفِ كل عنصر متحرّك حين يكون تقليل الحركة مفعّلًا.","why":"إخفاء المحتوى يحذف معلومات. المستخدمون طلبوا حركة أقل، لا واجهة أقل."},{"text":"أبطئ كل حركة إلى ثانيتين.","why":"الحركة الأبطأ تظل حركة، والحركات الطويلة تجعل الواجهة تبدو معطوبة."}],"answer":0}],"exercise":{"kind":"web","mode":"dom","starter":{"html":"<article class=\"session-card\">\n  <p class=\"status\"><span class=\"live-dot\" aria-hidden=\"true\"></span> Live now</p>\n  <h3>Motion With Manners</h3>\n  <p>Transitions, keyframes and reduced motion.</p>\n</article>","css":".session-card {\n  padding: 12px 16px;\n  border: 1px solid #e5e7eb;\n  border-radius: 12px;\n  background: white;\n}\n.session-card:hover {\n  translate: 0 -2px;\n  box-shadow: 0 6px 16px rgb(15 23 42 / 0.12);\n}\n.status { margin: 0; color: #b91c1c; font-weight: 600; }\n.live-dot {\n  position: relative;\n  display: inline-block;\n  width: 8px;\n  height: 8px;\n  border-radius: 50%;\n  background: currentColor;\n}\n.live-dot::after {\n  content: \"\";\n  position: absolute;\n  inset: 0;\n  border-radius: 50%;\n  background: currentColor;\n}\n\n/* Add the transition, the pulse animation and reduced-motion handling */\n"},"solution":{"html":"<article class=\"session-card\">\n  <p class=\"status\"><span class=\"live-dot\" aria-hidden=\"true\"></span> Live now</p>\n  <h3>Motion With Manners</h3>\n  <p>Transitions, keyframes and reduced motion.</p>\n</article>","css":".session-card {\n  padding: 12px 16px;\n  border: 1px solid #e5e7eb;\n  border-radius: 12px;\n  background: white;\n}\n.session-card:hover {\n  translate: 0 -2px;\n  box-shadow: 0 6px 16px rgb(15 23 42 / 0.12);\n}\n.status { margin: 0; color: #b91c1c; font-weight: 600; }\n.live-dot {\n  position: relative;\n  display: inline-block;\n  width: 8px;\n  height: 8px;\n  border-radius: 50%;\n  background: currentColor;\n}\n.live-dot::after {\n  content: \"\";\n  position: absolute;\n  inset: 0;\n  border-radius: 50%;\n  background: currentColor;\n}\n\n@keyframes pulse {\n  0% { scale: 1; opacity: 1; }\n  70% { scale: 2.2; opacity: 0; }\n  100% { scale: 2.2; opacity: 0; }\n}\n\n@media (prefers-reduced-motion: no-preference) {\n  .session-card {\n    transition: translate 200ms ease-out, box-shadow 200ms ease-out;\n  }\n  .live-dot::after {\n    animation: pulse 1.6s ease-out infinite;\n  }\n}\n"},"checks":[{"id":"transition-specific","test":"const all = []; const walk = (rules, media) => { for (const r of rules) { all.push({ r, media }); if (r.cssRules) walk(r.cssRules, media || (r instanceof CSSMediaRule && r.conditionText.includes('prefers-reduced-motion') ? r.conditionText : '')); } }; for (const s of document.styleSheets) walk(s.cssRules, ''); if (matchMedia('(prefers-reduced-motion: reduce)').matches) return all.some(({ r }) => r.style && /translate|transform/.test(r.style.transitionProperty || r.style.transition || '')); const props = style('.session-card', 'transition-property').split(',').map((s) => s.trim()); return props.some((p) => p === 'translate' || p === 'transform') && props.includes('box-shadow') && !props.includes('all');"},{"id":"transition-duration","test":"const all = []; const walk = (rules, media) => { for (const r of rules) { all.push({ r, media }); if (r.cssRules) walk(r.cssRules, media || (r instanceof CSSMediaRule && r.conditionText.includes('prefers-reduced-motion') ? r.conditionText : '')); } }; for (const s of document.styleSheets) walk(s.cssRules, ''); if (matchMedia('(prefers-reduced-motion: reduce)').matches) return true; const ds = style('.session-card', 'transition-duration').split(',').map((s) => parseFloat(s) * (s.trim().endsWith('ms') ? 0.001 : 1)); return ds.length > 0 && ds.every((d) => d >= 0.1 && d <= 0.4);"},{"id":"pulse-loops","test":"const all = []; const walk = (rules, media) => { for (const r of rules) { all.push({ r, media }); if (r.cssRules) walk(r.cssRules, media || (r instanceof CSSMediaRule && r.conditionText.includes('prefers-reduced-motion') ? r.conditionText : '')); } }; for (const s of document.styleSheets) walk(s.cssRules, ''); const hasKeyframes = all.some(({ r }) => r instanceof CSSKeyframesRule); if (matchMedia('(prefers-reduced-motion: reduce)').matches) return hasKeyframes; const s = getComputedStyle($('.live-dot'), '::after'); return hasKeyframes && s.animationName !== 'none' && s.animationIterationCount === 'infinite';"},{"id":"pulse-cheap","test":"const all = []; const walk = (rules, media) => { for (const r of rules) { all.push({ r, media }); if (r.cssRules) walk(r.cssRules, media || (r instanceof CSSMediaRule && r.conditionText.includes('prefers-reduced-motion') ? r.conditionText : '')); } }; for (const s of document.styleSheets) walk(s.cssRules, ''); const kf = all.map(({ r }) => r).filter((r) => r instanceof CSSKeyframesRule); if (!kf.length) return false; const allowed = ['scale', 'opacity', 'transform', 'translate', 'rotate']; return kf.every((k) => [...k.cssRules].every((f) => [...f.style].every((p) => allowed.includes(p))));"},{"id":"reduced-motion","test":"const all = []; const walk = (rules, media) => { for (const r of rules) { all.push({ r, media }); if (r.cssRules) walk(r.cssRules, media || (r instanceof CSSMediaRule && r.conditionText.includes('prefers-reduced-motion') ? r.conditionText : '')); } }; for (const s of document.styleSheets) walk(s.cssRules, ''); return all.some(({ r, media }) => media && r.style && (r.style.animation || r.style.animationName || r.style.transition || r.style.transitionProperty || r.style.animationDuration || r.style.transitionDuration));"}],"title":"حركة بأدب","prompt":"<p>أضف الحركة إلى البطاقة، بلطف:</p>\n<ol>\n<li>أضف transition لارتفاع المرور: حرّك <strong><code dir=\"ltr\">translate</code></strong> (أو <code dir=\"ltr\">transform</code>) و**<code dir=\"ltr\">box-shadow</code>** فقط، ولا تستخدم <code dir=\"ltr\">all</code> أبدًا، بمدد بين 100ms و400ms.</li>\n<li>أنشئ حركة <code dir=\"ltr\">@keyframes</code> تجعل هالة <code dir=\"ltr\">.live-dot::after</code> تنبض <strong>إلى الأبد</strong>، باستخدام خصائص رخيصة فقط (<code dir=\"ltr\">scale</code> و<code dir=\"ltr\">opacity</code> و<code dir=\"ltr\">transform</code> و<code dir=\"ltr\">translate</code> و<code dir=\"ltr\">rotate</code>).</li>\n<li>اجعل الـ transition والنبض يحترمان <strong><code dir=\"ltr\">prefers-reduced-motion</code></strong> (يُنصح بالنمط الاختياري مع <code dir=\"ltr\">no-preference</code>).</li>\n</ol>\n","hints":["تقبل <code dir=\"ltr\">transition</code> قائمة مفصولة بفواصل: <code dir=\"ltr\">translate 200ms ease-out, box-shadow 200ms ease-out</code>.","للنبض حرّك <code dir=\"ltr\">scale</code> و<code dir=\"ltr\">opacity</code> في <code dir=\"ltr\">@keyframes</code>، واستخدم <code dir=\"ltr\">infinite</code> كعدد للتكرار.","ضع تصريحي <code dir=\"ltr\">transition</code> و<code dir=\"ltr\">animation</code> كليهما داخل <code dir=\"ltr\">@media (prefers-reduced-motion: no-preference) { … }</code>."],"explanation":"<p>ذكر <code dir=\"ltr\">translate</code> و<code dir=\"ltr\">box-shadow</code> يعني أن الارتفاع وحده يتحرّك؛ وتبقى تغييرات الثيم والتخطيط فورية. والـ keyframes الخاصة بالنبض لا تلمس إلا <code dir=\"ltr\">scale</code> و<code dir=\"ltr\">opacity</code>، اللتين تستطيع المتصفّحات تحريكهما على الـ compositor دون إعادة حساب التخطيط. وتغليف الاثنين داخل <code dir=\"ltr\">@media (prefers-reduced-motion: no-preference)</code> يجعل الحركة اختيارية: من طلبوا من نظام التشغيل تقليل الحركة يحصلون على بطاقة ثابتة ونقطة ثابتة، بينما يظل النص «Live now» يخبرهم بما كانت تعنيه النقطة.</p>\n","checkLabels":{"transition-specific":"المرور يحرّك translate وbox-shadow، لا all","transition-duration":"مدد الـ transition بين 100ms و400ms","pulse-loops":"هالة نقطة Live تنبض إلى الأبد","pulse-cheap":"الـ keyframes لا تحرّك إلا خصائص رخيصة","reduced-motion":"الحركة تحترم prefers-reduced-motion"}},"words":637,"readMinutes":4}