Computer Science  ·  Level 6
Web Designing
Chapter 1: HTML basics
📚 5 Topics
⚑ What you will be able to do

By the end of this chapter, you will be able to:

  • Use common HTML terms correctly in your web design work.
  • Create a complete and well-structured HTML file from start to finish.
  • Apply the essential HTML elements accurately in your web pages.
  • Integrate these core HTML elements properly within your web documents.

Mastering these skills will help you build strong, professional web pages that form the foundation of any website.

The foundation of web development lies in understanding the structure and language used to create web pages. For Computer Science professionals in Kenya, mastering HTML is essential for designing websites that serve diverse sectors such as education, finance, healthcare, and government services. This chapter introduces the fundamental concepts of HTML, providing a solid base for developing interactive and well-structured web pages. Understanding HTML terminologies is crucial for effective communication and implementation of web design projects in professional environments.

1.1 Definition of HTML

HTML, short for HyperText Markup Language, is the standard markup language used to create and design web pages on the World Wide Web. It defines the structure of a web document by using a series of elements that browsers interpret to display text, images, links, and other content. In Kenya’s growing digital economy, professionals at institutions such as the Kenya Revenue Authority or county government offices rely on HTML to build informative and user-friendly websites that facilitate public services.

1.1.1 Origin and Evolution of HTML

HTML was first developed by Tim Berners-Lee in 1991 to facilitate the sharing of documents over the internet. Since then, it has evolved through various versions, with HTML5 being the current standard. HTML5 introduced new semantic elements, multimedia support, and improved APIs, enabling richer web experiences without relying heavily on external plugins. This evolution reflects the increasing demand for dynamic and accessible web content in sectors like education and banking across Kenya.

1.1.2 Role of HTML in Web Development

HTML acts as the backbone of every web page, providing the skeleton that holds content together. It works alongside CSS and JavaScript to create visually appealing and interactive websites. For example, at Nairobi Hospital’s online portal, HTML structures patient information and appointment schedules, while CSS styles the interface and JavaScript manages user interactions. Understanding HTML is fundamental before advancing to these complementary technologies.

1.1.3 HTML as a Markup Language

Unlike programming languages that execute logic, HTML is a markup language that annotates text to instruct browsers how to display content. It uses tags to mark elements such as headings, paragraphs, images, and links. This distinction is important for developers in Kenya’s tech industry, where clear separation of content (HTML), presentation (CSS), and behavior (JavaScript) improves maintainability and scalability of web applications.

1.1.4 Importance of HTML Proficiency for Kenyan Computer Scientists

Proficiency in HTML enables Kenyan computer scientists to build accessible and responsive websites that meet local and international standards. For instance, SACCOs use HTML to create member portals that handle sensitive financial data securely. Moreover, knowledge of HTML enhances collaboration with designers and backend developers, ensuring seamless integration of front-end and server-side components.

Practice Questions

  1. Explain the primary purpose of HTML in web development. (5 marks)
  2. Describe the evolution of HTML and highlight the significance of HTML5. (6 marks)
  3. Differentiate between a markup language and a programming language with examples. (5 marks)
  4. Discuss why HTML proficiency is important for a computer scientist working in Kenya’s public sector. (6 marks)
The rest of this chapter
🔒

Create a free account to open more of this chapter.

Free: practical guides, quick cards, workplace scenarios and more.

Create a free account
🔒1.2 HTML Terminologies

A clear grasp of HTML-specific terminologies is vital for effective web design and communication among developers, designers, and stakeholders. These terms form the building blocks of web pages and are used universally in the field. Kenyan professionals workin…

🔒1.3 Creation of HTML file

Creating an HTML file is the foundational step in web design, enabling computer science professionals to build and publish content on the World Wide Web. In Kenya, where digital transformation is rapidly advancing across sectors such as banking, education, and…

🔒1.4 HTML core elements

In web design, understanding the fundamental HTML core elements is essential for structuring web pages correctly. These elements form the backbone of every webpage, defining its structure and content organization. For Computer Science professionals in Kenya wo…

🔒1.5 Integration of HTML Core Elements to File

In the Kenyan computer science sector, mastering the integration of HTML core elements into a file is fundamental for web developers and designers. This skill forms the backbone of creating functional and structured web pages used in diverse environments such…

Chapter Summary

This chapter introduced the fundamental concept of HTML as the standard markup language used to create web pages. Key HTML terminologies were explained, including the document which serves as the container for web content, stylesheets that control the appearance of elements, elements themselves which structure the content, and attributes that provide additional information about those elements. The process of creating an HTML file was detailed, emphasizing the importance of the document type declaration for browser compatibility and the necessity of saving the file with a .html extension. Core HTML elements were explored, highlighting the roles of the element as the root, the section for metadata, the element that defines the page title, and the <body> which contains the visible content. Finally, the chapter demonstrated how these core elements are integrated within an HTML file to form a complete and functional web document. This foundational knowledge prepares learners to build structured web pages and understand the essential components of HTML.</p> <div id="sec-self-assessment" class="self-assessment" style="background:#EBF5FB;border:1px solid #2E86C1;border-left:5px solid #2E86C1;border-radius:8px;padding:20px 24px;margin:32px 0;"> <h2>Self-Assessment</h2><div style="background:#E8F5E9;border:1px solid #A5D6A7;border-radius:6px;padding:9px 14px;margin:14px 0 0;font-size:13px;font-weight:700;color:#1B5E20;overflow:hidden;"><a href='/chapter/32522/self-assessment/pdf/' target='_blank' rel='noopener' style='float:right;font-size:11px;font-weight:600;color:#1E8449;text-decoration:none;border:1px solid #A5D6A7;border-radius:5px;padding:2px 9px;margin-left:8px;'>🔒 PDF</a>Download this self-assessment, with answers</div> <h3>A. Written Assessment</h3> <div class="practice-qs" style="background:#FFFBF0;border:1px solid #F0C040;border-left:5px solid #E8A020;border-radius:8px;padding:16px 20px;margin:20px 0 40px 0;"> <ol> <li>What does HTML stand for and why is it important in web design? (2 marks) </li> <li>Identify the correct HTML element used to define the title of a webpage. (MCQ)<br /> a) <code><head></code><br /> b) <code><title></code><br /> c) <code><body></code><br /> d) <code><html></code> (1 mark) </li> </ol> </div> <div style="margin:16px 0 4px;padding:14px 17px;background:#FFFDF7;border:1px solid #E8CFA0;border-radius:11px;display:flex;align-items:center;gap:11px;"><span style="flex-shrink:0;width:28px;height:28px;border-radius:50%;background:#fff;border:1px solid #E8CFA0;display:inline-flex;align-items:center;justify-content:center;font-size:13px;">🔒</span><span style="font-size:14.5px;line-height:1.6;color:#6b4a08;font-weight:600;">20 more in this section.</span></div> <p><div style="text-align:center;margin:2px 0 10px;"><a href="/signup/" style="display:inline-block;margin-top:11px;padding:7px 15px;border-radius:999px;background:#C87F0A;color:#fff;text-decoration:none;font-weight:700;font-size:12.5px;">Create a free account</a></p> </div> <h2>Chapter Examination Questions</h2><div style="background:#E8F5E9;border:1px solid #A5D6A7;border-radius:6px;padding:9px 14px;margin:14px 0 0;font-size:13px;font-weight:700;color:#1B5E20;overflow:hidden;"><a href='/chapter/32522/exam-questions/pdf/' target='_blank' rel='noopener' style='float:right;font-size:11px;font-weight:600;color:#1E8449;text-decoration:none;border:1px solid #A5D6A7;border-radius:5px;padding:2px 9px;margin-left:8px;'>🔒 PDF</a>Download these examination questions, with worked answers</div> <h3>SECTION A (40 Marks) - Answer ALL Questions</h3> <ol> <li>Define HTML and explain its role in web development, citing how a Kenyan software company might use it for an online banking portal. (4 marks) </li> <li>Differentiate between an HTML element and an HTML attribute, giving an example of each from a typical webpage. (4 marks) </li> </ol> <div style="margin:16px 0 4px;padding:14px 17px;background:#FFFDF7;border:1px solid #E8CFA0;border-radius:11px;display:flex;align-items:center;gap:11px;"><span style="flex-shrink:0;width:28px;height:28px;border-radius:50%;background:#fff;border:1px solid #E8CFA0;display:inline-flex;align-items:center;justify-content:center;font-size:13px;">🔒</span><span style="font-size:14.5px;line-height:1.6;color:#6b4a08;font-weight:600;">24 more in this section.</span></div> <div style="text-align:center;margin:2px 0 10px;"><a href="/signup/" style="display:inline-block;margin-top:11px;padding:7px 15px;border-radius:999px;background:#C87F0A;color:#fff;text-decoration:none;font-weight:700;font-size:12.5px;">Create a free account</a></div> </div><div id="sec-practicals" class="chapter-practicals" style="margin:32px 0;"><h2 style="background:linear-gradient(135deg,#1E8449,#27AE60);color:#fff;font-size:15px;font-weight:700;padding:12px 20px;border-radius:8px;margin-bottom:24px;letter-spacing:0.3px;">Chapter Practical Activities</h2><div class="practical-activity" style="background:#FAFFFE;border:1px solid #A5D6A7;border-left:6px solid #27AE60;border-radius:8px;padding:0 22px 18px;margin-bottom:44px;box-shadow:0 2px 6px rgba(30,132,73,0.08);page-break-inside:avoid;"><h3 style="background:#E8F5E9;color:#1B5E20;font-size:14px;font-weight:700;padding:12px 22px;margin:0 -22px 12px;border-bottom:2px solid #A5D6A7;border-radius:7px 7px 0 0;">Practical 1: Explain HTML and Define Key HTML Terminologies</h3><div style="text-align:center;border-bottom:2px solid #A5D6A7;padding:6px 0 10px;margin-bottom:12px;"><div style="font-size:11px;color:#555;letter-spacing:0.3px;">Computer Science · Level 6</div><div style="font-size:12px;font-weight:700;color:#222;">Web Designing</div><div style="font-size:11px;font-weight:700;color:#1B5E20;margin-top:3px;">PRACTICAL ASSESSMENT</div><div style="font-size:11px;color:#555;">TIME: 3 HOURS</div></div><div style="font-weight:700;color:#111;font-size:13px;margin:4px 0 8px;overflow:hidden;"><a href='/practical/230623/pdf/candidate_tool/' target='_blank' rel='noopener' style='float:right;font-size:11px;font-weight:600;color:#1E8449;text-decoration:none;border:1px solid #A5D6A7;border-radius:5px;padding:2px 9px;margin-left:8px;'>⬇ PDF</a>Candidate Instructions (Candidate Tool)</div><p style="font-size:12px;color:#444;margin:0 0 10px;"><strong>Type:</strong> Individual</p><div style="font-size:13px;line-height:1.6;margin:0 0 10px;"><div style="font-weight:700;margin-bottom:6px;">INSTRUCTIONS TO CANDIDATE:</div><div style="margin-left:6px;">1.  You are required to perform the following task:</div><div style="margin-left:30px;">i.  Write a one-page explanation defining HTML and describing at least five key HTML terminologies with examples.</div><div style="margin-left:6px;margin-top:4px;">2.  You have been provided with the following resources for the practical task:</div></div><div style="overflow-x:auto;-webkit-overflow-scrolling:touch;"><table style="width:100%;min-width:420px;border-collapse:collapse;font-size:12.5px;margin-bottom:10px;"><thead><tr style="background:#1E8449;color:#fff;"><th style="padding:7px 12px;text-align:left;">Tools & Equipment</th><th style="padding:7px 12px;text-align:left;">Materials</th></tr></thead><tbody><tr><td style="padding:5px 12px;border-bottom:1px solid #E0E0E0;vertical-align:top;">Functional computer with text editor installed</td><td style="padding:5px 12px;border-bottom:1px solid #E0E0E0;vertical-align:top;">Plain A4 paper</td></tr><tr><td style="padding:5px 12px;border-bottom:1px solid #E0E0E0;vertical-align:top;">Pen</td><td style="padding:5px 12px;border-bottom:1px solid #E0E0E0;vertical-align:top;"></td></tr></tbody></table></div><details style="margin:0;"><summary style="cursor:pointer;list-style:none;background:#E8F5E9;border:1px solid #A5D6A7;border-radius:6px;padding:9px 14px;margin:10px 0 0;font-size:13px;font-weight:700;color:#1B5E20;overflow:hidden;"><a href='/practical/230623/pdf/cutting_list/' target='_blank' rel='noopener' style='float:right;font-size:11px;font-weight:600;color:#1E8449;text-decoration:none;border:1px solid #A5D6A7;border-radius:5px;padding:2px 9px;margin-left:8px;'>⬇ PDF</a>Resources Required (Cutting List)</summary><div style="padding:12px 4px 4px;"><div style="overflow-x:auto;-webkit-overflow-scrolling:touch;"><table style="width:100%;min-width:440px;border-collapse:collapse;font-size:12.5px;"><thead><tr style="background:#1E8449;color:#fff;"><th style="padding:7px 12px;text-align:center;white-space:nowrap;">S/N</th><th style="padding:7px 12px;text-align:left;">Item</th><th style="padding:7px 12px;text-align:left;white-space:nowrap;">Quantity</th></tr></thead><tbody><tr><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;text-align:center;white-space:nowrap;">1</td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;">Functional computer with text editor installed</td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;white-space:nowrap;">1 Pc per Candidate</td></tr><tr><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;text-align:center;white-space:nowrap;">2</td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;">Plain A4 paper</td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;white-space:nowrap;">2 Sheets per Candidate</td></tr><tr><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;text-align:center;white-space:nowrap;">3</td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;">Pen</td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;white-space:nowrap;">1 Pc per Candidate</td></tr></tbody></table></div></div></details><details style="margin:0;"><summary style="cursor:pointer;list-style:none;background:#E8F5E9;border:1px solid #A5D6A7;border-radius:6px;padding:9px 14px;margin:10px 0 0;font-size:13px;font-weight:700;color:#1B5E20;overflow:hidden;"><a href='/practical/230623/pdf/assessor_guide/' target='_blank' rel='noopener' style='float:right;font-size:11px;font-weight:600;color:#1E8449;text-decoration:none;border:1px solid #A5D6A7;border-radius:5px;padding:2px 9px;margin-left:8px;'>⬇ PDF</a>Assessor Guide</summary><div style="padding:12px 4px 4px;"><div style="overflow-x:auto;-webkit-overflow-scrolling:touch;"><table style="width:100%;min-width:620px;border-collapse:collapse;font-size:12.5px;"><thead><tr style="background:#1E8449;color:#fff;"><th style="padding:7px 12px;text-align:left;">Items to be Evaluated</th><th style="padding:7px 12px;text-align:center;">Marks Available</th><th style="padding:7px 12px;text-align:left;">Marks Obtained</th><th style="padding:7px 12px;text-align:left;">Comments</th></tr></thead><tbody><tr><td colspan="4" style="background:#D8EEDC;font-weight:700;padding:7px 12px;color:#1B5E20;">TASK 1: Write a clear explanation of HTML</td></tr><tr><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;">Launched text editor on computer<br><span style="color:#666;font-size:11px;">(Award 2 marks for successful launch or zero)</span></td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;text-align:center;width:80px;">2</td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;width:100px;"></td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;width:110px;"></td></tr><tr><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;">Wrote a concise definition of HTML<br><span style="color:#666;font-size:11px;">(Award 5 marks for correct definition or zero)</span></td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;text-align:center;width:80px;">5</td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;width:100px;"></td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;width:110px;"></td></tr><tr><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;">Described at least five HTML terminologies correctly<br><span style="color:#666;font-size:11px;">(Award 3 marks for each correctly defined terminology or zero)</span></td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;text-align:center;width:80px;">15</td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;width:100px;"></td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;width:110px;"></td></tr><tr><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;">Provided appropriate examples for each terminology<br><span style="color:#666;font-size:11px;">(Award 2 marks for each example up to 8 marks or zero)</span></td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;text-align:center;width:80px;">8</td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;width:100px;"></td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;width:110px;"></td></tr><tr><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;">Organized content logically and legibly on paper<br><span style="color:#666;font-size:11px;">(Award 5 marks for clear presentation or zero)</span></td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;text-align:center;width:80px;">5</td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;width:100px;"></td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;width:110px;"></td></tr><tr style="background:#E8F5E9;font-weight:600;"><td style="padding:7px 12px;">Sub-Total</td><td style="padding:7px 12px;text-align:center;">35</td><td style="padding:7px 12px;"></td><td style="padding:7px 12px;"></td></tr><tr><td colspan="4" style="background:#D8EEDC;font-weight:700;padding:7px 12px;color:#1B5E20;">PRODUCT CHECKLIST</td></tr><tr><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;">Explanation is accurate, covers HTML definition and at least five terminologies with examples<br><span style="color:#666;font-size:11px;">(Award 10 marks for completeness and correctness or zero)</span></td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;text-align:center;width:80px;">10</td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;width:100px;"></td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;width:110px;"></td></tr><tr style="background:#E8F5E9;font-weight:600;"><td style="padding:7px 12px;">Sub-Total</td><td style="padding:7px 12px;text-align:center;">10</td><td style="padding:7px 12px;"></td><td style="padding:7px 12px;"></td></tr><tr style="background:#1E8449;color:#fff;font-weight:700;"><td style="padding:8px 12px;">GRAND TOTAL</td><td style="padding:8px 12px;text-align:center;">45</td><td style="padding:8px 12px;"></td><td style="padding:8px 12px;"></td></tr></tbody></table></div><div style="margin-top:10px;font-size:12px;"><strong>ASSESSMENT OUTCOME:</strong>   ☐ Competent    ☐ Not Yet Competent <span style="color:#666;">(competent if at least 50%)</span></div></div></details></div><div class="practical-activity" style="background:#FAFFFE;border:1px solid #A5D6A7;border-left:6px solid #27AE60;border-radius:8px;padding:0 22px 18px;margin-bottom:44px;box-shadow:0 2px 6px rgba(30,132,73,0.08);page-break-inside:avoid;"><h3 style="background:#E8F5E9;color:#1B5E20;font-size:14px;font-weight:700;padding:12px 22px;margin:0 -22px 12px;border-bottom:2px solid #A5D6A7;border-radius:7px 7px 0 0;">Practical 2: Create a Basic HTML Document Structure</h3><div style="text-align:center;border-bottom:2px solid #A5D6A7;padding:6px 0 10px;margin-bottom:12px;"><div style="font-size:11px;color:#555;letter-spacing:0.3px;">Computer Science · Level 6</div><div style="font-size:12px;font-weight:700;color:#222;">Web Designing</div><div style="font-size:11px;font-weight:700;color:#1B5E20;margin-top:3px;">PRACTICAL ASSESSMENT</div><div style="font-size:11px;color:#555;">TIME: 3 HOURS</div></div><div style="font-weight:700;color:#111;font-size:13px;margin:4px 0 8px;overflow:hidden;"><a href='/practical/230624/pdf/candidate_tool/' target='_blank' rel='noopener' style='float:right;font-size:11px;font-weight:600;color:#1E8449;text-decoration:none;border:1px solid #A5D6A7;border-radius:5px;padding:2px 9px;margin-left:8px;'>⬇ PDF</a>Candidate Instructions (Candidate Tool)</div><p style="font-size:12px;color:#444;margin:0 0 10px;"><strong>Type:</strong> Individual</p><div style="font-size:13px;line-height:1.6;margin:0 0 10px;"><div style="font-weight:700;margin-bottom:6px;">INSTRUCTIONS TO CANDIDATE:</div><div style="margin-left:6px;">1.  You are required to perform the following task:</div><div style="margin-left:30px;">i.  Create a basic HTML file named index.html with correct document type declaration and basic document structure including html, head, and body tags.</div><div style="margin-left:6px;margin-top:4px;">2.  You have been provided with the following resources for the practical task:</div></div><div style="overflow-x:auto;-webkit-overflow-scrolling:touch;"><table style="width:100%;min-width:420px;border-collapse:collapse;font-size:12.5px;margin-bottom:10px;"><thead><tr style="background:#1E8449;color:#fff;"><th style="padding:7px 12px;text-align:left;">Tools & Equipment</th><th style="padding:7px 12px;text-align:left;">Materials</th></tr></thead><tbody><tr><td style="padding:5px 12px;border-bottom:1px solid #E0E0E0;vertical-align:top;">Functional computer installed with text editor</td><td style="padding:5px 12px;border-bottom:1px solid #E0E0E0;vertical-align:top;">Plain paper</td></tr></tbody></table></div><details style="margin:0;"><summary style="cursor:pointer;list-style:none;background:#E8F5E9;border:1px solid #A5D6A7;border-radius:6px;padding:9px 14px;margin:10px 0 0;font-size:13px;font-weight:700;color:#1B5E20;overflow:hidden;"><a href='/practical/230624/pdf/cutting_list/' target='_blank' rel='noopener' style='float:right;font-size:11px;font-weight:600;color:#1E8449;text-decoration:none;border:1px solid #A5D6A7;border-radius:5px;padding:2px 9px;margin-left:8px;'>⬇ PDF</a>Resources Required (Cutting List)</summary><div style="padding:12px 4px 4px;"><div style="overflow-x:auto;-webkit-overflow-scrolling:touch;"><table style="width:100%;min-width:440px;border-collapse:collapse;font-size:12.5px;"><thead><tr style="background:#1E8449;color:#fff;"><th style="padding:7px 12px;text-align:center;white-space:nowrap;">S/N</th><th style="padding:7px 12px;text-align:left;">Item</th><th style="padding:7px 12px;text-align:left;white-space:nowrap;">Quantity</th></tr></thead><tbody><tr><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;text-align:center;white-space:nowrap;">1</td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;">Functional computer installed with text editor</td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;white-space:nowrap;">1 Pc per Candidate</td></tr><tr><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;text-align:center;white-space:nowrap;">2</td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;">Plain paper</td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;white-space:nowrap;">1 Pc per Candidate</td></tr></tbody></table></div></div></details><details style="margin:0;"><summary style="cursor:pointer;list-style:none;background:#E8F5E9;border:1px solid #A5D6A7;border-radius:6px;padding:9px 14px;margin:10px 0 0;font-size:13px;font-weight:700;color:#1B5E20;overflow:hidden;"><a href='/practical/230624/pdf/assessor_guide/' target='_blank' rel='noopener' style='float:right;font-size:11px;font-weight:600;color:#1E8449;text-decoration:none;border:1px solid #A5D6A7;border-radius:5px;padding:2px 9px;margin-left:8px;'>⬇ PDF</a>Assessor Guide</summary><div style="padding:12px 4px 4px;"><div style="overflow-x:auto;-webkit-overflow-scrolling:touch;"><table style="width:100%;min-width:620px;border-collapse:collapse;font-size:12.5px;"><thead><tr style="background:#1E8449;color:#fff;"><th style="padding:7px 12px;text-align:left;">Items to be Evaluated</th><th style="padding:7px 12px;text-align:center;">Marks Available</th><th style="padding:7px 12px;text-align:left;">Marks Obtained</th><th style="padding:7px 12px;text-align:left;">Comments</th></tr></thead><tbody><tr><td colspan="4" style="background:#D8EEDC;font-weight:700;padding:7px 12px;color:#1B5E20;">TASK 1: Create Basic HTML Document</td></tr><tr><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;">Launched the text editor and created a new file named index.html<br><span style="color:#666;font-size:11px;">(Award 2 marks for correctly launching editor and naming the file)</span></td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;text-align:center;width:80px;">2</td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;width:100px;"></td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;width:110px;"></td></tr><tr><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;">Added correct document type declaration <!DOCTYPE html><br><span style="color:#666;font-size:11px;">(Award 4 marks for correct and properly placed DOCTYPE declaration)</span></td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;text-align:center;width:80px;">4</td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;width:100px;"></td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;width:110px;"></td></tr><tr><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;">Created the root <html> element enclosing the entire document<br><span style="color:#666;font-size:11px;">(Award 3 marks for correctly coding the <html> opening and closing tags)</span></td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;text-align:center;width:80px;">3</td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;width:100px;"></td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;width:110px;"></td></tr><tr><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;">Included the <head> section with a <title> element<br><span style="color:#666;font-size:11px;">(Award 5 marks for correct <head> and <title> tags inside the document)</span></td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;text-align:center;width:80px;">5</td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;width:100px;"></td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;width:110px;"></td></tr><tr><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;">Created the <body> section with placeholder content<br><span style="color:#666;font-size:11px;">(Award 4 marks for adding the <body> opening and closing tags with placeholder text)</span></td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;text-align:center;width:80px;">4</td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;width:100px;"></td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;width:110px;"></td></tr><tr style="background:#E8F5E9;font-weight:600;"><td style="padding:7px 12px;">Sub-Total</td><td style="padding:7px 12px;text-align:center;">18</td><td style="padding:7px 12px;"></td><td style="padding:7px 12px;"></td></tr><tr><td colspan="4" style="background:#D8EEDC;font-weight:700;padding:7px 12px;color:#1B5E20;">PRODUCT CHECKLIST</td></tr><tr><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;">HTML file contains correct document type declaration and properly nested html, head, title, and body tags<br><span style="color:#666;font-size:11px;">(Award 10 marks for correct structure, syntax, and file saved as index.html)</span></td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;text-align:center;width:80px;">10</td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;width:100px;"></td><td style="padding:6px 12px;border-bottom:1px solid #E0E0E0;width:110px;"></td></tr><tr style="background:#E8F5E9;font-weight:600;"><td style="padding:7px 12px;">Sub-Total</td><td style="padding:7px 12px;text-align:center;">10</td><td style="padding:7px 12px;"></td><td style="padding:7px 12px;"></td></tr><tr style="background:#1E8449;color:#fff;font-weight:700;"><td style="padding:8px 12px;">GRAND TOTAL</td><td style="padding:8px 12px;text-align:center;">28</td><td style="padding:8px 12px;"></td><td style="padding:8px 12px;"></td></tr></tbody></table></div><div style="margin-top:10px;font-size:12px;"><strong>ASSESSMENT OUTCOME:</strong>   ☐ Competent    ☐ Not Yet Competent <span style="color:#666;">(competent if at least 50%)</span></div></div></details></div><div style="margin:18px 0 20px;padding:22px 24px;border-radius:16px;border:1px solid #E8CFA0;text-align:center;background:linear-gradient(160deg,#FFFDF7 0%,#FFF6E6 100%);"><span style="display:inline-flex;width:40px;height:40px;border-radius:50%;background:#fff;border:1px solid #E8CFA0;align-items:center;justify-content:center;font-size:18px;margin-bottom:10px;">🔒</span><p style="margin:0;font-size:16.5px;line-height:1.55;color:#5c3f06;font-weight:800;">Free: practical guides, quick cards, workplace scenarios and more.</p><a href="/signup/" style="display:inline-block;margin-top:11px;padding:7px 15px;border-radius:999px;background:#C87F0A;color:#fff;text-decoration:none;font-weight:700;font-size:12.5px;">Create a free account</a></div><div style="display:flex;align-items:center;gap:13px;margin:0 0 9px;padding:14px 18px;border:1px solid #E8EDF3;border-radius:12px;background:#fff;"><span style="flex-shrink:0;width:30px;height:30px;border-radius:50%;background:#F4F7FA;display:inline-flex;align-items:center;justify-content:center;font-size:13px;line-height:1;">🔒</span><span style="flex:1;min-width:0;font-size:15px;font-weight:600;color:#7C8B9C;line-height:1.45;">Save HTML file with correct .html extension</span><span style="flex-shrink:0;font-size:10.5px;font-weight:700;letter-spacing:1.1px;text-transform:uppercase;color:#B08A3E;">Practical 3</span></div><div style="display:flex;align-items:center;gap:13px;margin:0 0 9px;padding:14px 18px;border:1px solid #E8EDF3;border-radius:12px;background:#fff;"><span style="flex-shrink:0;width:30px;height:30px;border-radius:50%;background:#F4F7FA;display:inline-flex;align-items:center;justify-content:center;font-size:13px;line-height:1;">🔒</span><span style="flex:1;min-width:0;font-size:15px;font-weight:600;color:#7C8B9C;line-height:1.45;">Create a valid HTML5 page with core elements</span><span style="flex-shrink:0;font-size:10.5px;font-weight:700;letter-spacing:1.1px;text-transform:uppercase;color:#B08A3E;">Practical 4</span></div><div style="display:flex;align-items:center;gap:13px;margin:0 0 9px;padding:14px 18px;border:1px solid #E8EDF3;border-radius:12px;background:#fff;"><span style="flex-shrink:0;width:30px;height:30px;border-radius:50%;background:#F4F7FA;display:inline-flex;align-items:center;justify-content:center;font-size:13px;line-height:1;">🔒</span><span style="flex:1;min-width:0;font-size:15px;font-weight:600;color:#7C8B9C;line-height:1.45;">Integrate core HTML elements into a complete webpage</span><span style="flex-shrink:0;font-size:10.5px;font-weight:700;letter-spacing:1.1px;text-transform:uppercase;color:#B08A3E;">Practical 5</span></div><div style="display:flex;align-items:center;gap:13px;margin:0 0 9px;padding:14px 18px;border:1px solid #E8EDF3;border-radius:12px;background:#fff;"><span style="flex-shrink:0;width:30px;height:30px;border-radius:50%;background:#F4F7FA;display:inline-flex;align-items:center;justify-content:center;font-size:13px;line-height:1;">🔒</span><span style="flex:1;min-width:0;font-size:15px;font-weight:600;color:#7C8B9C;line-height:1.45;">Create and Explain Basic HTML Elements in a Sample Webpage</span><span style="flex-shrink:0;font-size:10.5px;font-weight:700;letter-spacing:1.1px;text-transform:uppercase;color:#B08A3E;">Practical 6</span></div><div style="display:flex;align-items:center;gap:13px;margin:0 0 9px;padding:14px 18px;border:1px solid #E8EDF3;border-radius:12px;background:#fff;"><span style="flex-shrink:0;width:30px;height:30px;border-radius:50%;background:#F4F7FA;display:inline-flex;align-items:center;justify-content:center;font-size:13px;line-height:1;">🔒</span><span style="flex:1;min-width:0;font-size:15px;font-weight:600;color:#7C8B9C;line-height:1.45;">Add and Explain HTML Attributes in a Web Page</span><span style="flex-shrink:0;font-size:10.5px;font-weight:700;letter-spacing:1.1px;text-transform:uppercase;color:#B08A3E;">Practical 7</span></div><div style="display:flex;align-items:center;gap:13px;margin:0 0 9px;padding:14px 18px;border:1px solid #E8EDF3;border-radius:12px;background:#fff;"><span style="flex-shrink:0;width:30px;height:30px;border-radius:50%;background:#F4F7FA;display:inline-flex;align-items:center;justify-content:center;font-size:13px;line-height:1;">🔒</span><span style="flex:1;min-width:0;font-size:15px;font-weight:600;color:#7C8B9C;line-height:1.45;">Create and Link External Stylesheet to HTML Document</span><span style="flex-shrink:0;font-size:10.5px;font-weight:700;letter-spacing:1.1px;text-transform:uppercase;color:#B08A3E;">Practical 8</span></div><div style="display:flex;align-items:center;gap:13px;margin:0 0 9px;padding:14px 18px;border:1px solid #E8EDF3;border-radius:12px;background:#fff;"><span style="flex-shrink:0;width:30px;height:30px;border-radius:50%;background:#F4F7FA;display:inline-flex;align-items:center;justify-content:center;font-size:13px;line-height:1;">🔒</span><span style="flex:1;min-width:0;font-size:15px;font-weight:600;color:#7C8B9C;line-height:1.45;">Write an HTML Document with Head and Title Elements</span><span style="flex-shrink:0;font-size:10.5px;font-weight:700;letter-spacing:1.1px;text-transform:uppercase;color:#B08A3E;">Practical 9</span></div><div style="display:flex;align-items:center;gap:13px;margin:0 0 9px;padding:14px 18px;border:1px solid #E8EDF3;border-radius:12px;background:#fff;"><span style="flex-shrink:0;width:30px;height:30px;border-radius:50%;background:#F4F7FA;display:inline-flex;align-items:center;justify-content:center;font-size:13px;line-height:1;">🔒</span><span style="flex:1;min-width:0;font-size:15px;font-weight:600;color:#7C8B9C;line-height:1.45;">Create a Simple HTML Document with Body Content</span><span style="flex-shrink:0;font-size:10.5px;font-weight:700;letter-spacing:1.1px;text-transform:uppercase;color:#B08A3E;">Practical 10</span></div><div style="display:flex;align-items:center;gap:13px;margin:0 0 9px;padding:14px 18px;border:1px solid #E8EDF3;border-radius:12px;background:#fff;"><span style="flex-shrink:0;width:30px;height:30px;border-radius:50%;background:#F4F7FA;display:inline-flex;align-items:center;justify-content:center;font-size:13px;line-height:1;">🔒</span><span style="flex:1;min-width:0;font-size:15px;font-weight:600;color:#7C8B9C;line-height:1.45;">Explain the Purpose of Document Type Declaration in HTML</span><span style="flex-shrink:0;font-size:10.5px;font-weight:700;letter-spacing:1.1px;text-transform:uppercase;color:#B08A3E;">Practical 11</span></div><div style="display:flex;align-items:center;gap:13px;margin:0 0 9px;padding:14px 18px;border:1px solid #E8EDF3;border-radius:12px;background:#fff;"><span style="flex-shrink:0;width:30px;height:30px;border-radius:50%;background:#F4F7FA;display:inline-flex;align-items:center;justify-content:center;font-size:13px;line-height:1;">🔒</span><span style="flex:1;min-width:0;font-size:15px;font-weight:600;color:#7C8B9C;line-height:1.45;">Save and Open a Simple HTML File in a Web Browser</span><span style="flex-shrink:0;font-size:10.5px;font-weight:700;letter-spacing:1.1px;text-transform:uppercase;color:#B08A3E;">Practical 12</span></div><div style="display:flex;align-items:center;gap:13px;margin:0 0 9px;padding:14px 18px;border:1px solid #E8EDF3;border-radius:12px;background:#fff;"><span style="flex-shrink:0;width:30px;height:30px;border-radius:50%;background:#F4F7FA;display:inline-flex;align-items:center;justify-content:center;font-size:13px;line-height:1;">🔒</span><span style="flex:1;min-width:0;font-size:15px;font-weight:600;color:#7C8B9C;line-height:1.45;">Create an HTML page demonstrating multiple attributes in elements</span><span style="flex-shrink:0;font-size:10.5px;font-weight:700;letter-spacing:1.1px;text-transform:uppercase;color:#B08A3E;">Practical 13</span></div><div style="display:flex;align-items:center;gap:13px;margin:0 0 9px;padding:14px 18px;border:1px solid #E8EDF3;border-radius:12px;background:#fff;"><span style="flex-shrink:0;width:30px;height:30px;border-radius:50%;background:#F4F7FA;display:inline-flex;align-items:center;justify-content:center;font-size:13px;line-height:1;">🔒</span><span style="flex:1;min-width:0;font-size:15px;font-weight:600;color:#7C8B9C;line-height:1.45;">Create a minimal valid HTML webpage with core elements</span><span style="flex-shrink:0;font-size:10.5px;font-weight:700;letter-spacing:1.1px;text-transform:uppercase;color:#B08A3E;">Practical 14</span></div><div style="display:flex;align-items:center;gap:13px;margin:0 0 9px;padding:14px 18px;border:1px solid #E8EDF3;border-radius:12px;background:#fff;"><span style="flex-shrink:0;width:30px;height:30px;border-radius:50%;background:#F4F7FA;display:inline-flex;align-items:center;justify-content:center;font-size:13px;line-height:1;">🔒</span><span style="flex:1;min-width:0;font-size:15px;font-weight:600;color:#7C8B9C;line-height:1.45;">Explain the difference between HTML element and attribute with examples</span><span style="flex-shrink:0;font-size:10.5px;font-weight:700;letter-spacing:1.1px;text-transform:uppercase;color:#B08A3E;">Practical 15</span></div></div> </div> <script id="sa-fc-data" type="application/json">[{"q": "What is HTML and what is its primary purpose in web development?", "points": ["HTML stands for HyperText Markup Language.", "It is the standard markup language used to create and design web pages.", "Defines the structure of web documents using elements and tags.", "Browsers interpret HTML to display text, images, links, and other content.", "Forms the backbone or skeleton of every web page."]}, {"q": "Who developed HTML and what is the significance of HTML5?", "points": ["HTML was developed by Tim Berners-Lee in 1991.", "HTML5 is the current standard version of HTML.", "Introduced new semantic elements for better content structure.", "Added multimedia support without relying on external plugins.", "Improved APIs enable richer and more dynamic web experiences."]}]</script> <script id="sa-mcq-data" type="application/json">[{"q": "Which Kenyan institution is mentioned as using HTML to build informative and user-friendly websites?", "why": "Kenya Revenue Authority relies on HTML for public service websites.", "answer": 3, "options": ["Kenya Airways", "Kenya Power", "Kenya Wildlife Service", "Kenya Revenue Authority"]}, {"q": "What is a key feature introduced by HTML5 according to the notes?", "why": "HTML5 introduced semantic elements and multimedia support.", "answer": 3, "options": ["Built-in database management", "Ability to execute server-side scripts", "Automatic website translation", "New semantic elements and multimedia support"]}]</script> <div class="study-aids" style="margin-top:36px;"> <details class="sa-block" style="border:1px solid #B6D0EA;border-radius:14px;margin-bottom:18px;overflow:hidden;"> <summary style="cursor:pointer;list-style:none;padding:16px 20px;background:var(--primary);display:flex;align-items:center;gap:12px;"> <span style="font-size:16px;font-weight:600;color:#fff;">Flashcards</span> <span style="background:#fff;color:var(--primary);font-size:12px;font-weight:600;padding:3px 10px;border-radius:999px;">20 cards</span> <span style="margin-left:auto;font-size:13px;color:#CFE0F0;">Study deck ▾</span> </summary> <div style="padding:18px 18px 20px;"> <div style="display:flex;align-items:center;gap:10px;margin-bottom:12px;"> <div id="sa-fc-dots" style="display:flex;gap:5px;flex:1;"></div> <span id="sa-fc-count" style="font-size:13px;color:#6B7B8D;font-weight:600;"></span> </div> <div id="sa-fc-card" style="border:1.5px solid #2E86C1;border-radius:16px;background:#fff;cursor:pointer;min-height:190px;"> <div id="sa-fc-strip" style="background:#EBF2FA;padding:10px 18px;border-radius:14px 14px 0 0;"> <span id="sa-fc-side" style="font-size:11px;letter-spacing:.07em;text-transform:uppercase;font-weight:700;color:var(--primary);">Question</span> </div> <div style="padding:20px 20px 16px;"> <div id="sa-fc-front"> <div style="display:flex;gap:12px;align-items:flex-start;"> <div id="sa-fc-num" style="flex-shrink:0;width:32px;height:32px;border-radius:50%;background:#2E86C1;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:700;font-size:14px;">1</div> <p id="sa-fc-q" style="margin:0;font-size:17px;line-height:1.55;color:var(--primary);"></p> </div> </div> <div id="sa-fc-back" style="display:none;"> <div style="color:#1E8449;font-weight:700;font-size:13px;margin-bottom:10px;">Answer</div> <ul id="sa-fc-points" style="margin:0;padding-left:0;list-style:none;display:flex;flex-direction:column;gap:9px;"></ul> </div> </div> <div id="sa-fc-cue" style="background:#2E86C1;color:#fff;text-align:center;padding:9px;font-size:13px;font-weight:600;border-radius:0 0 14px 14px;">↻ Tap card to reveal answer</div> </div> <div style="display:flex;gap:10px;margin-top:14px;"> <button type="button" onclick="saFcPrev()" style="flex:1;padding:11px;border:1px solid #CBD5E0;border-radius:999px;background:#fff;color:var(--primary);font-weight:600;font-size:14px;cursor:pointer;">← Prev</button> <button type="button" onclick="saFcShuffle()" style="flex:1;padding:11px;border:1px solid #CBD5E0;border-radius:999px;background:#fff;color:var(--primary);font-weight:600;font-size:14px;cursor:pointer;">⨭ Shuffle</button> <button type="button" onclick="saFcNext()" style="flex:1;padding:11px;border:none;border-radius:999px;background:var(--primary);color:#fff;font-weight:600;font-size:14px;cursor:pointer;">Next →</button> </div> <div style="margin:14px 0 2px;padding:12px 15px;border:1px solid #E0C88A;border-radius:16px;text-align:center;background:linear-gradient(160deg,#FFFDF7 0%,#FFF6E6 100%);"><span style="display:inline-flex;width:26px;height:26px;border-radius:50%;background:#fff;border:1px solid #E0C88A;align-items:center;justify-content:center;font-size:12px;margin-bottom:6px;">🔒</span><p style="margin:0;font-size:13.5px;line-height:1.5;color:#5c3f06;font-weight:700;">18 more in this section.</p><a href="/signup/" style="display:inline-block;margin-top:11px;padding:7px 15px;border-radius:999px;background:#C87F0A;color:#fff;text-decoration:none;font-weight:700;font-size:12.5px;">Create a free account</a></div> </div> </details> <details class="sa-block" style="border:1px solid #CFE6D9;border-radius:14px;overflow:hidden;"> <summary style="cursor:pointer;list-style:none;padding:16px 20px;background:#2E7D4F;display:flex;align-items:center;gap:12px;"> <span style="font-size:16px;font-weight:600;color:#fff;">Test Yourself</span> <span style="background:#fff;color:#2E7D4F;font-size:12px;font-weight:600;padding:3px 10px;border-radius:999px;">15 questions</span> <span style="margin-left:auto;font-size:13px;color:#CFE6D9;">Start quiz ▾</span> </summary> <div style="padding:18px;"> <div style="display:flex;align-items:center;gap:12px;margin-bottom:14px;"> <div style="flex:1;height:8px;background:#EAEDF0;border-radius:999px;overflow:hidden;"><div id="sa-quiz-bar" style="height:100%;width:0;background:#1E8449;transition:width .3s;"></div></div> <div style="text-align:right;min-width:54px;"> <div id="sa-quiz-pct" style="font-size:22px;font-weight:700;color:var(--primary);line-height:1;">0%</div> <div id="sa-quiz-score" style="font-size:12px;color:#6B7B8D;">0 / 2</div> </div> </div> <div id="sa-quiz-mount"></div> <div id="sa-quiz-done" style="display:none;text-align:center;padding:16px;border-radius:12px;margin-top:12px;font-weight:700;"></div> <div style="margin:14px 0 2px;padding:12px 15px;border:1px solid #E0C88A;border-radius:16px;text-align:center;background:linear-gradient(160deg,#FFFDF7 0%,#FFF6E6 100%);"><span style="display:inline-flex;width:26px;height:26px;border-radius:50%;background:#fff;border:1px solid #E0C88A;align-items:center;justify-content:center;font-size:12px;margin-bottom:6px;">🔒</span><p style="margin:0;font-size:13.5px;line-height:1.5;color:#5c3f06;font-weight:700;">13 more in this section.</p><a href="/signup/" style="display:inline-block;margin-top:11px;padding:7px 15px;border-radius:999px;background:#C87F0A;color:#fff;text-decoration:none;font-weight:700;font-size:12.5px;">Create a free account</a></div> </div> </details> </div> <!-- Edit Panel --> <style> /* Topic-note editor: keep toolbar pinned, scroll only the note text (like the flag editor) */ #notesEditorWrap .editor-toolbar { position:sticky; top:0; z-index:6; background:#fff; } #notesEditorWrap .CodeMirror-scroll { max-height:52vh; } /* Editor opened from a per-heading ✏ Edit: float it in place like the flag editor */ #editPanel.edit-floating, #practicalsPanel.edit-floating { position: fixed; top: 6vh; left: 50%; transform: translateX(-50%); width: min(900px, 94vw); max-height: 86vh; overflow-y: auto; background: #fff; border: 1px solid #D8E0E8; border-radius: 14px; box-shadow: 0 18px 60px rgba(20,40,70,0.28); z-index: 4000; margin: 0; padding: 20px 22px; } #editFloatBackdrop { display:none; position:fixed; inset:0; background:rgba(20,35,55,0.35); z-index:3999; } .img-delete-btn { display:none !important; } body.note-editing .img-delete-btn { display:block !important; } #editFloatBackdrop.on { display:block; } </style> <div id="editFloatBackdrop" onclick="closeFloatingEditor()"></div> <div id="editPanel" style="display:none;margin-top:32px;border-top:2px solid #E0E4E8;padding-top:24px;"> <h3 style="font-size:15px;color:var(--primary);margin-bottom:20px;">📝 Content Editor</h3> <!-- Theory Notes Editor --> <div style="margin-bottom:28px;"> <div style="background:#F0F6FC;border:1px solid var(--primary);border-radius:8px 8px 0 0;padding:10px 16px;display:flex;justify-content:space-between;align-items:center;"> <span style="font-weight:700;color:var(--primary);font-size:13px;">📖 Theory Notes (Full Chapter)</span> <div style="display:flex;gap:6px;"> <select id="topicSelector" style="font-size:12px;padding:4px 8px;border:1px solid #CBD5E0;border-radius:6px;" onchange="loadTopicNote(this.value)"> <option value="">-- Select Topic to Edit --</option> <option value="423860">1.1 Definition of HTML</option> <option value="423861">1.2 HTML terminologies</option> <option value="423866">1.3 Creation of HTML file</option> <option value="423869">1.4 HTML core elements</option> <option value="423874">1.5 Integration of HTML core elements to fi…</option> <option value="full">📄 Full Chapter Notes</option> </select> <button type="button" onclick="togglePreviewBtn(this)" id="previewBtn" title="See it as your students will" style="font-size:12px;padding:4px 14px;background:#fff;color:var(--primary);border:1.5px solid var(--primary);border-radius:6px;cursor:pointer;font-weight:700;">👁 Preview</button> <button onclick="saveNotes()" style="font-size:12px;padding:4px 14px;background:var(--primary);color:white;border:none;border-radius:6px;cursor:pointer;margin-left:6px;">💾 Save</button> <button onclick="closeFloatingEditor()" style="font-size:12px;padding:4px 12px;background:#fff;color:#6B7B8D;border:1.5px solid #CBD5E0;border-radius:6px;cursor:pointer;margin-left:6px;">✕ Close</button> </div> </div> <div style="border:1px solid var(--primary);border-top:none;border-radius:0 0 8px 8px;padding:12px;"> <div id="previewNudge" style="display:none;font-size:12.5px;color:#8A6D02; background:#FEF9EE;border-left:3px solid #E0C88A;padding:8px 12px; border-radius:0 6px 6px 0;margin-bottom:10px;"> <strong>Preview before you save</strong> — especially after changing a table or an image, so you see it exactly as your students will. </div> <div id="notesEditorWrap" style="display:none;"> <textarea id="notesEditor"></textarea> <div style="margin-top:8px;font-size:12px;color:#41556B;"> Markdown: <code>##</code> heading, <code>**bold**</code>, <code>![caption](url)</code> image. <span id="notesSaveStatus" style="margin-left:12px;font-weight:600;"></span> </div> </div> <p id="notesEditorPlaceholder" style="color:#888;font-size:12px;text-align:center;padding:20px;">Select a topic above to edit its notes.</p> </div> </div> <!-- Practice Questions --> <div style="margin-bottom:28px;"> <div style="background:#FFF8E1;border:1px solid #E0C88A;border-radius:8px 8px 0 0;padding:10px 16px;display:flex;justify-content:space-between;align-items:center;"> <span style="font-weight:700;color:var(--accent);font-size:13px;">📝 Practice Questions</span> <button onclick="addPracticeQuestion()" style="font-size:12px;padding:4px 12px;background:var(--accent);color:white;border:none;border-radius:6px;cursor:pointer;">+ Add</button> </div> <div id="practiceQsList" style="border:1px solid #E0C88A;border-top:none;border-radius:0 0 8px 8px;padding:12px;min-height:60px;"></div> </div> </div> <script> document.addEventListener('DOMContentLoaded', function () { var list = document.getElementById('cmpChecklist'); if (!list) return; var ticks = list.querySelectorAll('.cmp-tick'); var tally = document.getElementById('cmpTally'); var total = ticks.length; function refresh() { var done = list.querySelectorAll('.cmp-tick[aria-pressed="true"]').length; if (done === 0) { tally.textContent = 'Tick each ability you can genuinely do, just for yourself.'; tally.classList.remove('full'); } else if (done < total) { tally.textContent = done + ' of ' + total + '. Keep going, or decide below.'; tally.classList.remove('full'); } else { tally.textContent = 'You have reviewed all ' + total + '. Now make your honest call below.'; tally.classList.add('full'); } // ticking never pre-selects a declaration — the student always decides deliberately } list.querySelectorAll('.cmp-check').forEach(function (row) { var t = row.querySelector('.cmp-tick'); row.addEventListener('click', function () { var on = t.getAttribute('aria-pressed') === 'true'; t.setAttribute('aria-pressed', on ? 'false' : 'true'); row.classList.toggle('done', !on); refresh(); }); }); }); </script> <script> document.addEventListener('DOMContentLoaded', function () { var form = document.getElementById('cmpForm'); if (!form) return; var labels = { yes: 'Yes, I can do all of this', somewhat: 'Somewhat, I need more practice', no: 'Not yet, I need to go over it again' }; function csrf() { var m = document.cookie.match(/csrftoken=([^;]+)/); return m ? m[1] : (form.querySelector('[name=csrfmiddlewaretoken]') || {}).value; } form.addEventListener('submit', function (e) { e.preventDefault(); var btn = document.activeElement; var level = btn && btn.name === 'level' ? btn.value : null; if (!level) return; var body = 'level=' + encodeURIComponent(level) + '&csrfmiddlewaretoken=' + encodeURIComponent(csrf()); fetch(form.dataset.action, { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest' }, body: body }) .then(function () { // update highlight in place form.querySelectorAll('.cmp-btn').forEach(function (b) { b.classList.toggle('on', b.value === level); }); // update / create the "You said" banner var pad = form.closest('.cmp-pad'); var cur = pad.querySelector('.cmp-current'); if (!cur) { cur = document.createElement('div'); form.parentNode.insertBefore(cur, form); } cur.className = 'cmp-current cmp-' + level; var now = new Date().toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' }); cur.innerHTML = '<span class="cmp-current-label">Saved ✓ — you said</span> ' + labels[level] + ' <span class="cmp-when">· ' + now + '</span>'; // contextual follow-up under the buttons var follow = { yes: 'Nicely done. When you\'re ready, move on to the next chapter below.', somewhat: 'Good honesty. Work through the tools above once more, then come back and update this.', no: 'That\'s an honest place to start. Use the tools above to build up, then return and try again.' }; var fEl = pad.querySelector('.cmp-followup'); if (!fEl) { fEl = document.createElement('p'); fEl.className = 'cmp-followup'; form.parentNode.insertBefore(fEl, form.nextSibling); } fEl.className = 'cmp-followup cmp-follow-' + level; fEl.textContent = follow[level] || ''; }) .catch(function () { form.submit(); }); // fallback: normal post if fetch fails }); }); </script> <section class="cmp" id="competency"> <div class="cmp-head">Am I competent?</div> <div class="cmp-pad"> <!-- 1. THE PROMISE — check yourself against each outcome --> <p class="cmp-lead">At the start of this chapter we promised you would be able to:</p> <ul class="cmp-checklist" id="cmpChecklist"> <li class="cmp-check" data-i="0"> <button type="button" class="cmp-tick" aria-pressed="false"><span></span></button> <span class="cmp-check-text">Use common HTML terms correctly in your web design work.</span> </li> <li class="cmp-check" data-i="1"> <button type="button" class="cmp-tick" aria-pressed="false"><span></span></button> <span class="cmp-check-text">Create a complete and well-structured HTML file from start to finish.</span> </li> <li class="cmp-check" data-i="2"> <button type="button" class="cmp-tick" aria-pressed="false"><span></span></button> <span class="cmp-check-text">Apply the essential HTML elements accurately in your web pages.</span> </li> <li class="cmp-check" data-i="3"> <button type="button" class="cmp-tick" aria-pressed="false"><span></span></button> <span class="cmp-check-text">Integrate these core HTML elements properly within your web documents.</span> </li> </ul> <p class="cmp-tally" id="cmpTally">Tick each one you can genuinely do.</p> <!-- Evidence: best scores across simulations + persisted self-tests --> <!-- 4. DECLARE: the informed self-declaration --> <p class="cmp-ask">So, are you there yet?</p> <p class="cmp-threshold">You're competent when you can confidently do <strong>50% or more</strong> of what this chapter promised.</p> <p class="cmp-priv"><a href="/login/">Sign in</a> to record how you're doing.</p> </div> </section> <style> .cmp { margin: 34px 0 0; border: 1.5px solid var(--primary); border-radius: 14px; background: #fff; overflow: hidden; } /* ---- capstone slots (checklist / test / prove) ---- */ .cmp-checklist { list-style: none; margin: 0 0 8px; padding: 0; display: flex; flex-direction: column; gap: 8px; } .cmp-check { display: flex; align-items: center; gap: 13px; padding: 12px 14px; border: 1.5px solid #E4EAF0; border-radius: 11px; background: #fff; cursor: pointer; transition: border-color .15s, background .15s; } .cmp-check:hover { border-color: #C3D4E4; } .cmp-check.done { border-color: #BFE3CD; background: #F4FAF6; } .cmp-tick { flex: 0 0 auto; width: 26px; height: 26px; border-radius: 8px; border: 2px solid #C3D0DC; background: #fff; cursor: pointer; padding: 0; display: flex; align-items: center; justify-content: center; transition: background .18s, border-color .18s, transform .12s; } .cmp-check:active .cmp-tick { transform: scale(.92); } .cmp-tick span { width: 12px; height: 6.5px; border-left: 2.5px solid #fff; border-bottom: 2.5px solid #fff; transform: rotate(-45deg) translate(0, -1px); opacity: 0; transition: opacity .15s; } .cmp-tick[aria-pressed="true"] { background: #1E8449; border-color: #1E8449; } .cmp-tick[aria-pressed="true"] span { opacity: 1; } .cmp-check-text { font-size: 14.5px; line-height: 1.45; color: var(--primary); font-weight: 500; } .cmp-check.done .cmp-check-text { color: #1E6E43; } .cmp-tally { font-size: 13px; color: #8C9BAC; font-weight: 600; margin: 10px 2px 20px; } .cmp-tally.full { color: #1E8449; } .cmp-sim-sample { font-size: 12.5px; line-height: 1.5; color: var(--accent); background: #FEF9EE; border: 1px solid #E0C88A; border-radius: 8px; padding: 9px 12px; margin: 0 0 10px; } .cmp-slot { border: 1px solid #E1E7EE; border-radius: 12px; margin: 0 0 14px; overflow: hidden; background: #FbFCFD; } .cmp-slot-head { padding: 13px 16px; font-size: 14px; font-weight: 700; color: var(--primary); cursor: pointer; list-style: none; display: flex; align-items: center; gap: 9px; } .cmp-slot-head.static { cursor: default; } .cmp-slot-head::-webkit-details-marker { display: none; } .cmp-slot-icon { flex: 0 0 auto; width: 22px; height: 22px; border-radius: 50%; background: #EBF2FA; color: var(--primary); font-size: 12px; font-weight: 800; display: flex; align-items: center; justify-content: center; } .cmp-slot-icon.prove { background: #E9F6EE; color: #1E8449; } .cmp-slot-count { margin-left: auto; font-size: 11px; font-weight: 600; color: #8C9BAC; } .cmp-slot-body { padding: 4px 16px 16px; } .cmp-qs { margin: 0; padding-left: 20px; display: flex; flex-direction: column; gap: 9px; } .cmp-q { font-size: 13.5px; line-height: 1.5; color: #41556B; } .cmp-q-type { display: inline-block; font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: #6B7B8D; background: #EBF2FA; border-radius: 4px; padding: 1px 6px; margin-right: 7px; vertical-align: middle; } .cmp-answers { margin-top: 12px; border-top: 1px solid #EDF1F5; padding-top: 10px; } .cmp-answers summary { font-size: 13px; font-weight: 700; color: var(--primary); cursor: pointer; } .cmp-answers-body { font-size: 13.5px; line-height: 1.65; color: #41556B; margin-top: 10px; } .cmp-prove { background: #F5FBF7; border-color: #CDE9D8; } .cmp-evidence { border: 1.5px solid #E4EAF0; border-radius: 12px; padding: 14px 16px; margin: 0 0 16px; background: #FbFCFD; } .cmp-evidence-title { font-size: 12px; text-transform: uppercase; letter-spacing: .06em; color: #6B7B8D; font-weight: 700; margin-bottom: 12px; } .cmp-ev-row { display: flex; align-items: center; gap: 12px; margin-bottom: 9px; } .cmp-ev-label { flex: 0 0 42%; font-size: 13.5px; color: var(--primary); font-weight: 500; } .cmp-ev-bar { flex: 1; height: 8px; background: #E9EDF2; border-radius: 999px; overflow: hidden; } .cmp-ev-bar i { display: block; height: 100%; background: #1E8449; border-radius: 999px; } .cmp-ev-val { flex: 0 0 auto; font-size: 12.5px; font-weight: 700; color: #1E8449; min-width: 48px; text-align: right; } .cmp-ev-note { font-size: 12px; color: #8C9BAC; margin: 8px 0 0; line-height: 1.5; } .cmp-sim-best { font-size: 12.5px; font-weight: 700; color: #1E8449; margin: 2px 0 10px; } .cmp-sim-launch { display: flex; align-items: center; justify-content: space-between; gap: 10px; text-decoration: none; background: #1E8449; color: #fff; border-radius: 10px; padding: 14px 18px; font-size: 15px; font-weight: 700; margin-bottom: 8px; min-height: 50px; } .cmp-sim-launch:hover { background: #196B3A; } .cmp-sim-go { font-size: 13px; opacity: .9; white-space: nowrap; } .cmp-sim-soon { font-size: 13.5px; color: #6B7B8D; margin: 4px 0; } .ct-bar { scroll-margin-top: 80px; } .cmp-check-first { background: #FbFCFD; } .cmp-check-lead { font-size: 13.5px; color: #41556B; margin: 0 0 10px; } .cmp-tools { display: flex; flex-wrap: wrap; gap: 8px; } .cmp-tool { display: inline-block; font-size: 13px; font-weight: 600; color: var(--primary); background: #EBF2FA; border: 1px solid #D3E1F0; border-radius: 999px; padding: 7px 14px; text-decoration: none; } .cmp-tool:hover { background: #DCE9F6; border-color: #B9D0E8; } .cmp-threshold { font-size: 13px; color: #41556B; margin: 2px 0 12px; } .cmp-followup { font-size: 13.5px; line-height: 1.5; border-radius: 10px; padding: 11px 14px; margin: 12px 0 0; font-weight: 500; } .cmp-follow-yes { background: #F0F6F2; color: #1E6E43; border: 1px solid #C5E4D2; } .cmp-follow-somewhat { background: #FEF9EE; color: var(--accent); border: 1px solid #E0C88A; } .cmp-follow-no { background: #FDF3F2; color: #9C3A32; border: 1px solid #F1CFCB; } .cmp-head { padding: 14px 22px; background: var(--primary); color: #fff; font-size: 15px; font-weight: 700; letter-spacing: .04em; } .cmp-pad { padding: 20px 22px 22px; } .cmp-lead { margin: 0 0 12px; font-size: 14px; color: #6B7B8D; } .cmp-list { margin: 0 0 18px; padding-left: 0; list-style: none; } .cmp-list li { position: relative; padding: 7px 0 7px 30px; font-size: 15px; line-height: 1.6; color: var(--primary); } .cmp-list li::before { content: '\2713'; position: absolute; left: 6px; top: 7px; color: #2E86C1; font-weight: 700; } .cmp-fallback { margin-bottom: 18px; font-size: 15px; line-height: 1.7; color: var(--primary); } .cmp-ask { margin: 0 0 14px; font-size: 16px; font-weight: 700; color: var(--primary); } .cmp-current { margin: 0 0 14px; padding: 11px 14px; border-radius: 8px; font-size: 14px; font-weight: 600; color: var(--primary); background: #F4F7FA; border-left: 3px solid #8C9BAC; } .cmp-current-label { font-size: 10px; text-transform: uppercase; letter-spacing: 1.4px; color: #8C9BAC; font-weight: 700; margin-right: 6px; } .cmp-when { font-weight: 400; color: #8C9BAC; font-size: 12px; } .cmp-yes.on, .cmp-current.cmp-yes { border-color: #1E8449; } .cmp-current.cmp-yes { background: #F0F6F2; border-left-color: #1E8449; } .cmp-current.cmp-somewhat { background: #FDF6E8; border-left-color: var(--accent); } .cmp-current.cmp-no { background: #FBEEEC; border-left-color: #943126; } .cmp-form { display: flex; gap: 10px; flex-wrap: wrap; margin: 0; } /* phone-first: wide tables scroll sideways within the column, not the whole page */ /* A real table on desktop so it fills the column. display:block was applied at every width for phone side-scrolling, but it stops the element behaving as a table — it shrinks to its content and leaves dead space on the right. Scrolling is phone-only. */ .notes-body table { display: table; width: 100%; max-width: 100%; border-collapse: collapse; } @media (max-width: 820px) { .notes-body table { display: block; overflow-x: auto; -webkit-overflow-scrolling: touch; font-size: 13px; } .notes-body table th, .notes-body table td { white-space: nowrap; padding: 8px 10px; } .notes-body img, .notes-body .svg-diagram, .notes-body pre { max-width: 100%; } .notes-body pre { overflow-x: auto; } } .cmp-btn { flex: 1; min-width: 165px; padding: 14px 16px; text-align: left; cursor: pointer; font-family: inherit; font-size: 15px; font-weight: 700; color: var(--primary); background: #fff; border: 1.5px solid #E0E6ED; border-radius: 10px; transition: border-color .15s, background .15s; } .cmp-btn span { display: block; margin-top: 3px; font-size: 12px; font-weight: 400; color: #6B7B8D; } .cmp-yes:hover, .cmp-yes.on { border-color: #1E8449; background: #F4FAF6; } .cmp-somewhat:hover, .cmp-somewhat.on { border-color: var(--accent); background: #FEFAF3; } .cmp-no:hover, .cmp-no.on { border-color: #943126; background: #FDF7F6; } .cmp-btn.on { box-shadow: inset 0 0 0 2px currentColor; font-weight: 700; } .cmp-yes.on { color: #1E8449; } .cmp-somewhat.on { color: var(--accent); } .cmp-no.on { color: #943126; } .cmp-priv { margin: 14px 0 0; font-size: 12px; color: #8C9BAC; } #competency { scroll-margin-top: 90px; } </style> <div class="chapter-nav"> <span></span> <a href="/chapter/32523/">Ch 2: HTML elements →</a> </div> </div> </div> </div> </div> <!-- Notes Editor Section (inside edit panel, before modal) --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.css"> <script> window.MathJax = { tex: { inlineMath: [['\\(', '\\)']], displayMath: [['$$', '$$'], ['\\[', '\\]']], processEscapes: true }, options: { skipHtmlTags: ['script','noscript','style','textarea','pre','code'] }, svg: { fontCache: 'global' } }; </script> <script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" id="MathJax-script" async></script> <script src="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.js"></script> <!-- Modal --> <div id="editModal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:4100;align-items:center;justify-content:center;"> <div style="background:white;border-radius:12px;padding:24px;width:90%;max-width:640px;max-height:85vh;overflow-y:auto;position:relative;"> <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;"> <h3 id="modalTitle" style="font-size:15px;font-weight:700;color:var(--primary);margin:0;"></h3> <button onclick="closeModal()" style="border:none;background:none;font-size:22px;cursor:pointer;color:#888;line-height:1;">×</button> </div> <div id="modalBody"></div> <div style="display:flex;justify-content:flex-end;gap:8px;margin-top:20px;border-top:1px solid #E0E4E8;padding-top:16px;"> <button onclick="closeModal()" style="padding:8px 20px;border:1px solid #CBD5E0;border-radius:6px;background:white;cursor:pointer;">Cancel</button> <button id="modalSaveBtn" style="padding:8px 20px;border:none;border-radius:6px;background:var(--primary);color:white;cursor:pointer;font-weight:600;">Save</button> </div> </div> </div> <style> .stc-controls { font-size:0; white-space:nowrap; } .stc-btn { font-family:inherit; font-size:11px; font-weight:600; line-height:1; padding:3px 8px; border-radius:6px; cursor:pointer; margin-left:4px; vertical-align:middle; border:1px solid; } .stc-flag { background:#FDF3F1; color:#B23A2E; border-color:#E9C4BE; } .stc-flag:hover { background:#F9E3DF; } .stc-edit { background:#EEF3F8; color:var(--primary); border-color:#C2D5E8; } .stc-edit:hover { background:#DFE9F3; } @media print { .stc-controls { display:none !important; } } </style> <script> const CHAPTER_ID = 32522; const CAN_EDIT = false; // Deep-link: scroll to the actual CONTENT heading for a topic (not the sidebar item) (function(){ function scrollToHashTopic(){ var m = (location.hash||'').match(/sidebar-topic-(\d+)/); if(!m) return; var tid = m[1]; // find the injected control button carrying this topic id, then its heading var btn = document.querySelector('.notes-body [onclick*="stcFlag('+tid+',"], .notes-body [onclick*="stcEdit('+tid+',"]'); var target = null; if(btn){ target = btn.closest('h2,h3') || btn.parentElement; } if(!target){ // fallback: the sidebar item target = document.getElementById('sidebar-topic-'+tid); } if(target && target.scrollIntoView){ target.scrollIntoView({behavior:'smooth', block:'center'}); // brief highlight so the spot is obvious var h = target.closest('h2,h3') || target; if(h){ h.style.transition='background .3s'; var o=h.style.background; h.style.background='#FBF3DC'; setTimeout(function(){ h.style.background=o||''; }, 1800); } } } if(document.readyState!=='loading') setTimeout(scrollToHashTopic, 400); else document.addEventListener('DOMContentLoaded', function(){ setTimeout(scrollToHashTopic, 400); }); })(); // ---- Per-heading flag/edit controls ---- // Flag: opens the flag modal pre-filled with this subtopic's location. window._stcFlagTopicId = null; window._stcFlagNumber = null; window.stcFlag = function(topicId, number){ window._stcFlagTopicId = topicId; window._stcFlagNumber = number; if (typeof openFlagModal === 'function') { openFlagModal(); setTimeout(function(){ var loc = document.getElementById('flagLoc'); if (loc) loc.value = number; // ALWAYS set to THIS heading (was: only if empty) var d = document.getElementById('flagDesc'); if (d) { d.value = ''; d.focus(); } }, 30); } }; // Edit: opens THIS topic's note in the existing per-topic editor (reuses edit_topic_note). window.closeFloatingEditor = function(){ var _pp = document.getElementById('practicalsPanel'); if (_pp) { _pp.classList.remove('edit-floating'); _pp.style.display = 'none'; } var panel = document.getElementById('editPanel'); if(panel){ panel.classList.remove('edit-floating'); panel.style.display='none'; } var bd = document.getElementById('editFloatBackdrop'); if(bd) bd.classList.remove('on'); var pp = document.getElementById('practicalsPanel'); if(pp){ pp.classList.remove('edit-floating'); pp.style.display='none'; } // exit edit mode so per-heading edit controls + image Remove buttons hide again if (typeof editMode !== 'undefined' && editMode && typeof toggleEditMode === 'function') { toggleEditMode(); } // hard-hide any image delete buttons document.body.classList.remove('edit-mode-on'); document.body.classList.remove('note-editing'); }; // Renders the EasyMDE preview, then runs MathJax over it so an editor can VERIFY // formulas before saving. Display only -- the stored markdown is never modified. window.mdeMathPreview = function(plainText, preview) { // Markdown eats the backslash in \( ... \), so inline maths loses its delimiters // before MathJax ever sees it. Lift every maths span out to a plain-text placeholder, // run markdown, then put the spans back untouched. var store = []; var guarded = plainText.replace( /\$\$[\s\S]*?\$\$|\\\[[\s\S]*?\\\]|\\\([\s\S]*?\\\)/g, function (m) { store.push(m); return 'MJXMATH' + (store.length - 1) + 'HTAMJXM'; }); var html; try { html = this.parent.markdown(guarded); } catch (e) { html = guarded; } html = html.replace(/MJXMATH(\d+)HTAMJXM/g, function (_, i) { return store[Number(i)] || ''; }); preview.innerHTML = html; // EasyMDE writes the RETURNED html into the pane after this call, which would wipe // any typesetting done here. So typeset on the next tick, once the DOM has settled. if (window.MathJax && MathJax.typesetPromise) { setTimeout(function(){ var pane = document.querySelector('.editor-preview-active') || document.querySelector('.editor-preview-active-side') || preview; MathJax.typesetPromise([pane]).catch(function(){}); }, 60); } return html; }; // Toolbar button: wrap the selection (or insert a stub) in display-math delimiters. window.mdeMathButton = { name: 'math', className: 'fa', title: 'Insert formula ($$...$$)', action: function(editor) { var cm = editor.codemirror; var sel = cm.getSelection(); if (sel) { cm.replaceSelection('$$\n' + sel + '\n$$'); } else { var pos = cm.getCursor(); cm.replaceSelection('$$\n\n$$'); cm.setCursor({line: pos.line + 1, ch: 0}); } cm.focus(); } }; window.closeAllPanels = function(){ var ep = document.getElementById('editPanel'); if (ep) { ep.classList.remove('edit-floating'); ep.style.display = 'none'; } var pp = document.getElementById('practicalsPanel'); if (pp) { pp.classList.remove('edit-floating'); pp.style.display = 'none'; } var bd = document.getElementById('editFloatBackdrop'); if (bd) bd.classList.remove('on'); document.body.classList.remove('note-editing'); }; window.stcEditPracticals = function(){ closeAllPanels(); // Practicals are row-backed: open the editor panel and scroll to the practicals list. if (typeof editMode !== 'undefined' && !editMode && typeof toggleEditMode === 'function') { toggleEditMode(); } var pp = document.getElementById('practicalsPanel'); if (pp) { pp.style.display = 'block'; pp.classList.add('edit-floating'); var bd = document.getElementById('editFloatBackdrop'); if (bd) bd.classList.add('on'); } if (typeof loadPracticals === 'function') loadPracticals(); }; window.stcEditSection = function(key){ // Chapter-level section (Self-Assessment / Exam Questions). Loads the blob section // through the section endpoint into the SAME floating editor used for topic notes. closeAllPanels(); var wrap = document.getElementById('notesEditorWrap'); var ph = document.getElementById('notesEditorPlaceholder'); var st = document.getElementById('notesSaveStatus'); var panel = document.getElementById('editPanel'); if (typeof editMode !== 'undefined' && !editMode && typeof toggleEditMode === 'function') { toggleEditMode(); } var sel = document.getElementById('topicSelector'); if (sel) { sel.value = ''; } if (ph) ph.style.display = 'none'; if (wrap) wrap.style.display = 'block'; if (st) { st.textContent = 'Loading...'; st.style.color = '#888'; } if (panel) { panel.style.display = 'block'; panel.classList.add('edit-floating'); var bd = document.getElementById('editFloatBackdrop'); if (bd) bd.classList.add('on'); } document.body.classList.add('note-editing'); fetch('/api/chapter/' + CHAPTER_ID + '/section/' + key + '/') .then(function(r){ return r.json().then(function(d){ return {ok:r.ok, d:d}; }); }) .then(function(res){ if (!res.ok) { alert(res.d.error || 'Could not open this section.'); if (typeof closeFloatingEditor === 'function') closeFloatingEditor(); return; } currentNoteType = 'section'; currentSectionKey = key; currentTopicId = null; if (mde) { mde.toTextArea(); mde = null; } var w = document.getElementById('notesEditorWrap'); var old = w.querySelector('textarea'); if (old) old.remove(); var ta = document.createElement('textarea'); ta.id = 'notesEditor'; ta.value = res.d.content_md || ''; w.insertBefore(ta, w.firstChild); mde = new EasyMDE({ element: ta, spellChecker: false, autosave: { enabled: false }, previewRender: window.mdeMathPreview, toolbar: ['bold','italic','heading','|','quote','unordered-list','ordered-list','|', 'link','table','|','preview','guide'] }); if (st) { st.textContent = 'Editing: ' + (res.d.label || key); st.style.color = 'var(--primary)'; } if (typeof loadPracticeQuestions === 'function') loadPracticeQuestions(); }) .catch(function(){ alert('Network error loading section.'); if (typeof closeFloatingEditor === 'function') closeFloatingEditor(); }); }; window.stcEdit = function(topicId, number){ closeAllPanels(); // 1. Make sure edit mode is ON so the editor panel is visible if (typeof editMode !== 'undefined' && !editMode && typeof toggleEditMode === 'function') { toggleEditMode(); } // 2. Select this topic in the topic selector and load its note into the editor var sel = document.getElementById('topicSelector'); if (sel) { sel.value = topicId; if (typeof loadTopicNote === 'function') { loadTopicNote(topicId); } } // 3. Show the editor as a fixed overlay near the top of the viewport (like the flag // editor) so it opens IN PLACE — no jarring jump to the bottom of the page. var panel = document.getElementById('editPanel'); if (panel) { panel.style.display = 'block'; panel.classList.add('edit-floating'); var bd = document.getElementById('editFloatBackdrop'); if(bd) bd.classList.add('on'); // focus the editor without moving the page setTimeout(function(){ var cm = panel.querySelector('.CodeMirror'); if (cm && cm.CodeMirror) { cm.CodeMirror.focus(); } }, 200); } }; // ---- In-context flag modal (verified trainers) ---- function _flagCsrf(){ var m=document.cookie.match(/csrftoken=([^;]+)/); return m?m[1]:''; } window.openFlagModal = function(){ // REBUILD every time. Reusing the existing overlay meant that after one // successful flag - which REPLACES the overlay's innerHTML with the success // card - every later Flag button on the page reopened that stale card. The // trainer read "Flag submitted" and nothing was posted at all. It also left // the location label showing the PREVIOUS topic, so a flag that did submit // could be recorded against the wrong section. var ex = document.getElementById('flagModalOverlay'); if(ex){ ex.remove(); } if(window._flagMde){ try{ window._flagMde.toTextArea(); }catch(e){} window._flagMde=null; } if(!document.getElementById('flagModalStyle')){ var st=document.createElement('style'); st.id='flagModalStyle'; st.textContent= '#flagModalOverlay{position:fixed;inset:0;background:rgba(20,32,48,.5);z-index:99999;display:flex;align-items:center;justify-content:center;padding:16px;overflow-y:auto;box-sizing:border-box;}' + '#flagModalCard{background:#fff;border-radius:16px;width:100%;max-width:520px;box-sizing:border-box;box-shadow:0 20px 60px rgba(0,0,0,.3);max-height:90vh;display:flex;flex-direction:column;overflow:hidden;}' + '#flagModalCard *{box-sizing:border-box;}' + '.fm-head{padding:16px 20px 10px;cursor:move;flex:0 0 auto;border-bottom:1px solid #F0F3F6;}' + '.fm-title-row{display:flex;align-items:center;justify-content:space-between;gap:10px;}' + '.fm-title{font-size:17px;font-weight:800;color:var(--primary);}' + '.fm-x{border:none;background:#F0F3F6;color:#41556B;width:32px;height:32px;border-radius:8px;font-size:19px;line-height:1;cursor:pointer;flex:0 0 auto;}' + '.fm-intro{font-size:12.5px;color:#6B7B8D;margin-top:5px;line-height:1.5;}' + '.fm-draghint{font-size:11px;color:#8C9BAC;margin-top:6px;}' + '.fm-body{padding:14px 20px;overflow-y:auto;flex:1 1 auto;-webkit-overflow-scrolling:touch;}' + '.fm-body .editor-toolbar{position:sticky;top:0;z-index:5;background:#fff;}' + '.fm-body .CodeMirror{min-height:120px;}' + '.fm-body .CodeMirror-scroll{max-height:38vh;}' + '.fm-body label{display:block;font-size:12px;font-weight:700;color:var(--primary);margin-bottom:5px;}' + '.fm-body input[type=text],.fm-body textarea{width:100%;padding:12px;border:1.5px solid #CBD5E0;border-radius:9px;font:inherit;font-size:15px;margin-bottom:14px;}' + '.fm-body textarea{resize:vertical;min-height:96px;}' + '.fm-check{display:flex;align-items:center;gap:10px;font-size:14px;color:#41556B;margin-bottom:2px;}' + '.fm-check input{width:20px;height:20px;flex:0 0 auto;margin:0;}' + '.fm-foot{padding:12px 20px;border-top:1px solid #F0F3F6;display:flex;gap:10px;justify-content:flex-end;flex:0 0 auto;background:#fff;}' + '.fm-btn{padding:12px 20px;border-radius:9px;font:inherit;font-weight:700;cursor:pointer;}' + '.fm-cancel{border:1.5px solid #CBD5E0;background:#fff;color:#41556B;font-weight:600;}' + '.fm-submit{border:none;background:#B23A2E;color:#fff;}' + '@media (max-width:640px){#flagModalOverlay{align-items:flex-start;padding:8px;}#flagModalCard{max-width:none;max-height:94vh;}.fm-head{cursor:default;}.fm-draghint{display:none;}}'; document.head.appendChild(st); } var o=document.createElement('div'); o.id='flagModalOverlay'; o.innerHTML= '<div id="flagModalCard">' + '<div class="fm-head" id="flagModalHeader">' + '<div class="fm-title-row"><div class="fm-title">Flag content</div>' + '<button type="button" class="fm-x" onclick="closeFlagModal()" aria-label="Close">×</button></div>' + '<div class="fm-intro">Report a mistake, omission, shallow content, or suggest an addition or figure. Accepted flags earn credits.</div>' + '<div class="fm-intro" style="background:#F0F9F2;border:1px solid #A9E3C4;border-radius:9px;padding:9px 12px;color:#1A5C36;">' + '<strong>Have the diagram, photo or rewritten section already?</strong> Add it below. ' + 'A suggestion we can use as it is gets accepted faster.</div>' + '<div class="fm-draghint">✋ Drag this bar to move — peek at the content behind.</div>' + '</div>' + '<div class="fm-body">' + '<label>Flagging</label>' + '<div id="flagLoc" style="background:#F4F7FA;color:var(--primary);font-weight:600;padding:10px 12px;border:1px solid #CBD5E0;border-radius:8px;font-size:13.5px;line-height:1.45;word-break:break-word;"></div>' + '<label>What needs attention?</label>' + '<textarea id="flagDesc" placeholder="Describe the mistake, omission, or suggestion. You can format text, add images/diagrams, or attach a Word/PDF file below."></textarea>' + '<div id="flagAttachRow" style="margin-top:6px;display:flex;align-items:center;gap:10px;flex-wrap:wrap;">' + '<button type="button" onclick="document.getElementById(\'flagFileInput\').click()" style="background:#EEF3F8;color:var(--primary);border:1.5px solid #C2D5E8;font:inherit;font-weight:600;font-size:13px;padding:8px 14px;border-radius:8px;cursor:pointer;">📎 Attach a file (Word / PDF)</button>' + '<input type="file" id="flagFileInput" accept=".doc,.docx,.pdf,.odt,.rtf,.txt,.ppt,.pptx,.xls,.xlsx,image/*" style="display:none;" onchange="uploadFlagAttachment(this)">' + '<span id="flagAttachName" style="font-size:13px;color:#1E8449;font-weight:600;"></span>' + '</div>' + '</div>' + '<div class="fm-foot">' + '<button type="button" class="fm-btn fm-cancel" onclick="closeFlagModal()">Cancel</button>' + '<button type="button" class="fm-btn fm-submit" id="flagSubmitBtn" onclick="submitFlag()">Submit flag</button>' + '</div>' + '</div>'; document.body.appendChild(o); // Mount a lightweight markdown editor on the flag description (toolbar kept minimal for phones) // A LIST since 16 Sep. One slot meant a second upload silently replaced the // first, and nothing could be removed once chosen. window._flagAttach = []; var _isPhoneEd = window.matchMedia('(max-width:640px)').matches; // hidden image input (used by both the EasyMDE toolbar and the phone image button) if (!document.getElementById('flagImgInput')) { var imgIn = document.createElement('input'); imgIn.type='file'; imgIn.id='flagImgInput'; imgIn.accept='image/*'; imgIn.style.display='none'; imgIn.onchange = function(){ uploadFlagImage(this); }; document.body.appendChild(imgIn); } // Show WHAT is being flagged. This used to sit inside the block above, which // only runs the FIRST time the hidden image input is created - so on a second // flag the label still named the previous topic. var locEl = document.getElementById('flagLoc'); if (locEl) { locEl.textContent = window._flagLocLabel || (window._stcFlagNumber ? ('Topic ' + window._stcFlagNumber) : '') || 'Whole page'; } // Desktop: rich EasyMDE editor. Phone: reliable plain textarea + a simple image button // (EasyMDE's CodeMirror is finicky in a small modal, so we keep phone lightweight). if (window.EasyMDE && !_isPhoneEd) { try { if (window._flagMde) { try { window._flagMde.toTextArea(); } catch(e){} window._flagMde = null; } var _imgBtn = {name:'image', className:'fa fa-picture-o', title:'Upload image/diagram', action: function(ed){ document.getElementById('flagImgInput').click(); }}; window._flagMde = new EasyMDE({ element: document.getElementById('flagDesc'), spellChecker: false, status: false, minHeight: '200px', placeholder: 'Describe your suggestion. Format text, add an image/diagram, or attach a file below.', toolbar: ['bold','italic','heading','|','unordered-list','ordered-list','quote','|', _imgBtn,'link','table','|','preview','guide'] }); } catch(e) { window._flagMde = null; } } else { // phone: no EasyMDE — plain textarea stays. Add a simple "Add image" button. window._flagMde = null; if (!document.getElementById('flagPhoneImgBtn')) { var pd = document.getElementById('flagDesc'); if (pd && pd.parentNode) { var b = document.createElement('button'); b.type='button'; b.id='flagPhoneImgBtn'; b.textContent='🖼 Add image/diagram'; b.style.cssText='margin-top:8px;background:#EEF3F8;color:var(--primary);border:1.5px solid #C2D5E8;font:inherit;font-weight:600;font-size:13px;padding:9px 14px;border-radius:8px;cursor:pointer;'; b.onclick=function(){ document.getElementById('flagImgInput').click(); }; pd.parentNode.insertBefore(b, pd.nextSibling); } } } o.addEventListener('click', function(e){ if(e.target===o) closeFlagModal(); }); if(!window.matchMedia('(max-width:640px)').matches){ var card=document.getElementById('flagModalCard'), handle=document.getElementById('flagModalHeader'); var dragging=false,sx=0,sy=0,ox=0,oy=0; function down(e){ if(e.target.tagName==='BUTTON')return; dragging=true; var pt=e.touches?e.touches[0]:e; sx=pt.clientX; sy=pt.clientY; var r=card.getBoundingClientRect(); ox=r.left; oy=r.top; o.style.alignItems='flex-start'; o.style.justifyContent='flex-start'; card.style.transform='translate('+ox+'px,'+oy+'px)'; e.preventDefault(); } function move(e){ if(!dragging)return; var pt=e.touches?e.touches[0]:e; card.style.transform='translate('+(ox+pt.clientX-sx)+'px,'+(oy+pt.clientY-sy)+'px)'; } function up(){ dragging=false; } handle.addEventListener('mousedown',down); document.addEventListener('mousemove',move); document.addEventListener('mouseup',up); handle.addEventListener('touchstart',down,{passive:false}); document.addEventListener('touchmove',move,{passive:false}); document.addEventListener('touchend',up); } }; window.closeFlagModal = function(){ var o=document.getElementById('flagModalOverlay'); if(o) o.style.display='none'; }; window.uploadFlagImage = function(input){ if(!input.files||!input.files[0]) return; var fd=new FormData(); fd.append('image', input.files[0]); fd.append('for_flag','1'); fetch('/api/upload-image/', {method:'POST', credentials:'same-origin', headers:{'X-CSRFToken':_flagCsrf()}, body:fd}) .then(function(r){return r.json();}).then(function(d){ if(d.url){ var cap = (prompt('Briefly describe this image (what it shows, and the source if any):','') || 'diagram').trim(); cap = cap.replace(/[\[\]]/g,''); var md = '\n![' + cap + '](' + d.url + ')\n'; if(window._flagMde){ window._flagMde.codemirror.replaceSelection(md); } else { var ta=document.getElementById('flagDesc'); if(ta){ ta.value += md; } } } else { alert(d.error||'Image upload failed.'); } input.value=''; }).catch(function(){ alert('Image upload failed.'); input.value=''; }); }; window.uploadFlagAttachment = function(input){ if(!input.files||!input.files[0]) return; var nm=document.getElementById('flagAttachName'); nm.textContent='Uploading…'; var fd=new FormData(); fd.append('file', input.files[0]); fetch('/flag/upload-attachment/', {method:'POST', credentials:'same-origin', headers:{'X-CSRFToken':_flagCsrf()}, body:fd}) .then(function(r){return r.json();}).then(function(d){ if(d.url){ window._flagAttach.push({url:d.url,name:d.name}); window._drawFlagAttach(); input.value=''; } else { nm.textContent=''; alert(d.error||'File upload failed.'); } }).catch(function(e){ nm.textContent=''; console.error('flag attach failed:', e); alert('File upload failed: ' + (e && e.message ? e.message : e)); }); }; window._drawFlagAttach = function(){ var box=document.getElementById('flagAttachName'); if(!box) return; var a=window._flagAttach||[]; if(!a.length){ box.innerHTML=''; return; } box.innerHTML = a.map(function(f,i){ return '<span style="display:inline-flex;align-items:center;gap:6px;background:#EEF7F0;' + 'border:1px solid #A9E3C4;border-radius:999px;padding:4px 6px 4px 11px;margin:3px 6px 3px 0;' + 'font-size:12.5px;color:#1A5C36;font-weight:600;">📎 ' + f.name + '<button type="button" onclick="_rmFlagAttach(' + i + ')" title="Remove" ' + 'style="background:none;border:none;color:#1A5C36;font-size:15px;line-height:1;' + 'cursor:pointer;padding:0 3px;">×</button></span>'; }).join(''); }; window._rmFlagAttach = function(i){ window._flagAttach.splice(i,1); window._drawFlagAttach(); }; window.submitFlag = function(){ var desc = (window._flagMde ? window._flagMde.value() : (document.getElementById('flagDesc').value||'')).trim(); var atts = window._flagAttach||[]; if(!desc && !atts.length){ alert('Please describe your suggestion or attach a file.'); return; } var btn=document.getElementById('flagSubmitBtn'); btn.disabled=true; btn.textContent='Submitting…'; fetch(window._flagTarget || ('/chapter/' + (typeof CHAPTER_ID !== 'undefined' ? CHAPTER_ID : '') + '/flag/'), { method:'POST', credentials:'same-origin', headers:{'Content-Type':'application/json','X-CSRFToken':_flagCsrf()}, body: JSON.stringify({location:(document.getElementById('flagLoc')||{}).textContent||'', description:desc, safety_critical:false, topic_id: window._stcFlagTopicId, attachments:atts}) }).then(function(r){ return r.json(); }).then(function(d){ if(d.ok){ var o=document.getElementById('flagModalOverlay'); o.innerHTML='<div style="background:#fff;border-radius:16px;max-width:420px;padding:28px;text-align:center;"><div style="font-size:32px;">✅</div><div style="font-size:16px;font-weight:700;color:var(--primary);margin:10px 0 6px;">Flag submitted</div><div style="font-size:13.5px;color:#6B7B8D;line-height:1.5;">Thank you — our team will review it. If accepted, you\'ll earn credits you can spend on downloads and customisation requests.</div><button onclick="closeFlagModal()" style="margin-top:18px;padding:10px 22px;border-radius:9px;border:none;background:var(--primary);color:#fff;font:inherit;font-weight:700;cursor:pointer;">Done</button></div>'; } else { alert(d.error||'Could not submit.'); btn.disabled=false; btn.textContent='Submit flag'; } }).catch(function(){ alert('Network error.'); btn.disabled=false; btn.textContent='Submit flag'; }); }; const CSRF = 'B3BDF9SXnZoTw1o81KZAw4FMw9MoQqTUF6md9bSuJoQ18Atlmtsan5SAyxMhTWt0'; let editMode = false; let currentSectionKey = null; function toggleEditMode() { editMode = !editMode; const btn = document.getElementById('editModeBtn'); const panel = document.getElementById('editPanel'); const addBtn = document.getElementById('addTopicBtn'); document.querySelectorAll('.edit-controls').forEach(el => el.style.display = editMode ? 'flex' : 'none'); document.body.classList.toggle('edit-mode-on', editMode); if (editMode) { btn.textContent = '✅ Done'; btn.style.background='#E8F5E9'; btn.style.color='#1E8449'; btn.style.borderColor='#1E8449'; panel.style.display = 'block'; addBtn.style.display = 'block'; loadAll(); } else { btn.textContent = '✏️ Edit'; btn.style.background='white'; btn.style.color='#6B7B8D'; btn.style.borderColor='#CBD5E0'; panel.style.display = 'none'; addBtn.style.display = 'none'; document.body.classList.remove('note-editing'); } } async function api(url, method='GET', body=null) { const opts = {method, headers:{'X-CSRFToken':CSRF,'Content-Type':'application/json'}}; if (body) opts.body = JSON.stringify(body); const r = await fetch(url, opts); return r.json(); } function toast(msg, ok=true) { const t = document.createElement('div'); t.textContent = msg; t.style.cssText = `position:fixed;bottom:24px;right:24px;padding:10px 18px;border-radius:8px;background:${ok?'#1E8449':'#DC2626'};color:white;font-size:13px;font-weight:600;z-index:9999;box-shadow:0 4px 12px rgba(0,0,0,0.15);`; document.body.appendChild(t); setTimeout(()=>t.remove(),3000); } function openModal(title, bodyHtml, saveFn) { document.getElementById('modalTitle').textContent = title; document.getElementById('modalBody').innerHTML = bodyHtml; document.getElementById('modalSaveBtn').onclick = saveFn; document.getElementById('editModal').style.display = 'flex'; } function closeModal() { document.getElementById('editModal').style.display = 'none'; } function inp(id, label, val='', type='text') { return `<div style="margin-bottom:12px;"><label style="font-size:12px;color:#6B7B8D;font-weight:600;display:block;margin-bottom:4px;">${label}</label><input id="${id}" type="${type}" value="${val}" style="width:100%;padding:8px 10px;border:1px solid #CBD5E0;border-radius:6px;font-size:13px;"></div>`; } function ta(id, label, val='', rows=3) { return `<div style="margin-bottom:12px;"><label style="font-size:12px;color:#6B7B8D;font-weight:600;display:block;margin-bottom:4px;">${label}</label><textarea id="${id}" rows="${rows}" style="width:100%;padding:8px 10px;border:1px solid #CBD5E0;border-radius:6px;font-size:13px;resize:vertical;">${val}</textarea></div>`; } function sel(id, label, options, selected='') { const opts = options.map(o=>Array.isArray(o)?`<option value="${o[0]}" ${selected===o[0]?'selected':''}>${o[1]}</option>`:`<option ${selected===o?'selected':''}>${o}</option>`).join(''); return `<div style="margin-bottom:12px;"><label style="font-size:12px;color:#6B7B8D;font-weight:600;display:block;margin-bottom:4px;">${label}</label><select id="${id}" style="width:100%;padding:8px 10px;border:1px solid #CBD5E0;border-radius:6px;font-size:13px;">${opts}</select></div>`; } function g(id) { const el=document.getElementById(id); return el?el.value:''; } // Self-Assessment and Exam Questions are edited via their own per-heading Edit // buttons (blob-backed, the source the page actually renders). The row-based // loaders below are retained but no longer mounted in this panel. function loadAll() { loadPracticeQuestions(); } function card(content, borderColor='#CBD5E0', bg='white') { return `<div style="border:1px solid ${borderColor};border-radius:6px;padding:10px 12px;margin-bottom:8px;background:${bg};">${content}</div>`; } function editDelBtns(editFn, delFn) { return `<div style="display:flex;gap:4px;flex-shrink:0;"><button onclick="${editFn}" style="padding:4px 8px;font-size:11px;border:1px solid #CBD5E0;border-radius:4px;cursor:pointer;background:white;">✏️</button><button onclick="${delFn}" style="padding:4px 8px;font-size:11px;border:1px solid #FCA5A5;border-radius:4px;cursor:pointer;background:#FEF2F2;color:#DC2626;">🗑</button></div>`; } // ── Practice Questions ── async function loadPracticeQuestions() { const el = document.getElementById('practiceQsList'); if (!el) return; el.innerHTML = '<p style="color:#888;font-size:12px;padding:8px;">Loading...</p>'; const r = await api(`/api/chapter/${CHAPTER_ID}/practice-questions/`); // Practice questions are TOPIC-keyed. When a specific topic is open in the editor, // show only that topic's questions -- otherwise the list is chapter-wide and does // not match the note above it. let items = r.items || []; let scopeNote = ''; if (currentNoteType === 'topic' && currentTopicId) { const tid = String(currentTopicId); const scoped = items.filter(q => String(q.topic_id) === tid); const _sel = document.getElementById('topicSelector'); const _lbl = (_sel && _sel.selectedOptions && _sel.selectedOptions[0]) ? _sel.selectedOptions[0].textContent.trim().split(' ')[0] : ''; scopeNote = `<p style="margin:0 0 8px;font-size:11px;color:var(--accent);">Showing questions for topic ${_lbl} only. Adding a question attaches it to this topic.</p>`; items = scoped; } else if (currentNoteType === 'section') { el.innerHTML = '<p style="color:#888;font-size:12px;text-align:center;padding:16px;">Practice questions belong to topics. Open a topic to edit them.</p>'; return; } if (!items.length) { el.innerHTML = scopeNote + '<p style="color:#888;font-size:12px;text-align:center;padding:16px;">No questions yet.</p>'; return; } el.innerHTML = scopeNote + items.map(q=>card(` <div style="display:flex;justify-content:space-between;gap:8px;"> <div style="flex:1;"><span style="font-size:10px;background:#FFF8E1;padding:1px 7px;border-radius:8px;color:var(--accent);font-weight:600;">${q.question_type} · ${q.marks}m</span> <p style="margin:5px 0 0;font-size:13px;">${q.question_text?.substring(0,120)||''}${(q.question_text?.length||0)>120?'...':''}</p></div> ${editDelBtns(`editPracticeQuestion(${q.id})`,`deletePracticeQuestion(${q.id})`)} </div>`,'#E0C88A','#FFFBF0')).join(''); } function pqForm(q={}) { return sel('mQType','Type',[['short_answer','Short Answer'],['mcq','MCQ'],['structured','Structured'],['scenario','Scenario']],q.question_type||'short_answer') + ta('mQText','Question',q.question_text||'',3) + inp('mQMarks','Marks',q.marks||2,'number') + ta('mQAnswer','Model Answer (optional)',q.correct_answer||'',2); } function addPracticeQuestion() { if (!(currentNoteType === 'topic' && currentTopicId)) { toast('Open a topic first — a question must belong to a topic.', false); return; } openModal('Add Practice Question', pqForm(), async ()=>{ await api(`/api/chapter/${CHAPTER_ID}/practice-questions/`,'POST',{question_type:g('mQType'),question_text:g('mQText'),marks:+g('mQMarks'),correct_answer:g('mQAnswer')}); toast('✅ Question added'); closeModal(); loadPracticeQuestions(); }); } async function editPracticeQuestion(id) { const q = await api(`/api/practice-question/${id}/`); openModal('Edit Practice Question', pqForm(q), async ()=>{ await api(`/api/practice-question/${id}/`,'PUT',{question_type:g('mQType'),question_text:g('mQText'),marks:+g('mQMarks'),correct_answer:g('mQAnswer')}); toast('✅ Saved'); closeModal(); loadPracticeQuestions(); }); } async function deletePracticeQuestion(id) { if(!confirm('Delete this question?'))return; await api(`/api/practice-question/${id}/`,'DELETE'); toast('🗑 Deleted'); loadPracticeQuestions(); } // ── Self Assessment ── async function loadSelfAssessment() { const el = document.getElementById('selfAssessmentList'); el.innerHTML = '<p style="color:#888;font-size:12px;padding:8px;">Loading...</p>'; const r = await api(`/api/chapter/${CHAPTER_ID}/self-assessment/`); if (!r.items?.length) { el.innerHTML='<p style="color:#888;font-size:12px;text-align:center;padding:16px;">No items yet.</p>'; return; } const colors={mcq:'#EBF5FB',short_answer:'#F0FFF4',oral:'#FFF8E1',case_study:'#FFF0F0'}; el.innerHTML = r.items.map(i=>card(` <div style="display:flex;justify-content:space-between;gap:8px;"> <div style="flex:1;"><span style="font-size:10px;background:var(--primary);color:white;padding:1px 7px;border-radius:8px;font-weight:600;">${(i.item_type||'').toUpperCase().replace('_',' ')}</span> <p style="margin:5px 0 0;font-size:13px;">${(i.question_text||'').substring(0,120)}${(i.question_text?.length||0)>120?'...':''}</p></div> ${editDelBtns(`editSelfAssessment(${i.id})`,`deleteSelfAssessment(${i.id})`)} </div>`,`#2E86C1`,colors[i.item_type]||'white')).join(''); } function saForm(i={}) { return sel('mSAType','Type',[['mcq','MCQ'],['short_answer','Short Answer'],['oral','Oral'],['case_study','Case Study']],i.item_type||'mcq') + ta('mSAText','Question',i.question_text||'',4) + ta('mSAAnswer','Answer/Guide',i.correct_answer||'',2) + inp('mSAMarks','Marks',i.marks||1,'number'); } function addSelfAssessment() { openModal('Add Self-Assessment Item', saForm(), async ()=>{ await api(`/api/chapter/${CHAPTER_ID}/self-assessment/`,'POST',{item_type:g('mSAType'),question_text:g('mSAText'),correct_answer:g('mSAAnswer'),marks:+g('mSAMarks')}); toast('✅ Added'); closeModal(); loadSelfAssessment(); }); } async function editSelfAssessment(id) { const i = await api(`/api/self-assessment/${id}/`); openModal('Edit Self-Assessment', saForm(i), async ()=>{ await api(`/api/self-assessment/${id}/`,'PUT',{item_type:g('mSAType'),question_text:g('mSAText'),correct_answer:g('mSAAnswer'),marks:+g('mSAMarks')}); toast('✅ Saved'); closeModal(); loadSelfAssessment(); }); } async function deleteSelfAssessment(id) { if(!confirm('Delete?'))return; await api(`/api/self-assessment/${id}/`,'DELETE'); toast('🗑 Deleted'); loadSelfAssessment(); } // ── Exam Questions ── async function loadExamQuestions() { const el = document.getElementById('examQuestionsList'); el.innerHTML = '<p style="color:#888;font-size:12px;padding:8px;">Loading...</p>'; const r = await api(`/api/chapter/${CHAPTER_ID}/exam-questions/`); if (!r.items?.length) { el.innerHTML='<p style="color:#888;font-size:12px;text-align:center;padding:16px;">No questions yet.</p>'; return; } const secA = r.items.filter(q=>q.section==='A'); const secB = r.items.filter(q=>q.section==='B'); el.innerHTML = `<p style="font-size:11px;font-weight:700;color:var(--primary);margin:0 0 6px;">SECTION A (${secA.length})</p>` + secA.map(q=>card(`<div style="display:flex;justify-content:space-between;gap:8px;"><div style="flex:1;"><span style="font-size:10px;color:#888;">Q${q.order} · ${q.marks}m</span><p style="margin:4px 0 0;font-size:13px;">${(q.question_text||'').substring(0,100)}...</p></div>${editDelBtns(`editExamQuestion(${q.id})`,`deleteExamQuestion(${q.id})`)}</div>`,'#C5D0DE')).join('') + `<p style="font-size:11px;font-weight:700;color:var(--primary);margin:10px 0 6px;">SECTION B (${secB.length})</p>` + secB.map(q=>card(`<div style="display:flex;justify-content:space-between;gap:8px;"><div style="flex:1;"><span style="font-size:10px;color:#888;">Q${q.order}${q.is_compulsory?' · COMPULSORY':''} · ${q.marks}m</span><p style="margin:4px 0 0;font-size:13px;">${(q.question_text||'').substring(0,100)}...</p></div>${editDelBtns(`editExamQuestion(${q.id})`,`deleteExamQuestion(${q.id})`)}</div>`,'#C5D0DE',q.is_compulsory?'#F0F4F8':'white')).join(''); } function eqForm(q={}, section='A') { return sel('mEQSection','Section',[['A','Section A'],['B','Section B']],q.section||section) + ta('mEQText','Question',q.question_text||'',4) + inp('mEQMarks','Marks',q.marks||(section==='A'?4:20),'number') + ta('mEQAnswer','Model Answer',q.model_answer||'',3) + `<div style="margin-bottom:12px;"><label style="font-size:13px;"><input type="checkbox" id="mEQComp" ${q.is_compulsory?'checked':''}> Compulsory (Q11)</label></div>`; } function addExamQuestion(section) { openModal(`Add Section ${section} Question`, eqForm({},section), async ()=>{ await api(`/api/chapter/${CHAPTER_ID}/exam-questions/`,'POST',{section:g('mEQSection'),question_text:g('mEQText'),marks:+g('mEQMarks'),model_answer:g('mEQAnswer'),is_compulsory:document.getElementById('mEQComp').checked}); toast('✅ Added'); closeModal(); loadExamQuestions(); }); } async function editExamQuestion(id) { const q = await api(`/api/exam-question/${id}/`); openModal('Edit Exam Question', eqForm(q), async ()=>{ await api(`/api/exam-question/${id}/`,'PUT',{section:g('mEQSection'),question_text:g('mEQText'),marks:+g('mEQMarks'),model_answer:g('mEQAnswer'),is_compulsory:document.getElementById('mEQComp').checked}); toast('✅ Saved'); closeModal(); loadExamQuestions(); }); } async function deleteExamQuestion(id) { if(!confirm('Delete?'))return; await api(`/api/exam-question/${id}/`,'DELETE'); toast('🗑 Deleted'); loadExamQuestions(); } // ── Practicals ── async function loadPracticals() { const el = document.getElementById('practicalsList'); if (!el) return; el.innerHTML = '<p style="color:#888;font-size:12px;padding:8px;">Loading...</p>'; const r = await api(`/api/chapter/${CHAPTER_ID}/practicals/`); if (!r.items?.length) { el.innerHTML='<p style="color:#888;font-size:12px;text-align:center;padding:16px;">No practicals yet.</p>'; return; } el.innerHTML = r.items.map(p=>card(` <div style="display:flex;justify-content:space-between;gap:8px;"> <div style="flex:1;"><span style="font-size:10px;background:#E8F5E9;color:#1B5E20;padding:1px 7px;border-radius:8px;font-weight:600;">P${p.order} · ${p.time_minutes||'?'}min · ${p.activity_type||''}</span> <p style="margin:5px 0 2px;font-size:13px;font-weight:600;color:#1B5E20;">${p.title||''}</p> <p style="margin:0;font-size:12px;color:#6B7B8D;">${(p.scenario||'').substring(0,80)}...</p></div> ${editDelBtns(`editPractical(${p.id})`,`deletePractical(${p.id})`)} </div>`,'#A5D6A7')).join(''); } function pForm(p={}) { return inp('mPTitle','Title',p.title||'') + `<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">` + inp('mPTime','Time (minutes)',p.time_minutes||60,'number') + sel('mPType','Type',['Individual','Pairs','Group of 3','Group of 4'],p.activity_type||'Individual') + `</div>` + ta('mPResources','Resources Required',p.resources||'',3) + ta('mPScenario','Kenyan Scenario',p.scenario||'',3) + ta('mPTasks','Tasks (i. ii. iii.)',p.tasks||'',4) + ta('mPCriteria','Assessor Criteria',p.assessor_criteria||'',3); } function addPractical() { openModal('Add Practical Activity', pForm(), async ()=>{ await api(`/api/chapter/${CHAPTER_ID}/practicals/`,'POST',{title:g('mPTitle'),time_minutes:+g('mPTime'),activity_type:g('mPType'),resources:g('mPResources'),scenario:g('mPScenario'),tasks:g('mPTasks'),assessor_criteria:g('mPCriteria')}); toast('✅ Practical added'); closeModal(); loadPracticals(); }); } async function editPractical(id) { const p = await api(`/api/practical/${id}/`); openModal('Edit Practical', pForm(p), async ()=>{ await api(`/api/practical/${id}/`,'PUT',{title:g('mPTitle'),time_minutes:+g('mPTime'),activity_type:g('mPType'),resources:g('mPResources'),scenario:g('mPScenario'),tasks:g('mPTasks'),assessor_criteria:g('mPCriteria')}); toast('✅ Saved'); closeModal(); loadPracticals(); }); } async function deletePractical(id) { if(!confirm('Delete this practical?'))return; await api(`/api/practical/${id}/`,'DELETE'); toast('🗑 Deleted'); loadPracticals(); } // ── Notes Editor ── let mde = null; let currentTopicId = null; let currentNoteType = null; async function loadTopicNote(value) { const wrap = document.getElementById('notesEditorWrap'); const placeholder = document.getElementById('notesEditorPlaceholder'); const status = document.getElementById('notesSaveStatus'); if (!value) { wrap.style.display='none'; placeholder.style.display='block'; document.body.classList.remove('note-editing'); return; } placeholder.style.display = 'none'; wrap.style.display = 'block'; // Image "Remove" buttons belong to NOTE editing only -- NOT to the sidebar's // topic-name edit mode. Turn them on only now that a note is really open. document.body.classList.add('note-editing'); status.textContent = '⏳ Loading...'; status.style.color = '#888'; // Destroy existing editor if (mde) { mde.toTextArea(); mde = null; } let mdContent = ''; if (value === 'full') { currentNoteType = 'full'; currentTopicId = null; const r = await api(`/api/chapter/${CHAPTER_ID}/notes/`); mdContent = r.content_md || ''; } else { currentNoteType = 'topic'; currentTopicId = value; currentSectionKey = null; const r = await api(`/api/topic/${value}/note/`); mdContent = r.content_md || ''; if (typeof loadPracticeQuestions === 'function') loadPracticeQuestions(); } // Init EasyMDE — recreate textarea each time to avoid stale element const wrap2 = document.getElementById("notesEditorWrap"); wrap2.querySelector("textarea")?.remove(); const freshTA = document.createElement("textarea"); freshTA.id = "notesEditor"; freshTA.value = mdContent; wrap2.insertBefore(freshTA, wrap2.firstChild); mde = new EasyMDE({ element: freshTA, spellChecker: false, autosave: { enabled: false }, previewRender: window.mdeMathPreview, toolbar: [ 'bold','italic','heading','|', 'quote','unordered-list','ordered-list','|', 'link','image','table','|', { name: 'upload-image', action: function(editor) { const input = document.createElement('input'); input.type = 'file'; input.accept = 'image/jpeg,image/png,image/gif,image/webp,image/svg+xml'; input.onchange = async function() { const file = input.files[0]; if (!file) return; const formData = new FormData(); formData.append('image', file); toast('⏳ Uploading image...'); const resp = await fetch('/api/upload-image/', { method: 'POST', headers: {'X-CSRFToken': CSRF}, body: formData, }); const data = await resp.json(); if (data.url) { const cm = editor.codemirror; const cursor = cm.getCursor(); const caption = file.name.replace(/\.[^.]+$/, '').replace(/[-_]/g, ' '); cm.replaceRange(`\n![${caption}](${data.url})\n*Figure: ${caption}. Source: uploaded*\n`, cursor); toast('✅ Image inserted'); } else { toast('❌ Upload failed: ' + (data.error||'unknown'), false); } }; input.click(); }, className: 'fa fa-upload', title: 'Upload Image', }, '|', 'preview','side-by-side','fullscreen','|', 'guide' ], minHeight: '500px', placeholder: 'Write markdown here...', }); status.textContent = `✅ ${mdContent.split(' ').filter(w=>w).length} words`; status.style.color = '#1E8449'; } function togglePreviewBtn(btn) { if (!mde) { toast('Select a topic first', false); return; } EasyMDE.togglePreview(mde); var on = mde.isPreviewActive(); btn.style.background = on ? 'var(--primary)' : '#fff'; btn.style.color = on ? '#fff' : 'var(--primary)'; btn.innerHTML = on ? '\u270E Back to editing' : '\u{1F441} Preview'; } async function saveNotes() { if (!mde) { toast('Select a topic first', false); return; } const status = document.getElementById('notesSaveStatus'); const content_md = mde.value(); status.textContent = 'Saving...'; status.style.color = '#888'; let _resp; if (currentNoteType === 'full') { _resp = await api(`/api/chapter/${CHAPTER_ID}/notes/`, 'PUT', {content_md}); } else if (currentNoteType === 'section') { _resp = await api(`/api/chapter/${CHAPTER_ID}/section/${currentSectionKey}/`, 'PUT', {content_md}); } else { _resp = await api(`/api/topic/${currentTopicId}/note/`, 'PUT', {content_md}); } if (_resp && _resp.error) { status.textContent = '⚠ ' + _resp.error; status.style.color = '#DC2626'; // A formatting refusal names exactly what is wrong — show it where the editor is, // not in a toast that vanishes before it can be acted on. Nothing is lost: the // text stays in the editor to be fixed. if (_resp.format_errors && _resp.format_errors.length) { showFormatIssues(_resp.format_errors, _resp.format_warnings || [], true); } else { toast(_resp.error, false); } return; } clearFormatIssues(); status.textContent = '✅ Saved'; status.style.color = '#1E8449'; // saved, but say plainly if it removed a lot — better found now than by a student if (_resp && _resp.format_warnings && _resp.format_warnings.length) { showFormatIssues([], _resp.format_warnings, false); toast('✅ Saved — check the note below the editor'); if (typeof closeFloatingEditor === 'function') closeFloatingEditor(); setTimeout(() => location.reload(), 4200); return; } toast('✅ Notes saved — reloading...'); if (typeof closeFloatingEditor === 'function') closeFloatingEditor(); setTimeout(() => location.reload(), 1500); } function clearFormatIssues() { const b = document.getElementById('fmtIssues'); if (b) { b.style.display = 'none'; b.innerHTML = ''; } } function showFormatIssues(errors, warnings, blocking) { let b = document.getElementById('fmtIssues'); if (!b) { b = document.createElement('div'); b.id = 'fmtIssues'; const wrap = document.getElementById('notesEditorWrap'); if (wrap && wrap.parentNode) wrap.parentNode.insertBefore(b, wrap.nextSibling); else return; } let html = ''; if (errors.length) { html += '<div class="fmt-head fmt-bad">Not saved — this would break the formatting</div>' + '<ul class="fmt-list">' + errors.map(function(e){ return '<li>' + e.replace(/</g,'<') + '</li>'; }).join('') + '</ul>' + '<div class="fmt-hint">Your text is still here. Fix the point above, then use ' + '<strong>Preview</strong> to check it reads right before saving.</div>'; } if (warnings.length) { html += '<div class="fmt-head fmt-warn">' + (blocking ? 'Also worth checking' : 'Saved — but worth checking') + '</div>' + '<ul class="fmt-list">' + warnings.map(function(w){ return '<li>' + w.replace(/</g,'<') + '</li>'; }).join('') + '</ul>'; } b.innerHTML = html; b.className = errors.length ? 'fmt-box fmt-box-bad' : 'fmt-box fmt-box-warn'; b.style.display = 'block'; b.scrollIntoView({block:'nearest', behavior:'smooth'}); } // ── Topics ── function editTopic(id, number, title) { openModal('Edit Topic', inp('mTNum','Number',number)+inp('mTTitle','Title',title), async ()=>{ const r = await api(`/api/topic/${id}/`,'PUT',{number:g('mTNum'),title:g('mTTitle')}); if(r.success){document.querySelector(`#sidebar-topic-${id} .topic-title-display`).textContent=r.title; toast('✅ Saved'); closeModal();} }); } async function deleteTopic(id) { if(!confirm('Delete this topic?'))return; await api(`/api/topic/${id}/`,'DELETE'); document.getElementById(`sidebar-topic-${id}`)?.remove(); toast('🗑 Deleted'); } function addTopic() { openModal('Add Topic', inp('mTNum','Number (e.g. 1.15)','')+inp('mTTitle','Title','')+`<div><label style="font-size:13px;"><input type="checkbox" id="mTSub"> Subtopic</label></div>`, async ()=>{ await api(`/api/chapter/${CHAPTER_ID}/topics/`,'POST',{number:g('mTNum'),title:g('mTTitle'),is_subtopic:document.getElementById('mTSub').checked}); toast('✅ Topic added'); closeModal(); setTimeout(()=>location.reload(),1000); }); } // ── Structure editor (7 Oct 2026): the whole topic tree, edited in place ── const stEsc = s => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); let ST = null; document.head.insertAdjacentHTML('beforeend', `<style> #structTree{padding:4px 0 10px} .st-head{display:flex;justify-content:space-between;align-items:baseline;gap:8px;padding:6px 12px;font-size:12px;color:#6B7B8D} .st-head b{color:var(--primary)} .st-last{margin:4px 10px 8px;padding:6px 8px;background:#FFF8E1;border:1px solid #F3D57A;border-radius:6px;font-size:11.5px;color:#5D4A00} .st-last button{margin-left:6px;font-size:11px;padding:1px 8px;border:1px solid #C9A227;border-radius:10px;background:#fff;cursor:pointer} .st-row{display:flex;align-items:flex-start;gap:6px;padding:5px 8px;border-bottom:1px solid #EEF1F4;font-size:12.5px} .st-row:hover{background:#F6F8FB} .st-num{min-width:44px;font-weight:600;color:var(--primary);font-size:11.5px;padding-top:1px} .st-title{flex:1;line-height:1.35} .st-sub .st-title{color:#4A5568} .st-only .st-num,.st-only .st-title{color:#8A6D00} .st-tag{display:inline-block;font-size:10px;background:#FFF3C4;color:#7A5B00;border-radius:8px;padding:0 6px;margin-left:4px} .st-row{flex-wrap:wrap}.st-title{flex:1 1 150px;min-width:0} .st-btns{display:flex;gap:2px;flex-shrink:0;margin-left:auto} .st-cur{margin:4px 10px 8px;padding:8px 10px;background:#EEF4FB;border:1px solid #C9DBEF;border-radius:6px;font-size:12px;color:#1F3A5F;line-height:1.45} .st-cur a{display:inline-block;margin-top:4px;font-weight:600} .st-sum{margin-top:6px} .st-miss{background:#FEF2F2}.st-miss .st-num,.st-miss .st-title{color:#B42318} .st-mini{font-size:10.5px;padding:0 7px;margin-left:4px;border:1px solid #98A2B3;border-radius:9px;background:#fff;cursor:pointer;color:#344054} .st-t-miss{background:#FEE4E2;color:#B42318}.st-t-nosec{background:#FFE8D9;color:#9A3412}.st-t-off{background:#EEF0F3;color:#475467}.st-t-ok{background:#DCFCE7;color:#166534} .st-btns button{font-size:11px;padding:1px 5px;border:1px solid #CBD5E0;border-radius:3px;background:#fff;cursor:pointer;line-height:1.3} .st-btns .st-del{border-color:#FCA5A5;background:#FEF2F2;color:#DC2626} .st-addend{display:block;width:calc(100% - 20px);margin:8px 10px 0;padding:6px;border:1px dashed #CBD5E0;border-radius:6px;background:#fff;cursor:pointer;font-size:12px;color:#6B7B8D} .st-opt{display:block;margin:6px 0;font-size:13px} </style>`); // ── Compare side by side (13 Oct 2026): the curriculum pages beside the topic list, as on the review screen. // Laptop: list | pages (notes hidden until you go back). Wide screen: list | pages | notes. Remembered per browser. document.head.insertAdjacentHTML('beforeend', `<style> body.st-compare .two-col, body.st-compare .two-col:has(.sidebar.topics-open) { display: grid !important; grid-template-columns: var(--st-left, 50%) minmax(0, 1fr) !important; gap: 12px; align-items: start; } body.st-compare .two-col > .sidebar { width: auto !important; max-width: none !important; min-width: 0 !important; } #stPages { position: sticky; } #stDrag { position: absolute; left: -10px; top: 0; bottom: 0; width: 10px; cursor: col-resize; z-index: 3; } #stDrag::after { content: ""; position: absolute; left: 3px; top: 50%; height: 48px; margin-top: -24px; width: 4px; border-radius: 2px; background: #C9D3E0; } #stDrag:hover::after { background: #4A7FB5; } /* compare mode uses the whole screen: the site's left menu and the outer margins go, the help paragraph goes */ body.st-compare .dash-sidebar, body.st-compare .dash-nav, body.st-compare .dash-shell > aside { display: none !important; } body.st-compare .dash-main { margin-left: 0 !important; width: 100% !important; max-width: none !important; } body.st-compare .dash-inner, body.st-compare .page-wrapper { max-width: none !important; padding-left: 10px !important; padding-right: 10px !important; } body.st-compare .st-help { display: none; } body.st-compare .two-col > :not(.sidebar):not(#stPages) { display: none; } #stPages { position: sticky; align-self: start; border: 1px solid #E5E8EE; border-radius: 10px; overflow: hidden; background: #fff; } #stPages iframe { width: 100%; height: 100%; border: 0; display: block; } #stPages .st-pages-bar { display: flex; justify-content: space-between; align-items: center; padding: 6px 10px; font-size: 12px; background: #EEF4FB; border-bottom: 1px solid #C9DBEF; color: #1F3A5F; } @media (min-width: 1700px) { body.st-compare .two-col, body.st-compare .two-col:has(.sidebar.topics-open) { grid-template-columns: var(--st-left, 34%) minmax(0, 1fr) minmax(0, 1.2fr) !important; } body.st-compare .two-col > :not(.sidebar):not(#stPages) { display: block; } } </style>`); function stPagesSize() { const sb = document.querySelector('.two-col > .sidebar'), pg = document.getElementById('stPages'); if (!sb || !pg) return; const top = parseInt(sb.style.top || '12', 10) || 12; pg.style.top = top + 'px'; pg.style.height = 'calc(100vh - ' + (top + 24) + 'px)'; } function stCompare(pdf, on) { const want = on === undefined ? !document.body.classList.contains('st-compare') : on; const col = document.querySelector('.two-col'), sb = col && col.querySelector('.sidebar'); let pg = document.getElementById('stPages'); if (want && pdf && col) { if (sb && !sb.classList.contains('topics-open')) sb.classList.add('topics-open'); if (!pg) { pg = document.createElement('div'); pg.id = 'stPages'; pg.innerHTML = `<div class="st-pages-bar"><b>Curriculum pages</b><span><a href="${pdf}" target="_blank" rel="noopener">open in a tab ↗</a>   <a href="#" onclick="stCompare(null,false);return false;">close</a></span></div><iframe src="${pdf}#toolbar=0&navpanes=0&view=FitH" title="Curriculum pages"></iframe>`; sb.after(pg); // drag the divider to share the width between the list and the pages; remembered per browser const dg = document.createElement('div'); dg.id = 'stDrag'; dg.title = 'Drag to resize'; pg.appendChild(dg); dg.addEventListener('mousedown', e => { e.preventDefault(); const fr = pg.querySelector('iframe'); if (fr) fr.style.pointerEvents = 'none'; const mv = ev => { const r = col.getBoundingClientRect(); const pct = Math.max(25, Math.min(75, (ev.clientX - r.left) / r.width * 100)); col.style.setProperty('--st-left', pct.toFixed(1) + '%'); }; const up = () => { document.removeEventListener('mousemove', mv); document.removeEventListener('mouseup', up); if (fr) fr.style.pointerEvents = ''; try { localStorage.setItem('pp_compare_left', col.style.getPropertyValue('--st-left')); } catch (e) {} }; document.addEventListener('mousemove', mv); document.addEventListener('mouseup', up); }); try { const w = localStorage.getItem('pp_compare_left'); if (w) col.style.setProperty('--st-left', w); } catch (e) {} } document.body.classList.add('st-compare'); stPagesSize(); } else { if (pg) pg.remove(); document.body.classList.remove('st-compare'); } try { localStorage.setItem('pp_compare', document.body.classList.contains('st-compare') ? '1' : '0'); } catch (e) {} const btns = document.querySelectorAll('#structTree .st-cur button.st-mini'); btns.forEach(b => { if (/Compare side by side|Back to the notes/.test(b.textContent)) b.textContent = document.body.classList.contains('st-compare') ? 'Back to the notes' : 'Compare side by side'; }); } function stCompareRestore(pdf) { let on = false; try { on = localStorage.getItem('pp_compare') === '1'; } catch (e) {} if (on && pdf && !document.getElementById('stPages')) stCompare(pdf, true); } window.addEventListener('resize', stPagesSize); // Extras (sub-topics the notes have that the curriculum does not list) hidden by default, so the list reads like // the curriculum; one button shows them. Chapter title edited in place. 13 Oct 2026 document.head.insertAdjacentHTML('beforeend', `<style> body.st-hide-extra #structTree .st-only { display: none; } .st-ctitle { outline: none; border-radius: 3px; padding: 0 3px; cursor: text; } .st-ctitle:hover { background: #F3F5F9; } .st-ctitle:focus { background: #FFF7D6; } </style>`); function stHideExtra() { try { return localStorage.getItem('pp_hide_extra') !== '0'; } catch (e) { return true; } } function stApplyExtra() { document.body.classList.toggle('st-hide-extra', stHideExtra()); } function stToggleExtra() { try { localStorage.setItem('pp_hide_extra', stHideExtra() ? '0' : '1'); } catch (e) {} stApplyExtra(); structLoad(); } document.addEventListener('keydown', e => { const el = e.target.closest && e.target.closest('.st-ctitle'); if (!el) return; if (e.key === 'Escape') { el.textContent = ST.chapter_title; el.blur(); return; } if (e.key !== 'Enter') return; e.preventDefault(); const t = el.textContent.replace(/\s+/g, ' ').trim(); if (!t || t === ST.chapter_title) { el.textContent = ST.chapter_title; el.blur(); return; } structOp({op: 'chapter_title', title: t}).then(() => location.reload()); }); // Clicking anywhere on a topic row selects it: the row is highlighted and its tools stay open under it, as on the // review screen. (13 Oct 2026: tools showed only while the title kept keyboard focus, which the row click took away.) document.addEventListener('mousedown', e => { if (e.target.closest && e.target.closest('.st-tools, .st-btns, .st-mini, .st-new')) return; const row = e.target.closest && e.target.closest('#structTree .st-row[data-id]'); if (!row) return; document.querySelectorAll('#structTree .st-row.st-active').forEach(x => { if (x !== row) x.classList.remove('st-active'); }); row.classList.add('st-active'); }); async function structLoad() { const box = document.getElementById('structTree'); box.innerHTML = '<div class="st-head">Loading the structure…</div>'; const r = await api(`/api/chapter/${CHAPTER_ID}/structure/`); if (!r.ok) { box.innerHTML = `<div class="st-last">${stEsc(r.error || 'Could not load the structure.')}</div>`; return; } ST = r; const inList = r.topics.filter(t => t.id).length, miss = r.topics.filter(t => t.missing || (t.notes_only && t.in_curriculum)).length; const nosec = r.topics.filter(t => t.id && t.in_curriculum && t.has_section === false).length; const off = r.topics.filter(t => t.id && t.in_curriculum === false && !t.covers).length; const nExtra = r.topics.filter(t => t.notes_only && !t.in_curriculum && !t.covers).length; const nPts = r.topics.reduce((n, t) => n + (t.points || []).filter(q => !q.where).length, 0), nWeak = r.topics.reduce((n, t) => n + (t.points || []).filter(q => q.where && q.where.startsWith('the text of')).length, 0); let h = `<div class="st-head"><b>Structure</b><span>${inList} in the topic list</span></div>`; if (r.curriculum) h += `<div class="st-cur"><div><b>Curriculum</b> (${stEsc(r.curriculum.status)}): ${stEsc(r.curriculum.outcome)}</div>` + (r.curriculum.pdf ? `<a href="${r.curriculum.pdf}" target="_blank" rel="noopener">View the curriculum pages ↗</a>` + ` <button class="st-mini" style="font-weight:700;margin-left:6px" onclick="stCompare(ST.curriculum.pdf)">${document.body.classList.contains('st-compare') ? 'Back to the notes' : 'Compare side by side'}</button>` : '') + ((r.chapter_title && r.curriculum.outcome && stNorm(r.chapter_title) !== stNorm(r.curriculum.outcome)) ? `<div class="st-sum">Chapter title "<b>${stEsc(r.chapter_title)}</b>" differs from our copy's outcome. Check the content table, then: <button class="st-mini" onclick="structChapterTitle()">chapter is wrong</button><button class="st-mini" onclick="curOutcome()">our copy is wrong</button></div>` : '') + ((r.curriculum.corrected || []).length ? `<div class="st-sum" style="color:#9A3412">The curriculum PDF misnumbers some items here. Shown by their position: ${r.curriculum.corrected.map(stEsc).join('; ')}. Check against the pages.</div>` : '') + `<div class="st-sum">Chapter title: <b class="st-ctitle" contenteditable="true" spellcheck="false" title="Click to edit; Enter saves, Esc cancels">${stEsc(r.chapter_title)}</b></div>` + `<div class="st-sum"><a href="/quality/curriculum/296/outlines/9715/" target="_blank" rel="noopener">Edit the curriculum outline ↗</a> <button type="button" class="st-arr" onclick="stArrange()">Arrange as in the outline</button>` + (nExtra ? ` <button class="st-mini" onclick="stToggleExtra()">${stHideExtra() ? 'Show the ' + nExtra + ' extra sub-topic' + (nExtra > 1 ? 's' : '') + ' in the notes' : 'Hide the extras: curriculum only'}</button>` : '') + `</div>` + `<div class="st-sum">${miss ? `<span class="st-tag st-t-miss">${miss} missing from the list</span>` : ''}${nosec ? `<span class="st-tag st-t-nosec">${nosec} without a notes section</span>` : ''}${nPts ? `<span class="st-tag st-t-miss">${nPts} curriculum point${nPts > 1 ? 's' : ''} without notes</span>` : ''}${nWeak ? `<span class="st-tag st-t-weak">${nWeak} point${nWeak > 1 ? 's' : ''} only mentioned in the text: check</span>` : ''}${off ? `<span class="st-tag st-t-off">${off} in the list, not in the curriculum</span>` : ''}${!miss && !nosec && !off && !nPts && !nWeak ? '<span class="st-tag st-t-ok">matches the curriculum</span>' : ''}</div></div>`; else h += `<div class="st-cur">No curriculum outline for this unit yet.</div>`; if (r.last) h += `<div class="st-last">Last change: ${stEsc(r.last.summary)} <span style="opacity:.7">(${stEsc(r.last.who)}, ${stEsc(r.last.when)})</span><button onclick="structUndo()">Undo</button></div>`; const nGlue = r.topics.filter(t => t.id && t.glue).length; if (r.curriculum && r.curriculum.status === 'confirmed' && nGlue) h += `<div class="st-cur" style="background:#FFF7ED;border-color:#FDBA74"><b>${nGlue} title${nGlue > 1 ? 's carry' : ' carries'} leftover text from the curriculum PDF</b> (bullets, column words). <button class="st-mini" style="font-weight:700" onclick="structOp({op:'align'})">Fix ${nGlue > 1 ? 'all ' + nGlue : 'it'}</button></div>`; h += `<div class="st-help">Click a title to edit it. <b>Enter</b> saves; in the middle of a title it splits it, the rest becoming the next topic. Tools under the title: ← → level, + line, + sub, join ↑, ×. Every change is saved at once and can be undone.</div>`; for (const t of r.topics) { const pad = 8 + 14 * Math.max(0, t.depth - 1); if (t.missing || (t.notes_only && t.in_curriculum)) { const ct = t.curriculum_title || t.title; h += `<div class="st-row st-miss" style="padding-left:${pad}px"><span class="st-num">${stEsc(t.number)}</span><span class="st-title">${stEsc(ct)}<span class="st-tag st-t-miss">missing: in the curriculum, not in the chapter</span>${t.has_section ? '' : '<span class="st-tag st-t-nosec">no notes yet</span>'}</span><span class="st-btns">` + (t.implied ? '<span class="st-tag st-t-off">title not captured: read it from the pages, then use +</span></span></div>' : (() => { // the same title already sits in the chapter under another number: renumber that one, never add a twin const twin = r.topics.find(x => x.id && x.in_curriculum === false && stNorm(x.title) === stNorm(ct)); return twin ? `<button onclick='structOp({op:"renumber",topic:${twin.id},number:${JSON.stringify(t.number)}})'>Number ${stEsc(twin.number)} as ${stEsc(t.number)}</button></span></div>` : `<button onclick='structOp({op:"adopt",number:${JSON.stringify(t.number)},title:${JSON.stringify(ct)}})'>Add topic</button></span></div>`; })()); continue; } if (t.notes_only) { h += `<div class="st-row ${t.covers ? 'st-cov' : 'st-only'}" style="padding-left:${pad}px"><span class="st-num">${stEsc(t.number)}</span><span class="st-title">${stEsc(t.title)}${t.covers ? `<span class="st-tag st-t-cov">covers the curriculum point: ${stEsc(t.covers)}</span>` : '<span class="st-tag">extra in the notes</span>' + (() => { const tw = ST.topics.find(x => x.number !== t.number && (x.id || x.notes_only) && stNorm(x.title) === stNorm(t.title)); return (tw ? '<span class="st-tag st-t-miss">repeats ' + stEsc(tw.number) + '</span>' : '') + '<button type="button" class="st-delbtn st-dropbtn" data-num="' + stEsc(t.number) + '" data-title="' + stEsc(t.title) + '">Delete</button>'; })()}</span><span class="st-btns">` + `<button title="Add a topic next to it" onclick="structAdd(null,'${stEsc(t.number)}')">+</button></span></div>`; continue; } let chips = ''; if (t.in_curriculum === false) { const twin = t.numbered && r.topics.find(x => x.missing && !x.implied && stNorm(x.curriculum_title || x.title) === stNorm(t.title)); chips += `<span class="st-tag st-t-off">not in the curriculum</span>` + (twin ? `<button class="st-mini" style="font-weight:700" onclick='structOp({op:"renumber",topic:${t.id},number:${JSON.stringify(twin.number)}})'>Number it ${stEsc(twin.number)}, as in the curriculum</button>` : '') + `<button class="st-mini" onclick="curAdd(${t.id})">it is in the curriculum</button>`; } if (t.title_differs) chips += `<span class="st-tag ${t.glue ? 'st-t-miss' : 'st-t-off'}">curriculum: ${stEsc(t.curriculum_title)}</span>` + `<button class="st-mini" style="font-weight:700" onclick='structOp({op:"rename",topic:${t.id},title:${JSON.stringify(t.curriculum_title)}})'>Use curriculum title</button>` + `<button class="st-mini" title="Only if the curriculum pages say otherwise" onclick="curRetitle(${t.id})">curriculum is wrong</button>`; if (t.numbered && t.has_section === false) chips += t.other_section ? `<span class="st-tag st-t-nosec">no notes yet: the notes section ${stEsc(t.number)} is about "${stEsc(t.other_section)}"</span>` : t.thin ? '<span class="st-tag st-t-nosec">empty notes section</span>' : `<span class="st-tag st-t-nosec">no notes section</span><button class="st-mini" onclick="structOp({op:'heading',topic:${t.id}})">add to notes</button>`; h += `<div class="st-row${t.depth > 1 ? ' st-sub' : ''}" data-id="${t.id}" style="padding-left:${pad}px">${t.number ? `<span class="st-num st-nedit" contenteditable="true" spellcheck="false" title="Click to change the number" data-id="${t.id}">${stEsc(t.number)}</span>` : '<span class="st-num">•</span>'}` + `<span class="st-title"><span class="st-edit" contenteditable="true" spellcheck="false" data-id="${t.id}">${stEsc(t.title)}</span>${t.covers ? '<span class="st-tag st-t-cov">covers the curriculum point: ' + stEsc(t.covers) + '</span>' + chips.replace(/<span[^>]*>\s*not in the curriculum\s*<\/span>/, '').replace(/<button[^>]*>\s*it is in the curriculum\s*<\/button>/, '') : chips}${(t.in_curriculum === false && !t.covers && String(t.number || '').split('.').length >= 3) ? '<button type="button" class="st-pbtn st-dem" data-topic="' + t.id + '">Make it a heading in ' + stEsc(String(t.number).split('.').slice(0, -1).join('.')) + '</button>' : ''}${(t.in_curriculum === false && !t.covers) ? (() => { const tw = ST.topics.find(x => x.id && x.id !== t.id && x.in_curriculum && stNorm(x.title) === stNorm(t.title)); return (tw ? '<span class="st-tag st-t-miss">repeats ' + stEsc(tw.number) + '</span>' : '') + '<button type="button" class="st-delbtn" onclick="structRemove(' + t.id + ')">Delete</button>'; })() : ''}${(t.points || []).length ? '<span class="st-pts">' + t.points.map((q, qi) => { const wk = q.where && q.where.startsWith('the text of'); const bt = (m, l) => `<button type="button" class="st-pbtn" data-topic="${t.id}" data-pt="${qi}" data-mode="${m}">${l}</button>`; return '<span class="st-pt' + (!q.where ? ' miss' : wk ? ' weak' : '') + '">' + stEsc(q.text) + (q.empty ? ': no notes yet' : !q.where ? ': not in the notes' : wk ? ': only mentioned in ' + stEsc(q.where) + ': check' : (q.head && q.head.toLowerCase() === q.text.toLowerCase()) ? '' : ': covered in ' + stEsc(q.where)) + (q.empty ? '' : !q.where ? bt('empty', 'Add heading') : wk ? bt('empty', 'Add heading') : (q.head && q.where.startsWith('"') && q.head.toLowerCase() !== q.text.toLowerCase() && t.points.filter(x => x.head === q.head).length === 1) ? bt('rename1', 'Use curriculum name') : '') + '</span>'; }).join('') + '</span>' : ''}</span><span class="st-btns">` + (t.depth === 1 ? `<button title="Copy the prompt, this topic's curriculum and its current notes for Claude" onclick="structClaude(${t.id})">Claude</button>` : '') + (t.numbered ? `<button title="Move up" onclick="structOp({op:'move',topic:${t.id},dir:'up'})">↑</button><button title="Move down" onclick="structOp({op:'move',topic:${t.id},dir:'down'})">↓</button>` : '') + `</span><span class="st-tools" contenteditable="false">` + (t.numbered ? `<button data-a="out" title="Up a level">←</button><button data-a="in" title="Down a level">→</button>` : '') + `<button data-a="add" title="Add a topic below, same level">+ line</button>` + `<button data-a="bullet" title="Add an unnumbered point under it, as the curriculum lists (e.g. Knowledge, Management)">+ point</button>` + (t.numbered ? `<button data-a="sub" title="Add a sub-topic under it">+ sub</button><button data-a="join" title="Merge it into the topic above (its notes join that topic)">join ↑</button>` : '') + `<button data-a="del" title="Remove">×</button></span></div>`; } h += `<button class="st-addend" onclick="structAdd(null)">+ Add a main topic at the end</button>`; box.innerHTML = h; stCompareRestore(r.curriculum && r.curriculum.pdf); stApplyExtra(); } const stTopic = id => (ST && ST.topics.find(t => t.id === id)) || {}; // ── In-place editing (13 Oct 2026): the outline editor's tools on the chapter's own topic list. Every change goes // through the structure operations, so the notes headings and per-topic copies follow, and Undo works step by step. document.head.insertAdjacentHTML('beforeend', `<style> .st-edit{outline:none;border-radius:3px;padding:0 2px} .st-edit:hover{background:#F3F5F9}.st-edit:focus{background:#FFF7D6} .st-nedit{outline:none;border-radius:3px;cursor:text}.st-nedit:hover{background:#F3F5F9}.st-nedit:focus{background:#FFF7D6} .st-pts{display:block;margin-top:4px;font-size:12px;line-height:1.7}.st-pt{display:block;margin:0;padding-left:16px;color:#067647}.st-pt.miss{color:#B42318;font-weight:600}.st-pt.weak{color:#B54708;font-weight:600}.st-pbtn{margin-left:8px;font-size:11px;font-weight:600;padding:1px 9px;border:1px solid currentColor;border-radius:10px;background:#fff;color:inherit;cursor:pointer}.st-pbtn:hover{background:#FFF7D6}.st-delbtn{margin-left:8px;font-size:11px;font-weight:600;padding:1px 9px;border:1px solid #B42318;border-radius:10px;background:#fff;color:#B42318;cursor:pointer}.st-delbtn:hover{background:#FEF2F2}.st-arr{margin-left:8px;font-size:12px;font-weight:700;padding:3px 12px;border:1px solid #1E40AF;border-radius:12px;background:#1E40AF;color:#fff;cursor:pointer}.st-arr:hover{background:#1E3A8A}.st-pick{position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:9999;display:flex;align-items:center;justify-content:center}.st-pick-box{background:#fff;border-radius:10px;padding:16px;max-width:680px;width:92%;max-height:80vh;overflow:auto;font-size:13px;line-height:1.5}.st-pick-box button{display:block;width:100%;text-align:left;margin-top:8px;padding:8px 10px;border:1px solid #D0D5DD;border-radius:6px;background:#F9FAFB;cursor:pointer;font-size:13px;line-height:1.5}.st-pick-box button:hover{background:#FFF7D6}.st-pick-box .st-pick-cancel{text-align:center;background:#fff}.st-t-weak{background:#FFFAEB!important;color:#B54708!important}.st-t-cov{background:#ECFDF3!important;color:#067647!important} .st-tools{display:none;flex-basis:100%;margin:3px 0 2px;gap:2px} .st-row:focus-within .st-tools,.st-row.st-active .st-tools{display:flex} .st-row.st-active{background:#FFFBEB;box-shadow:inset 3px 0 0 #E8A020} .st-tools button{border:1px solid #E5E8EE;background:#fff;border-radius:4px;cursor:pointer;font-size:11.5px;padding:1px 6px;color:#334155} .st-tools button:hover{background:#E5E8EE} .st-help{margin:2px 10px 6px;font-size:11.5px;color:#6B7B8D;line-height:1.4} .st-new{display:block;margin:2px 0 4px;padding:3px 6px;border:1px dashed #94A3B8;border-radius:4px;background:#FFFBEB;font-size:12.5px;outline:none} .st-new:empty::before{content:"type the title, then Enter (Esc cancels)";color:#94A3B8} </style>`); function stCaretSplit(el) { const sel = getSelection(); if (!sel.rangeCount) return [el.textContent.trim(), '']; const rg = sel.getRangeAt(0), pre = document.createRange(); pre.selectNodeContents(el); pre.setEnd(rg.startContainer, rg.startOffset); const k = pre.toString().length, full = el.textContent; return [full.slice(0, k).trim(), full.slice(k).trim()]; } function stNewRow(row, id, where) { document.querySelectorAll('.st-new').forEach(x => x.remove()); const n = document.createElement('div'); n.className = 'st-new'; n.contentEditable = 'true'; n.dataset.id = id; n.dataset.where = where; n.style.marginLeft = (where === 'below' ? 14 : 28) + 'px'; if (where === 'bullet') n.dataset.ph = 'type the point, then Enter (Esc cancels)'; row.after(n); n.focus(); } (function () { const box = document; // the panel is redrawn on every load, so listen on the page box.addEventListener('mousedown', e => { if (e.target.closest('.st-tools')) e.preventDefault(); }); // number editing: click the number, type, Enter saves (renumber keeps notes, questions and sub-topics), Esc cancels window.stArrange = async () => { // PREVIEW FIRST: the server makes every change, lists them, and rolls them back. 15 Oct 2026 const p = await api(`/api/chapter/${CHAPTER_ID}/structure/`, 'POST', {op: 'arrange', preview: 1}); if (!p.ok) { alert(p.error || 'That did not work.'); return; } const msg = p.message || ''; if (/^Nothing to arrange/.test(msg)) { alert(msg); return; } if (!confirm(msg.replace('PREVIEW (nothing saved yet). ', '') + '\n\nPress OK to make these changes. Each one shows in Last change and can be undone.')) return; structOp({op: 'arrange'}); }; // Buttons on curriculum points: Add heading / Use curriculum name / move a paragraph. 13 Oct 2026 if (!window._stPtBound) { window._stPtBound = true; document.addEventListener('click', e => { const dr = e.target.closest('.st-dropbtn'); if (dr) { e.preventDefault(); e.stopPropagation(); if (confirm('Delete the notes section "' + dr.dataset.num + ' ' + dr.dataset.title + '"?\n\nIts text is removed from the notes and the numbers after it close up. Undo restores it.')) structOp({op: 'drop', number: dr.dataset.num}); return; } const dm = e.target.closest('.st-dem'); if (dm) { e.preventDefault(); e.stopPropagation(); const t = ST.topics.find(x => String(x.id) === dm.dataset.topic); if (!t) return; const par = String(t.number).split('.').slice(0, -1).join('.'); const nt = prompt('"' + t.number + ' ' + t.title + '" is not in the curriculum. It will leave the topic list and become a heading inside ' + par + ', with all its notes text kept.\n\nHeading title (correct it to what the text is about):', t.title); if (nt && nt.trim()) structOp({op: 'demote', topic: t.id, title: nt.trim()}); return; } const b = e.target.closest('.st-pbtn'); if (!b) return; e.preventDefault(); e.stopPropagation(); const t = ST.topics.find(x => String(x.id) === b.dataset.topic); if (!t) return; const q = t.points[+b.dataset.pt], base = {op: 'point', topic: t.id, point: q.text}; const pick = (title, items, done) => { const d = document.createElement('div'); d.className = 'st-pick'; d.innerHTML = '<div class="st-pick-box"><div style="margin-bottom:6px">' + title + '</div>' + items.map((x, i) => '<button type="button" data-i="' + i + '">' + x.html + '</button>').join('') + '<button type="button" data-i="-1" class="st-pick-cancel">Cancel</button></div>'; d.addEventListener('click', ev => { const bb = ev.target.closest('button'); if (!bb && ev.target !== d) return; d.remove(); const i = bb ? +bb.dataset.i : -1; if (i >= 0) done(items[i]); }); document.body.appendChild(d); }; if (b.dataset.mode === 'rename1') structOp(Object.assign(base, {mode: 'rename', heading: q.head})); else if (b.dataset.mode === 'empty' && (t.free_heads || []).length) pick('Is <b>' + stEsc(q.text) + '</b> already in the notes under another heading in <b>' + stEsc(t.number) + '</b>? Pick it and it is renamed to <b>' + stEsc(q.text) + '</b>, its text kept.', t.free_heads.map(x => ({html: stEsc(x), v: x})).concat([{html: '<b>It is not in the notes:</b> add a new heading <b>' + stEsc(q.text) + '</b>', v: null}]), it => structOp(Object.assign(base, it.v ? {mode: 'rename', heading: it.v} : {mode: 'empty'}))); else if (b.dataset.mode === 'empty') structOp(Object.assign(base, {mode: 'empty'})); else if (b.dataset.mode === 'rename') pick('Which heading in <b>' + stEsc(t.number) + '</b> covers <b>' + stEsc(q.text) + '</b>? It will be renamed to <b>' + stEsc(q.text) + '</b>. Its text stays as it is.', (t.free_heads || []).map(x => ({html: stEsc(x), v: x})), it => structOp(Object.assign(base, {mode: 'rename', heading: it.v}))); else pick('<b>' + stEsc(q.text) + '</b> is mentioned in these paragraphs. Pick the one that is about it: it moves under the new heading <b>' + stEsc(q.text) + '</b>.', (q.paras || []).map(x => ({html: stEsc(x.length > 450 ? x.slice(0, 450) + '...' : x), v: x})).concat([{html: '<b>None of these:</b> add the heading empty and leave the text where it is', v: null}]), it => structOp(Object.assign(base, it.v ? {mode: 'move', para: it.v} : {mode: 'empty'}))); }, true); } box.addEventListener('focusin', e => { const el = e.target.closest('.st-nedit'); if (el) el.dataset.orig = el.textContent.trim(); }); box.addEventListener('keydown', e => { const el = e.target.closest('.st-nedit'); if (!el) return; if (e.key === 'Enter') { e.preventDefault(); el.blur(); } else if (e.key === 'Escape') { e.preventDefault(); el.textContent = el.dataset.orig; el.blur(); } }); box.addEventListener('focusout', e => { const el = e.target.closest('.st-nedit'); if (!el) return; const v = el.textContent.trim().replace(/\.$/, ''), old = el.dataset.orig; if (!v || v === old) { el.textContent = old; return; } if (!/^\d+(\.\d+)*$/.test(v)) { el.textContent = old; alert('Use a number like 2.1'); return; } structOp({op: 'renumber', id: +el.dataset.id, topic: +el.dataset.id, from: old, number: v}); }); box.addEventListener('focusin', e => { const el = e.target.closest('.st-edit'); if (el && el.dataset.orig === undefined) el.dataset.orig = el.textContent; }); box.addEventListener('paste', e => { if (!e.target.closest('.st-edit, .st-new')) return; e.preventDefault(); document.execCommand('insertText', false, (e.clipboardData.getData('text/plain') || '').replace(/\s+/g, ' ')); }); box.addEventListener('keydown', e => { const nw = e.target.closest('.st-new'); if (nw) { if (e.key === 'Escape') { nw.remove(); return; } if (e.key !== 'Enter') return; e.preventDefault(); const title = nw.textContent.replace(/\s+/g, ' ').trim(); if (!title) return; structOp(nw.dataset.where === 'bullet' ? {op: 'add', topic: +nw.dataset.id, where: 'inside', unnumbered: true, title: title} : {op: 'add', topic: +nw.dataset.id, where: nw.dataset.where, title: title}); return; } const el = e.target.closest('.st-edit'); if (!el) return; if (e.key === 'Escape') { el.textContent = el.dataset.orig || el.textContent; el.blur(); return; } if (e.key !== 'Enter') return; e.preventDefault(); const id = +el.dataset.id, [before, after] = stCaretSplit(el), orig = (el.dataset.orig || '').trim(); if (!after) { if (before && before !== orig) { el.dataset.saving = '1'; structOp({op: 'rename', topic: id, title: before}); } else stNewRow(el.closest('.st-row'), id, 'below'); return; } // split: the part before the caret stays; the rest (its own number dropped) becomes the next topic const m = after.match(/^(\d+(?:\s?\.\s?\d+)*)\.?\s+(.+)$/); el.dataset.saving = '1'; structOp({op: 'batch', ops: [{op: 'rename', topic: id, title: before}, {op: 'add', topic: id, where: 'below', title: m ? m[2] : after}]}); }); box.addEventListener('focusout', e => { const el = e.target.closest('.st-edit'); if (!el || el.dataset.saving) return; const now = el.textContent.replace(/\s+/g, ' ').trim(), orig = (el.dataset.orig || '').trim(); delete el.dataset.orig; if (now && now !== orig) { el.dataset.saving = '1'; structOp({op: 'rename', topic: +el.dataset.id, title: now}); } else if (!now) el.textContent = orig; }); box.addEventListener('click', e => { const b = e.target.closest('.st-tools button'); if (!b) return; e.preventDefault(); const row = b.closest('.st-row'), id = +row.dataset.id, t = stTopic(id), a = b.dataset.a; if (a === 'out' || a === 'in') structOp({op: 'relevel', topic: id, dir: a}); else if (a === 'add') stNewRow(row, id, 'below'); else if (a === 'sub') stNewRow(row, id, 'inside'); else if (a === 'bullet') stNewRow(row, id, 'bullet'); else if (a === 'join') { if (ST.topics.some(x => x.id && x.number && x.number.startsWith(t.number + '.'))) { alert(t.number + ' has sub-topics: join them into it first, or move them.'); return; } if (confirm('Merge ' + t.number + ' ' + t.title + ' into the topic above it?\n\nIts notes text joins that topic; the topics after it move up one. Undo restores it.')) structOp({op: 'remove', topic: id, notes: 'merge', close_gap: true}); } else if (a === 'del') structRemove(id); }); })(); function structAdd(id, num) { const t = id ? stTopic(id) : (num ? (ST.topics.find(x => x.notes_only && x.number === num) || {number: num}) : null); const opts = [['above', 'Above it (takes its number; the ones after move down one)'], ['below', 'Below it, at the same level'], ['inside', 'Inside it, as a sub-topic (after its last sub-topic)']]; if (id) opts.push(['example', 'Inside it, as an unnumbered line (an example the curriculum lists)']); let body = t ? `<div style="font-size:13px;margin-bottom:6px;">Next to <b>${stEsc(t.number)} ${stEsc(t.title)}</b>:</div>` + opts.map(([v, l]) => `<label class="st-opt"><input type="radio" name="stWhere" value="${v}"${v === 'below' ? ' checked' : ''}> ${l}</label>`).join('') : `<div style="font-size:13px;margin-bottom:6px;">A new main topic after the last one.</div>`; body += inp('stTitle', 'Title, exactly as in the curriculum', ''); openModal(t ? 'Add a topic' : 'Add a main topic', body, () => { const w = t ? (document.querySelector('input[name=stWhere]:checked') || {}).value : 'end'; structOp({op: 'add', topic: id || null, ref_number: id ? null : (num || null), where: w === 'example' ? 'inside' : w, unnumbered: w === 'example', title: g('stTitle')}); }); } function structRename(id) { const t = stTopic(id); openModal('Rename ' + (t.number || 'line'), inp('stTitle', 'Title, exactly as in the curriculum', t.title), () => structOp({op: 'rename', topic: id, title: g('stTitle')})); } function structRemove(id) { // The word count and the curriculum twin are shown before anything is deleted. 14 Oct 2026 const t = stTopic(id), w = t.words || 0; const warn = w ? `<div style="font-size:13px;margin:6px 0 8px;padding:7px 9px;background:#FEF2F2;color:#991B1B;border-radius:6px;">Its notes section has <b>${w} words</b>. "Delete its notes section too" deletes them.` + (t.twin ? ` This looks like the curriculum topic <b>${stEsc(t.twin)}</b>: change its number to ${stEsc(t.twin)} instead (click the number), so the notes move with it.` : '') + `</div>` : ''; openModal('Remove ' + (t.number || 'line'), `<div style="font-size:13px;margin-bottom:8px;">Remove <b>${stEsc(t.number)} ${stEsc(t.title)}</b>?</div>` + warn + `<label class="st-opt"><input type="radio" name="stNotes" value="delete" checked> Delete its notes section too</label>` + `<label class="st-opt"><input type="radio" name="stNotes" value="merge"> Keep its notes text (it joins the topic above)</label>` + (w ? `<label class="st-opt" style="color:#991B1B;"><input type="checkbox" id="stWords"> I understand: ${w} words of notes will be deleted</label>` : '') + (t.numbered ? `<label class="st-opt"><input type="checkbox" id="stGap" checked> Renumber the ones after it to close the gap</label>` : ''), () => { const notes = (document.querySelector('input[name=stNotes]:checked') || {}).value; if (w && notes === 'delete' && !(document.getElementById('stWords') || {}).checked) { alert('Tick "I understand: ' + w + ' words of notes will be deleted", or choose to keep its notes text.'); return; } structOp({op: 'remove', topic: id, notes: notes, close_gap: !!(document.getElementById('stGap') || {}).checked, confirm_words: true}); }); } // Writer rules: the HOUSE RULES block (item 11) is a copy of content/notes_rules.py, the one source shared with // the notes writer (content/notes_brief.py) and the API (company_apinotes). Change the rules THERE, then run // python3 content/notes_rules.py --write; content/tests_notes_check.py fails while this copy differs. const ST_PROMPT = `You are a PhD-level lecturer with 20 years experience writing world-class TVET textbooks for Profplanet, Kenya's premium TVET learning platform. You are completing the notes for ONE topic so that they match the CDACC curriculum exactly. Below are: CURRICULUM TOPICS (this topic, every numbered curriculum item under it, and the curriculum points listed under each item, indented by level) and CURRENT NOTES (this topic's notes in Markdown, exactly as on the platform). Return the WHOLE topic's notes, complete and ready to paste back: 1. Start with the topic's heading exactly as it is now, followed by its opening paragraph. 2. EVERY numbered curriculum item must have its own heading with its exact number and title, at its level: ## for X.Y, ### for X.Y.Z, #### for X.Y.Z.W, ##### for deeper, in curriculum order. Never rename, merge or renumber a curriculum item, and never replace the curriculum's list with a different one. 3. Lines marked "(curriculum point)" are points the curriculum lists under the item above them. Each point gets its own unnumbered heading with the point's exact words, as a #### heading under a ## or ### item, or ##### under a #### item, placed straight after the item's opening paragraph, in the curriculum's order. 4. ONE SECTION PER THING: if the current notes already cover a curriculum item or point under a different heading or wording (for example "Government and Regulatory Bodies" for the point "Government agencies", or "Employee Expertise and Experience" for "Personnel"), move that text under the curriculum's heading and merge it with anything already written there. Never leave two sections on the same thing. A heading that covers two points (for example "Accessibility and Format") is split into one section per point. Before you finish, read the whole topic again: each idea, list item and example must appear only ONCE. If an extra section repeats what a curriculum section says, keep it in the curriculum section and delete it from the extra; delete leftover lines that repeat a section, too. Never repeat content that belongs to another topic of this chapter (listed under OTHER TOPICS IN THIS CHAPTER): leave it to that topic. 5. Keep existing text, tables, images and their links, and the practice questions block at the end. Sections that are not in the curriculum (extras) are welcome when they add relevant material the curriculum does not mention: the curriculum is the minimum, not the limit. But nothing is said twice: anything an extra repeats from a curriculum section is merged into that curriculum section, and the extra keeps only what is genuinely additional. An extra left with nothing new is dropped. An extra about another topic of this chapter is left to that topic. Kept extras go under unnumbered #### headings inside the curriculum item they belong to (or after this topic's last curriculum item), with titles that say what they add. An extra never gets a number: a numbered heading that is not a curriculum item shows as a stray sub-topic and the save refuses it. 6. Every curriculum heading must have text under it. Write each missing or empty section to the platform's standard: open with 2 to 4 sentences of developed explanation (what it is, why it matters, how it works in practice) with a concrete example. Named groups of points go under an unnumbered heading one level below. Lists have at least 5 points, each explained in 1 to 3 sentences. Procedures are numbered steps (at least 6), each explained. Tools and equipment get a | Name | Specification | Use | table first. 7. Style: no em dashes (use commas or colons). Write "Example:", never "Kenyan Example". Do not bold words inside paragraph text; bold only the label at the start of a list item and table header cells. Use Ksh. Examples come from the workplaces this unit trains for (a welding workshop for welding, a salon for cosmetology, a hotel or restaurant for catering); vary them within that trade, never borrow settings from unrelated fields, and do not name real organisations or invent incidents. No summary sentence after a list. Tables only for specifications or genuine comparisons. 8. Images: keep every image exactly as it is in the current notes, character for character (the Markdown image line or <img>/<figure>/<div> tag, its link and its caption), in the same place relative to its text. Never remove, describe, move or alter an image. 9. Keep every HTML block exactly (for example the <div class="practice-qs" markdown="1"> practice questions block), including its opening and closing tags. 10. Formulas and calculations: write formulas as plain text with ordinary symbols (×, ÷, ², √, =), never LaTeX notation, because the platform does not display it. Show calculations as formula, substitution, working, then the result with units, each on its own line. 11. HOUSE RULES, they apply to every topic: - The CURRICULUM TOPICS list is the source of truth: write about each listed item and point, with its exact heading, and nothing the list contradicts. The curriculum is the minimum, not the limit. - Do not repeat content of the other topics of the chapter or unit: leave it to them. When sibling topics split one subject (Documents / Content of documents; Measurement / Benefits / Key metrics; Order consolidation / Definition / Methods), each covers only what its own title names; a later topic that meets a term again refers back in one sentence instead of defining it again. Refer to another topic by its title, never by its number ("see Netiquette principles", not "see 4.1"). - A "Types of" or list topic covers only the listed items: do not add other product families or categories as extra sections (they belong to other topics of the unit); delete any such section already in the notes. - Content from another unit or learning outcome (post-harvest or marketing material in a nursery topic, processing tools in a planting topic, another unit's practicals) is removed, not kept. - Heading levels: ## for the topic (X.Y), ### for each numbered sub-topic (X.Y.Z), #### for X.Y.Z.W and for every other heading inside the topic. ### is only for a numbered sub-topic. - A numbered heading is used ONLY for a number in the CURRICULUM TOPICS list or a topic row of this topic. Never add any other numbered heading: extra material goes under unnumbered #### headings inside the curriculum item it belongs to. Old notes headings are not curriculum items. - Sections of the CURRENT NOTES that carry a number not in the list are old generated sections: keep their useful, correct content as unnumbered #### headings inside the right curriculum item, or drop them; never keep their numbers and never refer to them by number. - When CDACC skips a parent number (it prints 2.2.1.1 with no 2.2.1 above it), the parent that the list gives is still written as its own heading at its level. - Headings go no deeper than ####, except a curriculum point one level below a #### sub-item, which is a ##### heading with the point's words. Below that use a short lead-in line. - Every curriculum heading keeps its exact number and the wording and capitals of the topic rows and outline (Labelling, not Labeling). Every heading the save requires must be present. - Correct CDACC misspellings and slips in headings and text, never copy them (Fibre, Crescent, packer, Crochet, Composting, Filing, Deglutition, trial balance, auger not augur, shade nets not shed nets, and/or not and/ or). Capitalise the first word of every heading. - Never end a heading with a full stop. - Never copy box, tick or bullet signs from the curriculum into headings or text. - Content about the whole topic goes under the ## heading before its first ### sub-topic, never inside the last sub-topic. Opening paragraphs go straight to the subject, with no filler about workplaces outside the trade. - A "Practice" line of the curriculum is a task, not a heading. - Do not write practice questions or a Practice Questions heading in the notes: they live in a separate store and the platform shows them in a box at the end of the topic. An existing practice questions block is kept exactly as it is. - British spelling everywhere, headings included: colour, flavour, behaviour, labour, fibre, centre, litre, programme (a software program keeps program), labelling, levelling, modelling, and -ise forms in headings (Sterilising, Sanitiser) and body text (organise, analyse, optimise, utilise, minimise, fertiliser). - No bold inside sentences. Bold only a short label at the start of a list item, written with the colon inside the bold and in sentence case (**Washing machines:**), and table header cells. The opening words of a numbered step are not bolded unless they are a label ending in a colon. - No LaTeX (no $ or $$ maths, no frac, times or sqrt commands): the platform shows it as raw code. Write formulas as plain text with ordinary symbols (×, ÷, ², √, =). Write negation in plain words or a single overbar letter (NOT (A + B), Ā), never a trailing overline character. Write a matrix in a fenced code block with its columns aligned. Tally marks have exactly as many strokes as the frequency, grouped in fives. - No em dashes: use a colon, a comma or a full stop. - Leave a blank line before every list. - Join words the curriculum wraps with a hyphen (non-metals, not non- metals). - Every example, setting and procedure comes from the workplaces this unit and course train for, not from the topic title read on its own. Do not use SACCOs, banks, county offices, hospitals, schools or churches as example settings unless the unit is about them. - Examples stay hypothetical ("a bakery may..."): never invented events, incidents, cases or fines, and never real companies, hotels, hospitals, banks or towns named as evidence. - Do not invent standards, regulations, codes, statistics, prices, figures or brand claims: when unsure, teach the general principle instead. Never claim what is "most common", "required by Kenyan law" or "popular in Kenya" unless it is certain. No unproven superlatives ("most used", "holds attention longest", "remember better"): say "widely used", "helps attention". - Facts, formulas and procedures must be correct for a Kenyan TVET learner at this level, and every example must make physical and dimensional sense. - Recompute every worked example before you finish: it must agree with the tables and rules in the same notes. Carry full precision through the working and round only the final answer. - Single-use items (lancets, needles, syringes, disposable gloves) are never described as cleaned or reused. - Product facts must be current: retired products are named as retired (Skype was retired in May 2025; Microsoft Publisher support ended in October 2026; use Microsoft Teams, Word, Canva, Scribus or another current tool for examples; Adobe Animate is in maintenance mode since February 2026; Premiere Rush was replaced by the free Premiere mobile app on iPhone and Android; Final Cut Pro and Motion are also in Apple Creator Studio since January 2026; Affinity by Canva is free; Premiere Pro 2025 removed the Essential Graphics panel: titles are edited in the Properties panel, templates in the Graphics Templates panel). Affinity Publisher, Designer and Photo were withdrawn in September 2025 (one free Affinity app by Canva); Microsoft 365 apps list Windows 11 (1.6 GHz 2-core), Office 2024 a 1.1 GHz 2-core CPU; on Windows 10 they get security fixes only, to October 2028. Kenyan public procurement runs on the National Treasury e-GP system (main platform since July 2025, its mandatory use contested in court); IFMIS remains for budgets and payments. Check any product claim made after mid-2025 rather than writing it from memory. - Laws and codes must be the ones in force: Kenya's media code is the Code of Conduct for Media Practice, 2025 (Legal Notice 88 of 2025, under the Media Council Act; it replaced the Code of Conduct for the Practice of Journalism and applies to journalists, media practitioners and media enterprises; watershed 5.00 am to 10.00 pm); programme content standards rest with the Media Council of Kenya, not the Communications Authority. Check any law or code by name before citing it, and check what it says before writing that it permits or requires something: the Alcoholic Drinks Control Act, 2010 forbids selling alcohol by automatic vending machine (section 30(1)) and selling or supplying it to anyone under 18 (section 28(1)). - Notes speak to the learner about the subject: never mention CDACC, the curriculum document, its misprints or its numbering. Do not reuse one example across topics of a unit: vary them. - Metric units only. Money is in Ksh. - Cite a Kenyan Act only by its enacted title and year as published on Kenya Law. A Bill is not an Act: the Food and Feed Safety Control Co-ordination Bill (National Assembly Bill No. 21 of 2023) is not the "Food and Feed Safety Control Coordination Act, 2023". If only a Bill is found, or an Act's title and start date cannot be confirmed, name the laws already in force instead (for food: the Public Health Act and the Food, Drugs and Chemical Substances Act, enforced by county public health officers). - One colour-coded chopping board scheme everywhere: red raw meat and raw poultry, blue raw fish, yellow cooked meat, brown raw vegetables, green salad and fruit, white bakery and dairy. Hot holding at 63 °C or above. Waste: hazardous waste (chemicals, used oil, batteries, fluorescent tubes) is not the same as sharps (broken glass, blades); check each material's degradability (cellophane is biodegradable; plastic film is not). END OF HOUSE RULES. 12. OUTPUT FORMAT, CRITICAL: return the topic's raw Markdown inside ONE code block that starts with a line \`\`\`\`markdown and ends with a line \`\`\`\` (four backticks), so it can be copied with the code block's Copy button exactly as written. Nothing before the block. After the block, write one line: "Removed as off-topic:" followed by the headings you left out, or "none".`; async function structClaude(id) { const t = stTopic(id), i0 = ST.topics.indexOf(t), pre = t.number + '.', lines = [], seen = new Set(); for (let k = i0; k < ST.topics.length; k++) { const x = ST.topics[k]; if (k > i0 && x.number && !x.number.startsWith(pre)) break; if ((x.notes_only && !x.in_curriculum) || x.in_curriculum === false) continue; const ind = ' '.repeat(Math.max(0, (x.depth || 1) - (t.depth || 1))); if (!x.number && seen.has(stNorm(x.title))) continue; lines.push(ind + (x.number ? x.number + ' ' : '(curriculum point) ') + (x.curriculum_title || x.title)); (x.points || []).forEach(q => { seen.add(stNorm(q.text)); lines.push(ind + ' (curriculum point) ' + q.text); }); } const others = ST.topics.filter(x => x.id && x.number && x !== t && !x.number.startsWith(pre) && x.in_curriculum !== false).map(x => x.number + ' ' + (x.curriculum_title || x.title) + ((x.points || []).length ? ': ' + x.points.map(q => q.text).join(', ') : '')); const r = await api(`/api/topic/${id}/note/`); const text = ST_PROMPT + '\n\nCURRICULUM TOPICS:\n' + lines.join('\n') + '\n\nOTHER TOPICS IN THIS CHAPTER (their content belongs there, do not repeat it):\n' + others.join('\n') + '\n\nCURRENT NOTES:\n' + ((r && r.content_md) || '(this topic has no notes yet)'); try { await navigator.clipboard.writeText(text); toast('✅ Copied ' + t.number + ': paste it into Claude'); } catch (e) { openModal('Copy this into Claude', `<textarea style="width:100%;height:320px;font-size:12px;">${stEsc(text)}</textarea>`, closeModal); } } function curAdd(id) { const t = stTopic(id); openModal('Did our curriculum copy miss ' + t.number + '?', `<div style="font-size:13px;margin-bottom:8px;">Only if the curriculum pages show <b>${stEsc(t.number)}</b> and our saved copy missed it. This corrects the copy the check uses; the topic itself is not changed. Everything is recorded.</div>` + inp('cfTitle', 'Title exactly as in the curriculum pages', t.title) + inp('cfWhy', 'Why: what the pages show', ''), () => structOp({op: 'cur_add', number: t.number, title: g('cfTitle'), reason: g('cfWhy')})); } function curRetitle(id) { const t = stTopic(id); openModal('Correct the curriculum copy for ' + t.number, `<div style="font-size:13px;margin-bottom:8px;">Our saved copy says "<b>${stEsc(t.curriculum_title)}</b>". Correct it only if the curriculum pages say otherwise. If it is the topic list that is wrong, use ✎ on the topic instead.</div>` + inp('cfTitle', 'Title exactly as in the curriculum pages', t.curriculum_title) + inp('cfWhy', 'Why: what the pages show', ''), () => structOp({op: 'cur_retitle', number: t.number, title: g('cfTitle'), reason: g('cfWhy')})); } function structAttached(body, r) { const what = Object.entries(r.attached || {}).map(([k, n]) => n + ' ' + k).join(', '); openModal('Something is attached', `<div style="font-size:13px;margin-bottom:8px;">This topic has <b>${stEsc(what)}</b> attached. What should happen to them?</div>` + (r.target ? `<label class="st-opt"><input type="radio" name="stAtt" value="move"> Move them to <b>${stEsc(r.target)}</b></label>` : '') + `<label class="st-opt"><input type="radio" name="stAtt" value="delete" checked> Delete them with the topic (flags and lessons are only detached)</label>` + `<div style="font-size:12px;color:#6B7B8D;margin-top:6px;">Undo restores everything, these included.</div>`, () => structOp(Object.assign({}, body, {attached: (document.querySelector('input[name=stAtt]:checked') || {}).value}))); } const stNorm = s => String(s || '').toLowerCase().replace(/[^a-z0-9]/g, ''); function structChapterTitle() { const o = String(ST.curriculum.outcome || '').trim().replace(/\.+$/, ''); openModal('Rename the chapter', `<div style="font-size:13px;margin-bottom:8px;">Only if the content table prints this wording.<br>Now: <b>${stEsc(ST.chapter_title)}</b><br>Our copy: <b>${stEsc(ST.curriculum.outcome)}</b></div>` + inp('stCT', 'Chapter title, exactly as the content table prints the outcome', o), () => structOp({op: 'chapter_title', title: g('stCT')})); } function curOutcome() { openModal('Correct our curriculum copy', `<div style="font-size:13px;margin-bottom:8px;">Our copy says "<b>${stEsc(ST.curriculum.outcome)}</b>"; the chapter says "<b>${stEsc(ST.chapter_title)}</b>". Correct the copy only if the content table prints the chapter's wording. The chapter is not changed.</div>` + inp('cfTitle', 'Outcome exactly as the content table prints it', ST.chapter_title) + inp('cfWhy', 'Why: what the pages show', ''), () => structOp({op: 'cur_outcome', title: g('cfTitle'), reason: g('cfWhy')})); } function structUndo() { if (confirm('Undo the last structure change on this chapter?')) structOp({op: 'undo'}); } // After a change: fetch the page quietly, swap in the fresh notes, redraw the topic list, and keep both scroll // positions, instead of reloading and throwing the editor back to the top. Any failure falls back to a reload. async function stRefresh(msg) { const sb = document.querySelector('.two-col > .sidebar'), y = window.scrollY, sy = sb ? sb.scrollTop : 0; try { const html = await (await fetch(location.href, {credentials: 'same-origin', cache: 'no-store'})).text(); const fresh = new DOMParser().parseFromString(html, 'text/html').querySelector('.notes-body'); const cur = document.querySelector('.notes-body'); if (!fresh || !cur) throw new Error('no notes'); cur.innerHTML = fresh.innerHTML; if (window.MathJax && MathJax.typesetPromise) { try { await MathJax.typesetPromise([cur]); } catch (e) {} } await structLoad(); window.scrollTo(0, y); if (sb) sb.scrollTop = sy; if (typeof toast === 'function') toast('Saved: ' + (msg || 'done')); } catch (e) { try { sessionStorage.setItem('pp_struct_reopen', msg || '1'); } catch (e2) {} location.reload(); } } async function structOp(body) { const r = await api(`/api/chapter/${CHAPTER_ID}/structure/`, 'POST', body); if (!r.ok && r.code === 'children') { if (confirm('Remove it together with its sub-topics?\n\n' + (r.children || []).join('\n') + '\n\nTheir notes sections are deleted too. Undo restores everything.')) structOp(Object.assign({}, body, {branch: true, notes: 'delete'})); return; } if (!r.ok && r.code === 'attached') { structAttached(body, r); return; } if (!r.ok) { alert(r.error || 'That did not work.'); return; } if (typeof closeModal === 'function') { try { closeModal(); } catch (e) {} } stRefresh(r.message); } (function () { const orig = window.toggleEditMode; if (typeof orig !== 'function') return; window.toggleEditMode = function () { orig(); const on = typeof editMode !== 'undefined' && editMode; const box = document.getElementById('structTree'); if (box) box.style.display = on ? 'block' : 'none'; document.querySelectorAll('.sidebar-body > .sidebar-item').forEach(el => el.style.display = on ? 'none' : 'flex'); const ab = document.getElementById('addTopicBtn'); if (ab) ab.style.display = 'none'; if (on && box) structLoad(); }; let msg = null; try { msg = sessionStorage.getItem('pp_struct_reopen'); sessionStorage.removeItem('pp_struct_reopen'); } catch (e) {} if (msg && document.getElementById('editModeBtn')) { const go = () => { window.toggleEditMode(); if (msg !== '1') toast('✅ ' + msg); }; if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', go); else setTimeout(go, 0); } })(); // ── Verify ── function toggleVerify(chapterId) { fetch(`/chapter/${chapterId}/verify/`,{method:'POST',headers:{'X-CSRFToken':CSRF,'Content-Type':'application/json'}}) .then(r=>r.json()).then(data=>{ const btn=document.getElementById('verifyBtn'); if(data.verified){btn.textContent='✅ Verified';btn.style.background='#E8F5E9';btn.style.color='#1E8449';btn.style.borderColor='#1E8449';} else{btn.textContent='☐ Mark Verified';btn.style.background='white';btn.style.color='#6B7B8D';btn.style.borderColor='#CBD5E0';} if(data.all_verified&&data.canonical){alert(data.canonical.is_new_canonical?'🎯 Unit is now canonical!':'🔗 Linked to existing canonical unit.');} }); } // Image delete functionality function deleteImage(btn) { const imgPath = btn.getAttribute('data-path'); const chapterId = btn.getAttribute('data-chapter'); if (!confirm('Remove this image from the notes?')) return; fetch('/api/delete-image/', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({chapter_id: chapterId, image_path: imgPath}) }) .then(r => r.json()) .then(data => { if (data.ok) { btn.closest('.img-wrapper').remove(); } else { alert('Error: ' + (data.error || 'Unknown error')); } }); } // Wrap all images in notes-body with delete button overlay (edit mode only) function wrapImagesWithDelete() { const chapterId = typeof CHAPTER_ID !== 'undefined' ? CHAPTER_ID : null; if (!chapterId) return; if (typeof CAN_EDIT === 'undefined' || !CAN_EDIT) return; // only authorized editors get delete buttons document.querySelectorAll('.notes-body img').forEach(img => { if (img.closest('.img-wrapper')) return; const wrapper = document.createElement('div'); wrapper.className = 'img-wrapper'; wrapper.style.cssText = 'position:relative;display:inline-block;max-width:100%;'; img.parentNode.insertBefore(wrapper, img); wrapper.appendChild(img); const btn = document.createElement('button'); btn.innerHTML = '✕ Remove'; btn.className = 'img-delete-btn'; btn.setAttribute('data-path', img.getAttribute('src')); btn.setAttribute('data-chapter', chapterId); btn.onclick = function() { deleteImage(this); }; btn.style.cssText = 'display:none;position:absolute;top:4px;right:4px;background:var(--accent);color:white;border:none;padding:3px 8px;border-radius:4px;cursor:pointer;font-size:12px;z-index:10;'; wrapper.appendChild(btn); }); } // Show/hide delete buttons based on edit mode document.addEventListener('DOMContentLoaded', function() { wrapImagesWithDelete(); // no-op unless CAN_EDIT }); </script> <script> (function(){ var fcEl=document.getElementById('sa-fc-data'); var mcEl=document.getElementById('sa-mcq-data'); if(fcEl){ var FC=JSON.parse(fcEl.textContent), i=0, flipped=false, order=FC.map(function(_,k){return k;}); var card=document.getElementById('sa-fc-card'),side=document.getElementById('sa-fc-side'),front=document.getElementById('sa-fc-front'),back=document.getElementById('sa-fc-back'),q=document.getElementById('sa-fc-q'),num=document.getElementById('sa-fc-num'),pts=document.getElementById('sa-fc-points'),cnt=document.getElementById('sa-fc-count'),dots=document.getElementById('sa-fc-dots'),strip=document.getElementById('sa-fc-strip'),cue=document.getElementById('sa-fc-cue'); function render(){ var c=FC[order[i]]; num.textContent=(i+1); q.textContent=c.q; pts.innerHTML=''; (c.points||[]).forEach(function(pt){ var li=document.createElement('li'); li.style.cssText='display:flex;gap:9px;align-items:flex-start;font-size:15px;line-height:1.5;color:var(--primary);'; li.innerHTML='<span style="color:#1E8449;font-size:17px;flex-shrink:0;">\u2713</span><span>'+pt+'</span>'; pts.appendChild(li); }); side.textContent=flipped?'Answer':'Question'; front.style.display=flipped?'none':'block'; back.style.display=flipped?'block':'none'; strip.style.background=flipped?'#E8F5E9':'#EBF2FA'; side.style.color=flipped?'#1E8449':'var(--primary)'; card.style.borderColor=flipped?'#1E8449':'#2E86C1'; num.style.background=flipped?'#1E8449':'#2E86C1'; cue.style.background=flipped?'#1E8449':'#2E86C1'; cue.innerHTML=flipped?'\u21bb Tap to see question':'\u21bb Tap card to reveal answer'; cnt.textContent=(i+1)+' / '+FC.length; dots.innerHTML=''; order.forEach(function(_,k){var d=document.createElement('div');d.style.cssText='flex:1;height:4px;border-radius:999px;background:'+(k===i?'#2E86C1':'#D6E2EE')+';';dots.appendChild(d);}); } card.onclick=function(){flipped=!flipped;render();}; window.saFcNext=function(){i=(i+1)%FC.length;flipped=false;render();}; window.saFcPrev=function(){i=(i-1+FC.length)%FC.length;flipped=false;render();}; window.saFcShuffle=function(){for(var k=order.length-1;k>0;k--){var j=Math.floor(Math.random()*(k+1));var t=order[k];order[k]=order[j];order[j]=t;}i=0;flipped=false;render();}; render(); } if(mcEl){ var MC=JSON.parse(mcEl.textContent), mount=document.getElementById('sa-quiz-mount'), score=0, answered=0; var pctEl=document.getElementById('sa-quiz-pct'), scoreEl=document.getElementById('sa-quiz-score'), barEl=document.getElementById('sa-quiz-bar'); MC.forEach(function(item,qi){ var wrap=document.createElement('div'); wrap.style.cssText='background:#fff;border:1px solid #E0E4E8;border-radius:12px;padding:16px 18px;margin-bottom:12px;'; var qp=document.createElement('p'); qp.textContent=(qi+1)+'. '+item.q; qp.style.cssText='font-size:15px;margin:0 0 12px;line-height:1.5;color:var(--primary);font-weight:600;'; wrap.appendChild(qp); item.options.forEach(function(opt,oi){ var b=document.createElement('button'); b.type='button'; b.textContent=opt; b.style.cssText='display:block;width:100%;text-align:left;margin-bottom:6px;padding:11px 14px;font-size:14px;border:1px solid #CBD5E0;border-radius:10px;background:#fff;cursor:pointer;color:var(--primary);transition:background .15s;'; b.onmouseover=function(){if(!wrap.dataset.done)b.style.background='#F4F8FC';}; b.onmouseout=function(){if(!wrap.dataset.done)b.style.background='#fff';}; b.onclick=function(){ if(wrap.dataset.done) return; wrap.dataset.done='1'; answered++; Array.prototype.forEach.call(wrap.querySelectorAll('button'),function(bb,bi){ bb.style.cursor='default'; if(bi===item.answer){bb.style.background='#E8F5E9';bb.style.borderColor='#1E8449';bb.style.color='#1E8449';bb.style.fontWeight='600';} else if(bi===oi){bb.style.background='#FDEDEC';bb.style.borderColor='var(--accent)';bb.style.color='#C0392B';} }); if(oi===item.answer) score++; if(item.why){var ex=document.createElement('p');ex.textContent=item.why;ex.style.cssText='font-size:13px;color:#6B7B8D;margin:8px 0 0;padding-top:8px;border-top:1px solid #EAEDF0;';wrap.appendChild(ex);} var pct=Math.round(score/MC.length*100); pctEl.textContent=Math.round(answered/MC.length*100>=0?score/MC.length*100:0)+'%'; pctEl.textContent=Math.round(score/MC.length*100)+'%'; scoreEl.textContent=score+' / '+MC.length; barEl.style.width=(answered/MC.length*100)+'%'; if(answered===MC.length){ var fp=Math.round(score/MC.length*100); var d=document.getElementById('sa-quiz-done'); d.style.display='block'; var msg, bg, col; if(fp>=80){msg='Excellent! You scored '+fp+'% ('+score+'/'+MC.length+'). You\'ve mastered this chapter.';bg='#E8F5E9';col='#1E8449';} else if(fp>=50){msg='Good effort \u2014 '+fp+'% ('+score+'/'+MC.length+'). Review the flashcards and try again to improve.';bg='#FEF9E7';col='var(--accent)';} else{msg='You scored '+fp+'% ('+score+'/'+MC.length+'). Go through the notes and flashcards, then retake the quiz.';bg='#FDEDEC';col='#C0392B';} d.textContent=msg; d.style.background=bg; d.style.color=col; // persist the score (best-effort; never blocks the UI) try { fetch('/chapter/' + CHAPTER_ID + '/quiz-record/', { method:'POST', credentials:'same-origin', headers:{'Content-Type':'application/json','X-CSRFToken':(document.cookie.match(/csrftoken=([^;]+)/)||[])[1]||''}, body: JSON.stringify({tool_kind:'test_yourself', score:score, max_score:MC.length, pct:fp}) }).catch(function(){}); } catch(e){} } }; wrap.appendChild(b); }); mount.appendChild(wrap); }); } })(); </script> </div> <footer class="site-footer"> <div class="ft-in"> <div class="ft-brand"> <img class="ft-logo-img" src="/static/img/logo.png?v=20260927" alt="Profplanet"> <p>CDACC notes, past papers and practicals for Kenyan TVET, written to the curriculum and kept accurate by the trainers who teach from them.</p> </div> <div class="ft-col"> <h4>Notes and papers</h4> <a href="/courses/">CDACC notes by course</a> <a href="/past-papers/">CDACC past papers</a> </div> <div class="ft-col"> <h4>For trainers</h4> <a href="/#what-you-get">Professional documents</a> <a href="/#how">How trainers earn access</a> </div> <div class="ft-col"> <h4>Account</h4> <a href="/login/">Log in</a> <a href="/signup/">Create account</a> </div> </div> <div class="ft-base"> <span>© 2026 Profplanet · CDACC notes and past papers for Kenyan TVET</span> <span class="ft-base-legal"> <a href="/privacy/">Privacy notice</a> <a href="/terms/">Terms of use</a> </span> <span>Past papers belong to their respective owners.</span> </div> </footer> <style> /* Gold is a light colour: white text on it is hard to read on a phone in daylight, so the button carries the dark brand blue instead. */ .hdr-cta { background: var(--accent); color: var(--primary) !important; font-weight: 700; } .hdr-cta:hover { background: var(--primary); color: var(--accent) !important; } .hdr-login { color: #fff !important; font-weight: 600; } .hdr-signup { background: var(--accent); color: var(--primary) !important; padding: 10px 22px !important; border-radius: 999px; font-weight: 700; margin-left: 8px; box-shadow: 0 4px 14px -4px rgba(200,127,10,.6); transition: transform .14s ease, box-shadow .16s ease, filter .16s ease; } .hdr-signup:hover { background: var(--primary); color: var(--accent) !important; transform: translateY(-1px); box-shadow: 0 6px 18px -6px rgba(5,10,48,.55); } .site-footer { margin-top: 0; background: #0F2032; color: #8C9BAC; border-top: 3px solid var(--accent); } .ft-in { max-width: 1160px; margin: 0 auto; padding: 56px 40px 40px; display: grid; grid-template-columns: 1.7fr 1fr 1fr 1fr; gap: 40px; } /* Four link columns now, not three. Between 640 and 900 the four tracks squeeze link text onto two lines, so drop to two columns there. */ @media (max-width: 900px) { .ft-in { grid-template-columns: 1fr 1fr; gap: 32px 28px; } } .ft-brand .ft-logo-img { display: block; height: 42px; width: auto; } .ft-brand p { font-size: 14px; line-height: 1.75; margin: 14px 0 0; max-width: 360px; color: #8C9BAC; } /* The one place --brand-cyan works: a small uppercase label on a dark ground, where the contrast that rules it out for body text does not apply. It is the logo's own colour and gives the footer an identity of its own. */ .ft-col h4 { font-size: 11px; text-transform: uppercase; letter-spacing: 1.8px; color: var(--brand-cyan); margin: 0 0 16px; } .ft-col a { display: block; font-size: 14.5px; color: #AAB8C7; text-decoration: none; padding: 7px 0; transition: color .14s ease, padding-left .14s ease; } .ft-col a:hover { color: #fff; padding-left: 4px; } .ft-base { border-top: 1px solid rgba(255,255,255,.1); padding: 22px 40px; display: flex; justify-content: space-between; flex-wrap: wrap; gap: 8px; max-width: 1160px; margin: 0 auto; font-size: 12.5px; color: #6B7B8D; } .ft-base-legal a { color: #8C9BAC; text-decoration: none; margin-right: 18px; } .ft-base-legal a:hover { color: #fff; } @media (max-width: 640px) { .ft-base { flex-direction: column; text-align: center; gap: 10px; } } @media (max-width: 640px) { .ft-in { grid-template-columns: 1fr; gap: 24px; } } </style> </body> </html>