Snapify

Let Snapify reveal KIIT answer sheets with a simple traversal trick

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Snapify
// @version      1.0
// @description  Let Snapify reveal KIIT answer sheets with a simple traversal trick
// @author       Prajwal Panth
// @license      MIT
// @match        http://btecheval.kiitresults.com/midfeb2025stview/cs/ImageDisplay.aspx*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @icon         https://www.google.com/s2/favicons?sz=64&domain=kiit.ac.in
// @require      https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js
// @namespace https://greasyfork.org/users/1460116
// ==/UserScript==

GM_addStyle(`
  :root {
    /* Refined Color Palette (Inspired by Bolt/Shadcn) */
    --background: #ffffff; /* White */
    --foreground: #111827; /* Gray 900 */
    --card: #ffffff;
    --card-foreground: #111827;
    --popover: #ffffff;
    --popover-foreground: #111827;
    --primary: #2563eb; /* Blue 600 */
    --primary-foreground: #ffffff; /* White */
    --secondary: #f3f4f6; /* Gray 100 */
    --secondary-foreground: #1f2937; /* Gray 800 */
    --muted: #f9fafb; /* Gray 50 */
    --muted-foreground: #6b7280; /* Gray 500 */
    --accent: #e5e7eb; /* Gray 200 */
    --accent-foreground: #111827; /* Gray 900 */
    --destructive: #dc2626; /* Red 600 */
    --destructive-foreground: #ffffff; /* White */
    --border: #e5e7eb; /* Gray 200 */
    --input: #e5e7eb; /* Gray 200 */
    --input-foreground: #111827;
    --ring: #3b82f6; /* Blue 500 - Focus Ring */

    /* Constants */
    --radius: 0.375rem; /* Slightly smaller radius */
    --font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";

    /* Shadows */
    --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
    --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
    --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
    --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
  }

  .dark {
    --background: #111827; /* Gray 900 */
    --foreground: #f9fafb; /* Gray 50 */
    --card: #1f2937; /* Gray 800 */
    --card-foreground: #f9fafb; /* Gray 50 */
    --popover: #1f2937; /* Gray 800 */
    --popover-foreground: #f9fafb; /* Gray 50 */
    --primary: #3b82f6; /* Blue 500 */
    --primary-foreground: #ffffff; /* White */
    --secondary: #374151; /* Gray 700 */
    --secondary-foreground: #f3f4f6; /* Gray 100 */
    --muted: #374151; /* Gray 700 */
    --muted-foreground: #9ca3af; /* Gray 400 - Lightened */
    --accent: #4b5563; /* Gray 600 */
    --accent-foreground: #f9fafb; /* Gray 50 */
    --destructive: #ef4444; /* Red 500 */
    --destructive-foreground: #ffffff; /* White */
    --border: #374151; /* Gray 700 */
    --input: #374151; /* Gray 700 */
    --input-foreground: #f9fafb;
    --ring: #60a5fa; /* Blue 400 - Lighter Focus Ring */
  }

  /* Base styles */
  body {
    font-family: var(--font-sans);
    background-color: var(--background);
    color: var(--foreground);
    margin-top: 20px !important; /* Ensure space from top */
  }
  #Form1 {
       margin-top: 20px;
  }
  #cimg {
    display: block;
    max-width: 90%; /* Give some breathing room */
    margin: 20px auto;
    border: 1px solid var(--border);
    box-shadow: var(--shadow);
    border-radius: var(--radius);
    transition: transform 0.2s ease, max-width 0.3s ease, margin 0.3s ease;
  }

  /* Main UI Panel */
  .kitee-ui {
    position: fixed;
    top: 20px;
    right: 20px;
    background-color: var(--card);
    border: 1px solid var(--border);
    box-shadow: var(--shadow-lg);
    padding: 16px;
    width: 340px; /* Preferred width */
    max-width: calc(100vw - 40px); /* Prevent overflow */
    font-family: var(--font-sans);
    font-size: 14px;
    color: var(--card-foreground);
    border-radius: var(--radius);
    z-index: 9999;
    transition: all 0.3s ease-in-out;
    overflow: hidden;
    display: flex; /* Use flex for main UI structure */
    flex-direction: column; /* Stack toggle button and content */
  }

  .kitee-ui.collapsed {
    width: 44px; /* Match toggle button size */
    height: 44px;
    padding: 0;
    box-shadow: var(--shadow-md);
    border-radius: 50%;
    /* When collapsed, toggle button is the only content */
    overflow: visible; /* Allow tooltip to show */
  }

   /* Toggle Button needs careful positioning */
   .toggle-btn {
     position: absolute; top: 6px; left: 6px; /* Position inside padding */
     width: 32px; height: 32px; display: flex; align-items: center; justify-content: center;
     font-size: 20px; cursor: pointer; background-color: transparent; /* Transparent */
     border: none; border-radius: var(--radius); color: var(--muted-foreground);
     z-index: 10; transition: background-color 0.15s ease, color 0.15s ease;
     /* Take it out of normal flow */
     flex-shrink: 0; /* Prevent shrinking */
   }
    .toggle-btn:hover { background-color: var(--secondary); color: var(--secondary-foreground); }
    .toggle-btn:focus-visible { outline: 2px solid var(--ring); outline-offset: 1px; background-color: var(--secondary); }

   .kitee-ui.collapsed .toggle-btn {
       /* Reset position when collapsed - becomes the main element */
       position: static; /* Back to normal flow inside the collapsed container */
       width: 100%; height: 100%; /* Fill collapsed container */
       background-color: var(--card);
       color: var(--card-foreground);
       box-shadow: none; /* Shadow is on the collapsed ui div */
       border: none; /* Border is on the collapsed ui div */
       border-radius: 50%;
       display: flex; align-items: center; justify-content: center;
   }
   .kitee-ui.collapsed .toggle-btn:hover { background-color: var(--secondary); }


  .kitee-ui .content-wrapper {
     opacity: 1;
     flex-grow: 1; /* Allow content to take remaining space */
     max-height: calc(85vh - 32px); /* Limit height, accounting for padding */
     overflow-y: auto;
     overflow-x: hidden;
     transition: opacity 0.2s ease-in-out 0.1s, max-height 0.3s ease-in-out;
     padding-right: 5px; /* Space for scrollbar */
     margin-top: 21px; /* Space for absolute positioned toggle button */
     display: flex; /* Use flex for content sections */
     flex-direction: column;
  }
   /* Scrollbar styling */
   .kitee-ui .content-wrapper::-webkit-scrollbar { width: 6px; }
   .kitee-ui .content-wrapper::-webkit-scrollbar-track { background: transparent; }
   .kitee-ui .content-wrapper::-webkit-scrollbar-thumb { background-color: var(--border); border-radius: 3px; }
   .kitee-ui .content-wrapper::-webkit-scrollbar-thumb:hover { background-color: var(--muted-foreground); }


   .kitee-ui.collapsed .content-wrapper {
      opacity: 0;
      max-height: 0;
      pointer-events: none;
      padding-right: 0;
      margin-top: 0; /* No margin needed when collapsed */
      flex-grow: 0; /* Don't take space */
      display: none; /* Completely remove from layout */
   }

  /* Header */
  .kitee-header {
    display: flex; align-items: center; justify-content: space-between;
    margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--border);
    flex-shrink: 0; /* Prevent shrinking */
  }
  .kitee-title {
    font-weight: 600; font-size: 16px; display: flex; align-items: center; gap: 8px;
  }
   .header-controls { display: flex; align-items: center; gap: 6px; }

  /* Sections */
  .kitee-ui section {
    margin-bottom: 16px; padding-bottom: 16px; border-bottom: 1px solid var(--border);
    flex-shrink: 0; /* Prevent sections from shrinking */
  }
  /* .kitee-ui section:last-of-type { Removed this - footer handles the last element now */
  /*   margin-bottom: 0; padding-bottom: 0; border-bottom: none; */
  /* } */

  /* Roll Number Section */
  .roll-display { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
  .roll-label {
    font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    flex-grow: 1; margin-right: 8px;
  }
  .roll-history { display: flex; flex-wrap: wrap; gap: 6px; max-height: 62px; overflow-y: auto; padding-top: 4px;}
  .roll-history-item {
    background-color: var(--secondary); color: var(--secondary-foreground); font-size: 12px;
    padding: 3px 8px; border-radius: calc(var(--radius) / 1.5); cursor: pointer;
    transition: background-color 0.15s ease, border-color 0.15s ease;
    border: 1px solid var(--border); white-space: nowrap;
  }
  .roll-history-item:hover { background-color: var(--accent); }
  .roll-history-item:focus-visible { outline: 2px solid var(--ring); outline-offset: 1px; }


  /* Button styles */
  .btn {
    display: inline-flex; align-items: center; justify-content: center;
    border-radius: var(--radius); font-weight: 500; font-size: 13px;
    height: 32px; padding: 0 10px;
    transition: background-color 0.15s ease, border-color 0.15s ease, opacity 0.15s ease, box-shadow 0.15s ease;
    cursor: pointer; border: 1px solid transparent; white-space: nowrap; gap: 6px;
    line-height: 1;
  }
  .btn:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
  .btn-primary { background-color: var(--primary); color: var(--primary-foreground); border-color: var(--primary); }
  .btn-primary:hover:not(:disabled) { background-color: #1d4ed8; border-color: #1d4ed8; }
  .dark .btn-primary:hover:not(:disabled) { background-color: #60a5fa; border-color: #60a5fa; }
  .btn-secondary { background-color: var(--secondary); color: var(--secondary-foreground); border-color: var(--border); }
  .btn-secondary:hover:not(:disabled) { background-color: var(--accent); }
  .btn-destructive { background-color: var(--destructive); color: var(--destructive-foreground); border-color: var(--destructive); }
  .btn-destructive:hover:not(:disabled) { background-color: #b91c1c; border-color: #b91c1c; }
  .dark .btn-destructive:hover:not(:disabled) { background-color: #f87171; border-color: #f87171; }
  .btn-icon { width: 32px; padding: 0; }
  .btn:disabled { opacity: 0.6; cursor: not-allowed; }

  /* Navigation */
  .page-section { display: flex; flex-direction: column; gap: 10px; }
  .page-nav { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
  .page-info { font-weight: 500; font-size: 13px; text-align: center; min-width: 90px; color: var(--muted-foreground); }
  .page-jump { display: flex; align-items: center; gap: 6px; }
  .page-jump-input {
    width: 55px; height: 32px; border-radius: var(--radius); border: 1px solid var(--border);
    background-color: var(--background); color: var(--input-foreground);
    padding: 0 8px; text-align: center; font-size: 13px;
  }
  .page-jump-input:focus { border-color: var(--ring); outline: 1px solid var(--ring); }
  .page-jump-input::-webkit-outer-spin-button, .page-jump-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
  .page-jump-input[type=number] { -moz-appearance: textfield; }
  .page-jump .btn { width: 32px; }

  /* Actions */
  .actions-section { display: flex; flex-wrap: wrap; gap: 8px; justify-content: space-between; }
  .actions-section .btn { flex-grow: 1; font-size: 12px; }
  .actions-section .btn-primary { flex-grow: 2; }


  /* Status Indicator */
  .status-indicator {
    display: flex; align-items: center; font-size: 12px; margin-top: 8px;
    color: var(--muted-foreground); height: 20px; transition: color 0.3s ease;
  }
  .loading-spinner {
    display: none; border: 2px solid var(--accent); border-top: 2px solid var(--primary);
    border-radius: 50%; width: 14px; height: 14px;
    animation: spin 1s linear infinite; margin-right: 6px;
  }
  .status-indicator.loading .loading-spinner { display: inline-block; }
  .status-indicator.loading .status-text { color: var(--primary); font-weight: 500;}
  .status-indicator.success .status-text { color: #16a34a !important; font-weight: 500; }
  .dark .status-indicator.success .status-text { color: #4ade80 !important; }
  .status-indicator.error .status-text { color: var(--destructive) !important; font-weight: 500; }


  /* Tooltip */
  .tooltip { position: relative; }
  .tooltip::after {
    content: attr(data-tooltip); position: absolute; bottom: 115%; left: 50%;
    transform: translateX(-50%); background-color: #1f2937; color: #f9fafb;
    padding: 4px 8px; border-radius: var(--radius); font-size: 11px;
    white-space: nowrap; z-index: 10000; box-shadow: var(--shadow-md);
    opacity: 0; visibility: hidden; transition: opacity 0.2s ease 0.1s, visibility 0.2s ease 0.1s;
    pointer-events: none;
  }
  .tooltip:hover::after { opacity: 1; visibility: visible; }

  /* Shortcuts List */
  .shortcuts-section { font-size: 12px; color: var(--muted-foreground); }
  .shortcuts-title { font-weight: 500; color: var(--card-foreground); margin-bottom: 8px; }
  .shortcuts-list { list-style-type: none; padding-left: 0; margin-top: 0; }
  .shortcuts-list li { margin-bottom: 6px; display: flex; justify-content: space-between; align-items: center; }
  .shortcuts-list kbd {
    font-family: inherit; background-color: var(--secondary); color: var(--secondary-foreground);
    padding: 2px 6px; border-radius: 4px; border: 1px solid var(--border);
    font-size: 11px; font-weight: 500; margin-left: 8px; box-shadow: var(--shadow-sm);
  }
  .shortcuts-list .key-combo { display: flex; gap: 4px; align-items: center; }

  /* NEW: Footer Styles */
  .kitee-footer {
    margin-top: 16px; /* Space above footer */
    padding-top: 12px; /* Space below last section */
    border-top: 1px solid var(--border);
    font-size: 11px;
    color: var(--muted-foreground);
    display: flex;
    align-items: center;
    justify-content: center; /* Center content */
    gap: 8px; /* Space between text and icon */
    flex-shrink: 0; /* Prevent shrinking */
  }
  .kitee-footer a {
    color: var(--muted-foreground);
    display: inline-flex; /* Align icon correctly */
    align-items: center;
    transition: color 0.15s ease;
  }
  .kitee-footer a:hover {
    color: var(--foreground); /* Make icon darker on hover */
  }
   .kitee-footer a:focus-visible {
      outline: 2px solid var(--ring);
      outline-offset: 2px;
      border-radius: var(--radius); /* Add radius to focus outline */
   }
  .kitee-footer svg {
    width: 16px;
    height: 16px;
  }

  /* Fullscreen Mode */
  body.fullscreen-mode #cimg {
    position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
    max-height: 95vh; max-width: 95vw; width: auto; height: auto; z-index: 9990;
    box-shadow: var(--shadow-lg); border: 2px solid var(--background); border-radius: 0;
    object-fit: contain; cursor: zoom-out;
  }
  .fullscreen-backdrop {
    position: fixed; top: 0; left: 0; right: 0; bottom: 0;
    background-color: rgba(0, 0, 0, 0.85); z-index: 9980; cursor: zoom-out;
    animation: fadeIn 0.3s ease;
  }

  /* PDF Export Progress */
  .pdf-progress-overlay {
    position: fixed; top: 0; left: 0; right: 0; bottom: 0;
    background-color: rgba(0, 0, 0, 0.7); display: flex; flex-direction: column;
    align-items: center; justify-content: center; z-index: 10000; color: white;
    text-align: center; font-family: var(--font-sans); animation: fadeIn 0.3s ease;
  }
   .pdf-progress-content {
      background-color: var(--card); color: var(--card-foreground);
      padding: 25px 35px; border-radius: var(--radius); box-shadow: var(--shadow-lg);
      display: flex; flex-direction: column; align-items: center; min-width: 300px;
   }
   .pdf-progress-spinner {
     border: 4px solid var(--secondary); border-top: 4px solid var(--primary);
     border-radius: 50%; width: 30px; height: 30px;
     animation: spin 1.5s linear infinite; margin-bottom: 15px;
   }
  .pdf-progress-text { font-size: 16px; font-weight: 500; margin-bottom: 10px; }
  .pdf-progress-details { font-size: 13px; color: var(--muted-foreground); margin-bottom: 15px; }
  .pdf-progress-bar-container {
    width: 100%; height: 8px; background-color: var(--secondary);
    border-radius: 4px; overflow: hidden; border: 1px solid var(--border);
  }
  .pdf-progress-bar {
    height: 100%; background-color: var(--primary); width: 0%;
    transition: width 0.3s ease; border-radius: 4px;
  }

  /* Hide default KITEE UI elements */
  #dg1, form[name="Form1"] > table:first-of-type { display: none !important; }

  /* Animation */
  @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
  @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
  .fade-in { animation: fadeIn 0.3s ease forwards; }
`);

(function() {
    'use strict';

    // --- State Management ---
    const state = {
        currentPage: 1,
        totalPages: 0,
        rollNumber: GM_getValue('roll') || '',
        rollHistory: JSON.parse(GM_getValue('rollHistory') || '[]'),
        subject: document.querySelector("#LblCouseCode")?.textContent?.trim() || 'UNKNOWN_SUB',
        examType: document.querySelector("#LblCourse")?.textContent?.trim() || 'Exam',
        year: parseInt(window.location.href.match(/midfeb(\d{4})stview/i)?.[1] || '2025', 10),
        semester: 6,
        isDarkMode: GM_getValue('darkMode', window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches),
        isCollapsed: GM_getValue('collapsed', false),
        isFullscreen: false,
        status: { type: '', message: '', timeoutId: null },
        pdfGenerating: false,
        pdfProgress: 0,
        pdfTotalPages: 0,
    };

    // --- Constants ---
    const MAX_ROLL_HISTORY = 5;
    const STATUS_CLEAR_DELAY = 3500;
    const A4_WIDTH_PT = 595.28;
    const A4_HEIGHT_PT = 841.89;
    const PDF_MARGIN_PT = 30;

    // --- Icons ---
    const icons = {
        collapse: `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg>`,
        expand: `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>`,
        edit: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>`,
        refresh: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path></svg>`,
        prev: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg>`,
        next: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>`,
        first: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="11 17 6 12 11 7"></polyline><polyline points="18 17 13 12 18 7"></polyline></svg>`,
        last: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="13 17 18 12 13 7"></polyline><polyline points="6 17 11 12 6 7"></polyline></svg>`,
        fullscreen: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"></path></svg>`,
        exitFullscreen: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"></path></svg>`,
        jump: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>`,
        dark: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>`,
        light: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>`,
        logo: `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path></svg>`,
        pdf: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>`,
        // NEW: GitHub Icon
        github: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path></svg>`
    };

    // --- DOM Elements Cache ---
    const elements = {};

    // --- Utility Functions & Core Logic ---
    // ... (generateKITEEUrl, setStatus, addToRollHistory, UI Update functions, Core Logic functions: updateImage, navigateToPage, promptRollNumber, toggles, exportToPDF, PDF progress, handleKeyPress - all remain the same as v2.2) ...
    // --- Utility Functions ---
    function generateKITEEUrl(subject, roll, page, year = state.year, semester = state.semester) {
        if (!year || !semester || !subject || !roll || !page) {
            console.error("Missing parameters for URL generation:", { subject, roll, page, year, semester });
            setStatus('error', 'Missing info for URL');
            return null;
        }
        const paddedPage = String(page).padStart(3, '0');
        // Verified path structure assumption - EC2R might be specific. If issues arise, this might need adjustment.
        const basePath = `../../../../../../../../KITEE/${year}_${semester}/${subject}/EC2R/${roll}/${paddedPage}|DU|1`;
        return `GenerateImage.aspx?pgno=${encodeURIComponent(basePath)}`;
    }

    function setStatus(type, message, clear = true) {
        if (state.status.timeoutId) {
            clearTimeout(state.status.timeoutId);
            state.status.timeoutId = null;
        }
        state.status.type = type;
        state.status.message = message;

        if (elements.statusIndicator && elements.statusText && elements.loadingSpinner) {
            elements.statusIndicator.className = `status-indicator ${type}`;
            elements.statusText.textContent = message;
            elements.loadingSpinner.style.display = type === 'loading' ? 'inline-block' : 'none';

            if (clear && (type === 'success' || type === 'error')) {
                state.status.timeoutId = setTimeout(() => {
                    setStatus('', ''); // Clear status after delay
                }, STATUS_CLEAR_DELAY);
            }
        }
        // Disable/Enable buttons during loading
        const isLoading = type === 'loading';
        const buttonsToDisable = [
            elements.firstBtn, elements.prevBtn, elements.nextBtn, elements.lastBtn,
            elements.jumpBtn, elements.refreshBtn, elements.fullscreenBtn, elements.pdfBtn,
            elements.editRollBtn, ...(elements.rollHistoryContainer?.children || [])
        ];
        buttonsToDisable.forEach(btn => {
            if (btn) btn.disabled = isLoading;
        });
        if (elements.jumpInput) elements.jumpInput.disabled = isLoading;
    }

    function addToRollHistory(newRoll) {
        if (!newRoll) return;
        const upperCaseRoll = newRoll.toUpperCase(); // Store consistently
        const filteredHistory = state.rollHistory.filter(r => r !== upperCaseRoll);
        const updatedHistory = [upperCaseRoll, ...filteredHistory];
        state.rollHistory = updatedHistory.slice(0, MAX_ROLL_HISTORY);
        GM_setValue('rollHistory', JSON.stringify(state.rollHistory));
        updateRollHistoryUI();
    }

    // --- UI Update Functions ---
    function updateRollDisplay() {
        const displayRoll = state.rollNumber || 'Not set';
        if (elements.rollLabel) {
            elements.rollLabel.textContent = `Roll: ${displayRoll}`;
            elements.rollLabel.title = `Roll: ${displayRoll}`;
        }
        if (elements.pdfBtn) {
             const pdfBtnText = elements.pdfBtn.querySelector('span');
             if(pdfBtnText) {
                 pdfBtnText.textContent = state.rollNumber ? `Export PDF (${state.rollNumber})` : 'Export PDF';
             }
        }
    }

    function updateRollHistoryUI() {
        if (!elements.rollHistoryContainer) return;
        elements.rollHistoryContainer.innerHTML = ''; // Clear existing items
        if (state.rollHistory.length === 0) {
            // No need for empty message if section just looks empty
        } else {
            state.rollHistory.forEach(roll => {
                const item = document.createElement('button');
                item.className = 'roll-history-item tooltip';
                item.textContent = roll;
                item.setAttribute('data-tooltip', `Load roll ${roll}`);
                item.onclick = () => {
                    if (state.rollNumber !== roll && !state.pdfGenerating) { // Prevent change during PDF export
                        state.rollNumber = roll;
                        GM_setValue('roll', roll);
                        updateRollDisplay();
                        state.currentPage = 1;
                        updateImage();
                        updatePageInfo();
                        setStatus('success', `Loaded roll ${roll}`);
                        // No need to re-add to history here, it's already there.
                    }
                };
                elements.rollHistoryContainer.appendChild(item);
            });
        }
    }

    function updatePageInfo() {
        if (elements.pageInfo && elements.jumpInput) {
            const total = state.totalPages > 0 ? state.totalPages : '?';
            elements.pageInfo.textContent = `Page ${state.currentPage} of ${total}`;
            elements.jumpInput.value = state.currentPage;
            elements.jumpInput.max = state.totalPages > 0 ? state.totalPages : 1;
        }
        if (elements.firstBtn && elements.prevBtn && elements.nextBtn && elements.lastBtn) {
             const onFirstPage = state.currentPage <= 1;
             const onLastPage = state.currentPage >= state.totalPages && state.totalPages > 0;
             const noPages = state.totalPages === 0;

             elements.firstBtn.disabled = onFirstPage || noPages || state.pdfGenerating;
             elements.prevBtn.disabled = onFirstPage || noPages || state.pdfGenerating;
             elements.nextBtn.disabled = onLastPage || noPages || state.pdfGenerating;
             elements.lastBtn.disabled = onLastPage || noPages || state.pdfGenerating;
             if(elements.jumpInput) elements.jumpInput.disabled = noPages || state.pdfGenerating;
             if(elements.jumpBtn) elements.jumpBtn.disabled = noPages || state.pdfGenerating;
        }
    }

    function updateTheme() {
        if (state.isDarkMode) {
            document.documentElement.classList.add('dark');
        } else {
            document.documentElement.classList.remove('dark');
        }
        if (elements.themeToggle) {
            elements.themeToggle.innerHTML = state.isDarkMode ? icons.light : icons.dark;
            elements.themeToggle.setAttribute('data-tooltip', state.isDarkMode ? 'Switch to Light Mode' : 'Switch to Dark Mode');
        }
        if (elements.pdfProgressOverlay && elements.pdfProgressOverlay.style.display !== 'none') {
           // PDF progress overlay styling is handled by CSS variables now
        }
    }

    function updateCollapseState() {
         if (!elements.ui || !elements.toggleBtn) return;
         if (state.isCollapsed) {
             elements.ui.classList.add('collapsed');
             elements.toggleBtn.innerHTML = icons.expand;
             elements.toggleBtn.setAttribute('data-tooltip', 'Expand Panel (C)');
         } else {
             elements.ui.classList.remove('collapsed');
             elements.toggleBtn.innerHTML = icons.collapse;
             elements.toggleBtn.setAttribute('data-tooltip', 'Collapse Panel (C)');
         }
         GM_setValue('collapsed', state.isCollapsed);
    }

    function updateFullscreenState() {
        const fullscreenBtnSpan = elements.fullscreenBtn?.querySelector('span');
        if (state.isFullscreen) {
            document.body.classList.add('fullscreen-mode');
            if (!elements.fullscreenBackdrop) {
                elements.fullscreenBackdrop = document.createElement('div');
                elements.fullscreenBackdrop.className = 'fullscreen-backdrop';
                elements.fullscreenBackdrop.onclick = toggleFullscreen;
                document.body.appendChild(elements.fullscreenBackdrop);
                elements.fullscreenBackdrop.classList.add('fade-in');
            }
            elements.fullscreenBackdrop.style.display = 'block';
            if (elements.fullscreenBtn) {
                elements.fullscreenBtn.innerHTML = `${icons.exitFullscreen} <span>Exit Fullscreen</span>`;
                elements.fullscreenBtn.setAttribute('data-tooltip', 'Exit Fullscreen (F or Esc)');
            }
            if (elements.imageElement) elements.imageElement.addEventListener('click', toggleFullscreen);

        } else {
            document.body.classList.remove('fullscreen-mode');
            if (elements.fullscreenBackdrop) {
                elements.fullscreenBackdrop.style.display = 'none';
            }
            if (elements.fullscreenBtn) {
                elements.fullscreenBtn.innerHTML = `${icons.fullscreen} <span>Fullscreen</span>`;
                elements.fullscreenBtn.setAttribute('data-tooltip', 'Enter Fullscreen (F)');
            }
             if (elements.imageElement) elements.imageElement.removeEventListener('click', toggleFullscreen);
        }
    }

    // --- Core Logic Functions ---

    async function updateImage(page = state.currentPage) {
        if (!state.rollNumber) {
            setStatus('error', 'Roll number not set.', false); return Promise.reject('No roll number');
        }
        if (!state.subject || state.subject === 'UNKNOWN_SUB') {
             setStatus('error', 'Subject code not found.', false); return Promise.reject('No subject code');
        }
        if (!elements.imageElement) {
             setStatus('error', 'Image element not found.', false); return Promise.reject('No image element');
         }

        const imageUrl = generateKITEEUrl(state.subject, state.rollNumber, page);
        if (!imageUrl) return Promise.reject('URL generation failed');

        setStatus('loading', `Loading page ${page}...`, false);

        return new Promise((resolve, reject) => {
            const img = elements.imageElement;
            let resolved = false; // Prevent multiple resolves/rejects
            let loadTimeout = null; // Store timeout ID

            const loadHandler = () => {
                cleanup();
                if (!resolved) {
                    resolved = true;
                    if(state.status.type === 'loading' && state.status.message.includes(`Loading page ${page}`)) {
                        setStatus('success', `Page ${page} loaded`);
                    }
                    state.currentPage = page;
                    updatePageInfo();
                    resolve();
                }
            };
            const errorHandler = (err) => {
                cleanup();
                if (!resolved) {
                    resolved = true;
                    console.error(`Error loading image for page ${page}:`, err instanceof Event ? 'Load Error Event' : err);
                    if(state.status.type === 'loading' && state.status.message.includes(`Loading page ${page}`)) {
                        setStatus('error', `Failed to load page ${page}`);
                    }
                    reject(new Error(`Failed to load image for page ${page}`));
                }
            };
            const cleanup = () => {
                img.removeEventListener('load', loadHandler);
                img.removeEventListener('error', errorHandler);
                if (loadTimeout) clearTimeout(loadTimeout);
                loadTimeout = null;
            };

            img.addEventListener('load', loadHandler);
            img.addEventListener('error', errorHandler);

            loadTimeout = setTimeout(() => {
                 if (!resolved) {
                     errorHandler(new Error('Image load timed out'));
                 }
             }, 15000); // 15 second timeout

             img.src = imageUrl;
        });
    }

    function navigateToPage(page) {
        page = parseInt(page, 10);
        if (isNaN(page) || page < 1 || (page > state.totalPages && state.totalPages > 0) || page === state.currentPage || state.pdfGenerating) {
            if (isNaN(page) || page < 1 || (page > state.totalPages && state.totalPages > 0)) {
                setStatus('error', 'Invalid page number');
            }
            if (elements.jumpInput) elements.jumpInput.value = state.currentPage;
            return;
        }
        updateImage(page).catch(err => console.warn("Navigation error ignored:", err)); // Log error but don't block UI
    }

    function promptRollNumber() {
         if (state.pdfGenerating) {
             setStatus('error', 'Cannot change roll during PDF export.');
             return;
         }
        const currentRoll = state.rollNumber;
        const newRoll = prompt('Enter KIIT Roll Number:', currentRoll);
        if (newRoll && newRoll.trim().toUpperCase() !== currentRoll) {
            state.rollNumber = newRoll.trim().toUpperCase();
            GM_setValue('roll', state.rollNumber);
            addToRollHistory(state.rollNumber);
            updateRollDisplay();
            state.currentPage = 1;
            updatePageInfo(); // Update page info immediately
            updateImage().catch(err => console.error("Error loading image for new roll:", err)); // Load image for the new roll
            setStatus('success', `Roll number updated to ${state.rollNumber}`);
        }
    }

    function toggleDarkMode() {
        state.isDarkMode = !state.isDarkMode;
        GM_setValue('darkMode', state.isDarkMode);
        updateTheme();
    }

     function toggleCollapse() {
        state.isCollapsed = !state.isCollapsed;
        updateCollapseState();
    }

     function toggleFullscreen() {
         state.isFullscreen = !state.isFullscreen;
         updateFullscreenState();
     }

    async function exportToPDF() {
        if (state.pdfGenerating) {
            setStatus('error', 'PDF generation already in progress.'); return;
        }
        if (!state.rollNumber || state.totalPages <= 0) {
            setStatus('error', 'Set roll number & ensure pages are detected.'); return;
        }
        if (typeof jspdf === 'undefined' || typeof html2canvas === 'undefined') {
             setStatus('error', 'PDF libraries not loaded.');
             console.error('jsPDF or html2canvas missing. Check @require directives and network.');
             return;
        }

        state.pdfGenerating = true;
        state.pdfProgress = 0;
        state.pdfTotalPages = state.totalPages;
        const originalPage = state.currentPage;
        showPdfProgress();
        setStatus('loading', 'Starting PDF export...', false);
        updatePageInfo(); // Update button disabled states

        const pdfDoc = new jspdf.jsPDF({
            orientation: 'p', unit: 'pt', format: 'a4',
            putOnlyUsedFonts: true, floatPrecision: 16
        });

        const availableWidth = A4_WIDTH_PT - 2 * PDF_MARGIN_PT;
        const availableHeight = A4_HEIGHT_PT - 2 * PDF_MARGIN_PT;
        let pagesExported = 0;

        try {
            for (let i = 1; i <= state.totalPages; i++) {
                state.pdfProgress = i;
                updatePdfProgress();
                setStatus('loading', `Exporting page ${i}/${state.totalPages}...`, false);

                try {
                    await updateImage(i);
                    const imgElement = elements.imageElement;

                    if (!imgElement || !imgElement.complete || imgElement.naturalWidth === 0) {
                         throw new Error(`Image element invalid after load for page ${i}`);
                     }

                    const canvas = await html2canvas(imgElement, {
                        scale: 2, useCORS: true, logging: false,
                        backgroundColor: state.isDarkMode ? '#111827' : '#ffffff',
                    });
                    const imgData = canvas.toDataURL('image/jpeg', 0.85);

                    const imgProps = pdfDoc.getImageProperties(imgData);
                    let imgWidth = imgProps.width;
                    let imgHeight = imgProps.height;
                    const ratio = imgWidth / imgHeight;

                    if (imgWidth > availableWidth) {
                        imgWidth = availableWidth;
                        imgHeight = imgWidth / ratio;
                    }
                    if (imgHeight > availableHeight) {
                        imgHeight = availableHeight;
                        imgWidth = imgHeight * ratio;
                    }

                    const x = PDF_MARGIN_PT + (availableWidth - imgWidth) / 2;
                    const y = PDF_MARGIN_PT + (availableHeight - imgHeight) / 2; // Center vertically too

                    if (pagesExported > 0) {
                        pdfDoc.addPage();
                    }
                    pdfDoc.addImage(imgData, 'JPEG', x, y, imgWidth, imgHeight);
                    pagesExported++;

                } catch (pageError) {
                    console.warn(`Skipping page ${i} due to error:`, pageError);
                    setStatus('error', `Skipped page ${i} (Check console)`);
                    await new Promise(resolve => setTimeout(resolve, 300)); // Shorter delay
                }
            } // End of loop

            if (pagesExported === 0) {
                 throw new Error("No pages could be exported.");
            }

            const filename = `${state.subject}_${state.rollNumber}_${state.examType}.pdf`.replace(/[^a-z0-9_.-]/gi, '_');
            pdfDoc.save(filename);
            setStatus('success', `PDF exported (${pagesExported}/${state.totalPages} pages)`);

        } catch (error) {
            console.error('PDF Export Failed:', error);
            setStatus('error', `PDF export failed: ${error.message}`);
        } finally {
            state.pdfGenerating = false;
            hidePdfProgress();
            updatePageInfo(); // Re-enable buttons
            // Go back to original page only if it's still valid
            if (originalPage >= 1 && originalPage <= state.totalPages) {
               updateImage(originalPage).catch(err => console.warn("Failed to return to original page after PDF export:", err));
            }
        }
    }

     function showPdfProgress() {
         if (!elements.pdfProgressOverlay) {
             elements.pdfProgressOverlay = document.createElement('div');
             elements.pdfProgressOverlay.className = 'pdf-progress-overlay';
             elements.pdfProgressOverlay.innerHTML = `
                 <div class="pdf-progress-content">
                     <div class="pdf-progress-spinner"></div>
                     <div class="pdf-progress-text">Generating PDF...</div>
                     <div class="pdf-progress-details">Page 1 of ${state.pdfTotalPages}</div>
                     <div class="pdf-progress-bar-container">
                         <div class="pdf-progress-bar"></div>
                     </div>
                 </div>
             `;
             document.body.appendChild(elements.pdfProgressOverlay);
             elements.pdfProgressDetails = elements.pdfProgressOverlay.querySelector('.pdf-progress-details');
             elements.pdfProgressBar = elements.pdfProgressOverlay.querySelector('.pdf-progress-bar');
             updateTheme(); // Apply theme
         }
         elements.pdfProgressOverlay.style.display = 'flex';
         updatePdfProgress();
     }

     function updatePdfProgress() {
         if (elements.pdfProgressOverlay && state.pdfGenerating) {
             const progressPercent = state.pdfTotalPages > 0 ? (state.pdfProgress / state.pdfTotalPages) * 100 : 0;
             if (elements.pdfProgressDetails) {
                 elements.pdfProgressDetails.textContent = `Page ${state.pdfProgress} of ${state.pdfTotalPages}`;
             }
             if (elements.pdfProgressBar) {
                 elements.pdfProgressBar.style.width = `${progressPercent}%`;
             }
         }
     }

     function hidePdfProgress() {
         if (elements.pdfProgressOverlay) {
             elements.pdfProgressOverlay.style.display = 'none';
         }
     }

    // --- Keyboard Shortcut Handler ---
    function handleKeyPress(event) {
        if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA' || state.pdfGenerating) {
            return;
        }
        if (event.altKey || event.ctrlKey || event.metaKey) {
            return;
        }

        let buttonClicked = null;

        switch (event.key.toUpperCase()) {
            case 'ARROWLEFT': buttonClicked = elements.prevBtn; break;
            case 'ARROWRIGHT': buttonClicked = elements.nextBtn; break;
            case 'HOME': buttonClicked = elements.firstBtn; break;
            case 'END': buttonClicked = elements.lastBtn; break;
            case 'F': toggleFullscreen(); break;
            case 'R': buttonClicked = elements.refreshBtn; break;
            case 'C': toggleCollapse(); break;
            case 'P': buttonClicked = elements.pdfBtn; break;
            case 'E': buttonClicked = elements.editRollBtn; break;
            case 'ESCAPE':
                 if (state.isFullscreen) { event.preventDefault(); toggleFullscreen(); }
                 break;
        }

        if (buttonClicked && !buttonClicked.disabled) {
             event.preventDefault();
             buttonClicked.click();
             buttonClicked.style.transform = 'scale(0.97)'; // Visual feedback
             setTimeout(() => { buttonClicked.style.transform = 'scale(1)'; }, 100);
        }
    }


    // --- UI Creation ---
    function createUI() {
        const ui = document.createElement('div');
        ui.className = 'kitee-ui fade-in';
        elements.ui = ui;

        const toggleBtn = document.createElement('button');
        toggleBtn.className = 'toggle-btn tooltip';
        toggleBtn.onclick = toggleCollapse;
        elements.toggleBtn = toggleBtn;
        ui.appendChild(toggleBtn);

        const contentWrapper = document.createElement('div');
        contentWrapper.className = 'content-wrapper';
        elements.contentWrapper = contentWrapper;

        // Header
        const header = document.createElement('header');
        header.className = 'kitee-header';
        const title = document.createElement('div');
        title.className = 'kitee-title';
        title.innerHTML = `${icons.logo} <span>KITEE Navigator</span>`;
        const headerControls = document.createElement('div');
        headerControls.className = 'header-controls';
        const themeToggle = document.createElement('button');
        themeToggle.className = 'btn btn-secondary btn-icon tooltip';
        themeToggle.onclick = toggleDarkMode;
        elements.themeToggle = themeToggle;
        headerControls.appendChild(themeToggle);
        header.append(title, headerControls);
        contentWrapper.appendChild(header);

        // Roll Number Section
        const rollSection = document.createElement('section');
        const rollDisplay = document.createElement('div');
        rollDisplay.className = 'roll-display';
        const rollLabel = document.createElement('div');
        rollLabel.className = 'roll-label';
        elements.rollLabel = rollLabel;
        const editRollBtn = document.createElement('button');
        editRollBtn.className = 'btn btn-secondary btn-icon tooltip';
        editRollBtn.innerHTML = icons.edit;
        editRollBtn.setAttribute('data-tooltip', 'Edit Roll Number (E)');
        editRollBtn.onclick = promptRollNumber;
        elements.editRollBtn = editRollBtn;
        rollDisplay.append(rollLabel, editRollBtn);
        const rollHistoryContainer = document.createElement('div');
        rollHistoryContainer.className = 'roll-history';
        elements.rollHistoryContainer = rollHistoryContainer;
        rollSection.append(rollDisplay, rollHistoryContainer);
        contentWrapper.appendChild(rollSection);

        // Page Navigation Section
        const pageSection = document.createElement('section');
        pageSection.className = 'page-section';
        const pageNav = document.createElement('div');
        pageNav.className = 'page-nav';
        const navControls1 = document.createElement('div');
        navControls1.style.display = 'flex'; navControls1.style.gap = '6px';
        const firstBtn = document.createElement('button');
        firstBtn.className = 'btn btn-secondary btn-icon tooltip'; firstBtn.innerHTML = icons.first;
        firstBtn.setAttribute('data-tooltip', 'First Page (Home)'); firstBtn.onclick = () => navigateToPage(1);
        elements.firstBtn = firstBtn;
        const prevBtn = document.createElement('button');
        prevBtn.className = 'btn btn-secondary btn-icon tooltip'; prevBtn.innerHTML = icons.prev;
        prevBtn.setAttribute('data-tooltip', 'Previous Page (←)'); prevBtn.onclick = () => navigateToPage(state.currentPage - 1);
        elements.prevBtn = prevBtn;
        navControls1.append(firstBtn, prevBtn);
        const pageInfo = document.createElement('div');
        pageInfo.className = 'page-info'; elements.pageInfo = pageInfo;
        const navControls2 = document.createElement('div');
        navControls2.style.display = 'flex'; navControls2.style.gap = '6px';
        const nextBtn = document.createElement('button');
        nextBtn.className = 'btn btn-secondary btn-icon tooltip'; nextBtn.innerHTML = icons.next;
        nextBtn.setAttribute('data-tooltip', 'Next Page (→)'); nextBtn.onclick = () => navigateToPage(state.currentPage + 1);
        elements.nextBtn = nextBtn;
        const lastBtn = document.createElement('button');
        lastBtn.className = 'btn btn-secondary btn-icon tooltip'; lastBtn.innerHTML = icons.last;
        lastBtn.setAttribute('data-tooltip', 'Last Page (End)'); lastBtn.onclick = () => state.totalPages > 0 && navigateToPage(state.totalPages);
        elements.lastBtn = lastBtn;
        navControls2.append(nextBtn, lastBtn);
        pageNav.append(navControls1, pageInfo, navControls2);
        const pageJump = document.createElement('div');
        pageJump.className = 'page-jump';
        const jumpInput = document.createElement('input');
        jumpInput.className = 'page-jump-input'; jumpInput.type = 'number'; jumpInput.min = 1; jumpInput.placeholder = "Go";
        elements.jumpInput = jumpInput;
        const jumpBtn = document.createElement('button');
        jumpBtn.className = 'btn btn-secondary btn-icon tooltip';
        jumpBtn.innerHTML = icons.jump; jumpBtn.setAttribute('data-tooltip', 'Jump to Page');
        jumpBtn.onclick = () => navigateToPage(jumpInput.value);
        elements.jumpBtn = jumpBtn;
        jumpInput.onkeydown = (e) => { if (e.key === 'Enter' && !jumpBtn.disabled) jumpBtn.click(); };
        pageJump.append(jumpInput, jumpBtn);
        pageSection.append(pageNav, pageJump);
        contentWrapper.appendChild(pageSection);

        // Actions Section
        const actionsSection = document.createElement('section');
        actionsSection.className = 'actions-section';
        const refreshBtn = document.createElement('button');
        refreshBtn.className = 'btn btn-secondary tooltip';
        refreshBtn.innerHTML = `${icons.refresh} <span>Refresh</span>`;
        refreshBtn.setAttribute('data-tooltip', 'Reload Image (R)');
        refreshBtn.onclick = () => updateImage();
        elements.refreshBtn = refreshBtn;
        const fullscreenBtn = document.createElement('button');
        fullscreenBtn.className = 'btn btn-secondary tooltip';
        fullscreenBtn.innerHTML = `${icons.fullscreen} <span>Fullscreen</span>`;
        fullscreenBtn.setAttribute('data-tooltip', 'Toggle Fullscreen (F)');
        fullscreenBtn.onclick = toggleFullscreen;
        elements.fullscreenBtn = fullscreenBtn;
        const pdfBtn = document.createElement('button');
        pdfBtn.className = 'btn btn-primary tooltip';
        pdfBtn.innerHTML = `${icons.pdf} <span>Export PDF</span>`;
        pdfBtn.setAttribute('data-tooltip', 'Export All Pages as PDF (P)');
        pdfBtn.onclick = exportToPDF;
        elements.pdfBtn = pdfBtn;
        actionsSection.append(refreshBtn, fullscreenBtn, pdfBtn);
        contentWrapper.appendChild(actionsSection);

         // Status Section
         const statusSection = document.createElement('section');
         statusSection.style.paddingBottom = '0'; statusSection.style.borderBottom = 'none';
         const statusIndicator = document.createElement('div');
         statusIndicator.className = 'status-indicator';
         const loadingSpinner = document.createElement('div'); loadingSpinner.className = 'loading-spinner';
         const statusText = document.createElement('span'); statusText.className = 'status-text';
         statusIndicator.append(loadingSpinner, statusText);
         elements.statusIndicator = statusIndicator; elements.loadingSpinner = loadingSpinner; elements.statusText = statusText;
         statusSection.appendChild(statusIndicator);
         contentWrapper.appendChild(statusSection);

         // Keyboard Shortcuts Info Section
         const shortcutsSection = document.createElement('section');
         shortcutsSection.className = 'shortcuts-section';
         shortcutsSection.innerHTML = `
           <div class="shortcuts-title">Keyboard Shortcuts</div>
           <ul class="shortcuts-list">
             <li>Previous / Next Page <span class="key-combo"><kbd>←</kbd> / <kbd>→</kbd></span></li>
             <li>First / Last Page <span class="key-combo"><kbd>Home</kbd> / <kbd>End</kbd></span></li>
             <li>Toggle Fullscreen <kbd>F</kbd></li>
             <li>Reload Image <kbd>R</kbd></li>
             <li>Collapse / Expand <kbd>C</kbd></li>
             <li>Export PDF <kbd>P</kbd></li>
             <li>Edit Roll No <kbd>E</kbd></li>
             <li>Exit Fullscreen <kbd>Esc</kbd></li>
           </ul>
         `;
         contentWrapper.appendChild(shortcutsSection);

         // NEW: Footer Section
         const footer = document.createElement('footer');
         footer.className = 'kitee-footer';
         const footerText = document.createElement('span');
         footerText.innerHTML = 'Made with ❤️ by Prajwal Panth'; // Use innerHTML for the heart emoji

         const githubLink = document.createElement('a');
         githubLink.href = 'https://github.com/prajwal-panth';
         githubLink.target = '_blank';
         githubLink.rel = 'noopener noreferrer';
         githubLink.className = 'tooltip';
         githubLink.setAttribute('data-tooltip', 'Visit GitHub Profile');
         githubLink.innerHTML = icons.github; // Add the GitHub icon

         footer.appendChild(footerText);
         footer.appendChild(githubLink);
         contentWrapper.appendChild(footer); // Add footer to the scrollable content

        ui.appendChild(contentWrapper);
        document.body.appendChild(ui);
    }


    // --- Initialization ---
    function init() {
        console.log("Snapify Initializing...");

        elements.imageElement = document.querySelector('#cimg');
        if (!elements.imageElement) {
            console.error("Error: Could not find main image element (#cimg). Script cannot function.");
            alert("Snapify Navigator Error: Main image element #cimg not found. Script disabled.");
            return;
        }

        const pageLinks = document.querySelectorAll('#dg1 a');
        state.totalPages = pageLinks.length;
        if (state.totalPages === 0) {
            console.warn("Warning: Could not find page links in #dg1. Total pages may be 0 or incorrect. PDF export might not work.");
        } else {
             console.log(`Found ${state.totalPages} pages.`);
        }

        createUI();

        // Initial setup after UI creation
        updateTheme();
        updateCollapseState();
        updateRollDisplay();
        updateRollHistoryUI();
        updatePageInfo();

        if (!state.rollNumber) {
            setTimeout(promptRollNumber, 150);
        } else {
            addToRollHistory(state.rollNumber);
            updateImage(state.currentPage)
                .catch(err => console.error("Initial image load failed:", err));
        }

        document.addEventListener('keydown', handleKeyPress);

        console.log("Snapify Initialized.");
        if (state.totalPages > 0 || state.rollNumber) {
             setStatus('success', 'Navigator Ready', true);
        } else {
             setStatus('error', 'Ready, but no pages detected or roll set.', false);
        }
    }

    // --- Run ---
    if (document.readyState === "complete" || document.readyState === "interactive") {
       setTimeout(init, 100);
    } else {
       window.addEventListener('DOMContentLoaded', init);
    }

})();