{
  "version": 1,
  "comment": "Layer-3 page rules consumed by the Python checker and the JS Vicunadator. Edit here only; both checkers read this file.",
  "sites": {
    "course": {
      "site_checks": [
        {
          "type": "text",
          "rule": "copyright (optional) is Firm ©YEAR - name, space, © tight against the year",
          "pattern": "©(?!20\\d\\d)|(?<!\\S )©",
          "present": false,
          "fail_hint_pattern": "©",
          "fail_hint": "© found but malformed - needs: YourFirmName ©2026 (space before ©, none after)"
        }
      ],
      "pages": {
        "index.html": {
          "checks": [
            {
              "type": "html",
              "rule": "nav links to Introduction",
              "pattern": "href=\"(?:[^\"#?]*/)?introduction\\.(?:html?|php)(?:[#?][^\"]*)?\"",
              "present": true
            },
            {
              "type": "html",
              "rule": "nav links to Contract",
              "pattern": "href=\"(?:[^\"#?]*/)?contract\\.(?:html?|php)(?:[#?][^\"]*)?\"",
              "present": true,
              "skip_for_dirs": [
                "itsc1110"
              ]
            },
            {
              "type": "html",
              "rule": "nav links to About",
              "pattern": "href=\"(?:[^\"#?]*/)?about\\.(?:html?|php)(?:[#?][^\"]*)?\"",
              "present": true,
              "skip_for_dirs": [
                "itsc1110"
              ]
            },
            {
              "type": "element",
              "rule": "welcome paragraph in main",
              "tag": "p",
              "within": "main",
              "min": 1
            }
          ]
        },
        "introduction.html": {
          "checks": [
            {
              "type": "heading",
              "rule": "h2 is Introduction",
              "level": 2,
              "pattern": "intro"
            },
            {
              "type": "heading",
              "rule": "h6 display name (First 'Nick' Last <divider> Mascot)",
              "level": 6,
              "pattern": "\\s[~|•·]\\s"
            },
            {
              "type": "css",
              "rule": "h6 display name styled large (and shouldn't wrap)",
              "pattern": "h6[^{]*\\{[^}]*font-size"
            },
            {
              "type": "element",
              "rule": "photo in a figure",
              "tag": "img",
              "within": "figure",
              "min": 1
            },
            {
              "type": "element",
              "rule": "caption is a figcaption",
              "tag": "figcaption",
              "within": "figure",
              "min": 1
            },
            {
              "type": "text",
              "rule": "no acknowledgment/signature sentence (doc-only item)",
              "pattern": "public document|anyone with the link|keep private",
              "present": false
            },
            {
              "type": "heading",
              "rule": "no Last, First TOC heading (doc-only item)",
              "level": 0,
              "pattern": "^[A-Z][a-zA-Z]+, [A-Z]",
              "present": false
            },
            {
              "type": "html_all",
              "rule": "bold labels before colons (current label set)",
              "patterns": [
                "Personal Background:",
                "Professional Background:",
                "Academic Background:",
                "Primary Work Computer",
                "Primary Work Location",
                "Alternate Computer",
                "Courses I['’]m Taking"
              ]
            },
            {
              "type": "element",
              "rule": "courses are a nested ordered list",
              "tag": "ol",
              "within": "li",
              "min": 1
            },
            {
              "type": "element",
              "rule": "quote in a blockquote",
              "tag": "blockquote",
              "min": 1
            },
            {
              "type": "html",
              "rule": "quote attribution starts with - or ~",
              "pattern": "<blockquote[\\s\\S]{0,500}?[-–~]\\s*[A-Za-z]",
              "present": true
            }
          ]
        },
        "contract.html": {
          "checks": [
            {
              "type": "heading",
              "rule": "h2 is Contract",
              "level": 2,
              "pattern": "contract"
            },
            {
              "type": "text_all",
              "rule": "contract language present (agrees to the contract, names von Briesen, has a year)",
              "patterns": [
                "agree",
                "von Briesen",
                "20\\d\\d"
              ]
            },
            {
              "type": "element",
              "rule": "at least four paragraphs",
              "tag": "p",
              "within": "main",
              "min": 4
            },
            {
              "type": "html",
              "rule": "last paragraph is Signed followed by your name/date in italics",
              "pattern": "<p[^>]*>\\s*Signed\\b(?:(?!</p>)[\\s\\S])*?<(em|i)[\\s>](?:(?!</p>)[\\s\\S])*</p>(?![\\s\\S]*<p[\\s>][\\s\\S]*</main>)",
              "present": true
            },
            {
              "type": "text",
              "rule": "no unedited placeholders",
              "pattern": "FIRST M\\.? LAST|SEMESTER YYYY|ABC123|NAME OF CLASS",
              "present": false
            }
          ]
        },
        "about.html": {
          "checks": [
            {
              "type": "heading",
              "rule": "h2 is About",
              "level": 2,
              "pattern": "about"
            },
            {
              "type": "html",
              "rule": "links to the instructor's teaching site",
              "pattern": "divonbriesen\\.github\\.io/teaching",
              "present": true,
              "skip_for_dirs": [
                "web115",
                "web-115",
                "web215",
                "web-215",
                "web250",
                "web-250",
                "cis110",
                "cis-110"
              ]
            },
            {
              "type": "text_all",
              "rule": "names the course tools",
              "patterns": [
                "VS Code",
                "Emmet",
                "GitHub",
                "Accumulus"
              ]
            },
            {
              "type": "text",
              "rule": "names the Vicunadator",
              "pattern": "Vicunadator",
              "present": true,
              "skip_for_dirs": [
                "itis3135"
              ]
            },
            {
              "type": "text_all",
              "rule": "lists the planned sites",
              "patterns": [
                "personal",
                "course site",
                "design"
              ]
            },
            {
              "type": "text_all",
              "rule": "lists the later planned sites (crappy, whimsical, client project)",
              "patterns": [
                "crapp?y",
                "whimsical|product company",
                "client project"
              ],
              "skip_for_dirs": [
                "web115",
                "web-115",
                "web215",
                "web-215",
                "web250",
                "web-250",
                "cis110",
                "cis-110"
              ]
            },
            {
              "type": "element",
              "rule": "description bullets",
              "tag": "ul",
              "min": 1
            },
            {
              "type": "element",
              "rule": "at least two paragraphs",
              "tag": "p",
              "within": "main",
              "min": 2
            }
          ]
        },
        "survey.html": {
          "checks": [
            {
              "type": "heading",
              "rule": "h2 names the page",
              "level": 2,
              "pattern": "\\S"
            },
            {
              "type": "element",
              "rule": "a form with 3+ inputs",
              "tag": "input",
              "within": "form",
              "min": 3
            }
          ]
        },
        "cards.html": {
          "checks": [
            {
              "type": "heading",
              "rule": "h2 names the page",
              "level": 2,
              "pattern": "\\S"
            },
            {
              "type": "element",
              "rule": "6+ themed card images",
              "tag": "img",
              "within": "main",
              "min": 6
            }
          ]
        },
        "inventory.html": {
          "checks": [
            {
              "type": "heading",
              "rule": "h2 names the page",
              "level": 2,
              "pattern": "\\S"
            },
            {
              "type": "element",
              "rule": "inventory rows in a table",
              "tag": "tr",
              "within": "table",
              "min": 3
            }
          ]
        },
        "documentation.html": {
          "checks": [
            {
              "type": "heading",
              "rule": "h2 names the page",
              "level": 2,
              "pattern": "\\S"
            },
            {
              "type": "element",
              "rule": "3+ documentation sections",
              "tag": "section",
              "within": "main",
              "min": 3
            }
          ]
        },
        "product.html": {
          "checks": [
            {
              "type": "heading",
              "rule": "h2 names the page",
              "level": 2,
              "pattern": "\\S"
            },
            {
              "type": "element",
              "rule": "2+ landing sections",
              "tag": "section",
              "within": "main",
              "min": 2
            },
            {
              "type": "element",
              "rule": "a product image",
              "tag": "img",
              "within": "main",
              "min": 1
            }
          ]
        },
        "website_evaluations.html": {
          "checks": [
            {
              "type": "heading",
              "rule": "h2 names the page",
              "level": 2,
              "pattern": "\\S"
            },
            {
              "type": "element",
              "rule": "one section per evaluated site (2+)",
              "tag": "section",
              "within": "main",
              "min": 2
            },
            {
              "type": "heading",
              "rule": "h3 per site (Ex: Apple)",
              "level": 3,
              "pattern": "\\S"
            },
            {
              "type": "html",
              "rule": "site names are links opening in a new tab",
              "pattern": "<h3[^>]*>\\s*<a[^>]*target=\"_blank\"",
              "fail_hint_pattern": "<h3[^>]*>\\s*<a",
              "fail_hint": "the h3 links are there but don't open in a new tab (target=\"_blank\")",
              "present": true
            },
            {
              "type": "element",
              "rule": "4 CRAP items per evaluation (8+ list items)",
              "tag": "li",
              "within": "ul",
              "min": 8
            }
          ]
        },
        "pictures.html": {
          "checks": [
            {
              "type": "heading",
              "rule": "h2 says Everything Pictures/Images",
              "level": 2,
              "pattern": "picture|image"
            },
            {
              "type": "element",
              "rule": "explanatory paragraph in main",
              "tag": "p",
              "within": "main",
              "min": 1
            },
            {
              "type": "element",
              "rule": "one section per image (2)",
              "tag": "section",
              "within": "main",
              "min": 2
            },
            {
              "type": "element",
              "rule": "h3 titling each section",
              "tag": "h3",
              "within": "section",
              "min": 2
            },
            {
              "type": "element",
              "rule": "horizontal rule between the sections",
              "tag": "hr",
              "within": "main",
              "min": 1
            },
            {
              "type": "element",
              "rule": "articles for each demonstration (6+)",
              "tag": "article",
              "within": "section",
              "min": 6
            },
            {
              "type": "element",
              "rule": "h4 on each article",
              "tag": "h4",
              "within": "article",
              "min": 6
            },
            {
              "type": "element",
              "rule": "figures with images",
              "tag": "img",
              "within": "figure",
              "min": 2
            },
            {
              "type": "element",
              "rule": "figcaption on each figure",
              "tag": "figcaption",
              "within": "figure",
              "min": 2
            },
            {
              "type": "html",
              "rule": "original-image links open in a new tab",
              "pattern": "<a[^>]*href=\"[^\"]*images/[^\"]*\"[^>]*target=\"_blank\"",
              "present": true
            },
            {
              "type": "html",
              "rule": "CSS-resizing demonstrations (inline width/height on images)",
              "pattern": "<img[^>]*style=\"[^\"]*(width|height)",
              "present": true
            },
            {
              "type": "html",
              "rule": "image names carry source and photographer (name@photographer_source)",
              "pattern": "images/[a-z0-9-]+@[a-z0-9-]+",
              "present": true
            }
          ]
        },
        "images.html": {
          "checks": [
            {
              "type": "heading",
              "rule": "h2 says Everything Pictures/Images",
              "level": 2,
              "pattern": "picture|image"
            },
            {
              "type": "element",
              "rule": "explanatory paragraph in main",
              "tag": "p",
              "within": "main",
              "min": 1
            },
            {
              "type": "element",
              "rule": "one section per image (2)",
              "tag": "section",
              "within": "main",
              "min": 2
            },
            {
              "type": "element",
              "rule": "h3 titling each section",
              "tag": "h3",
              "within": "section",
              "min": 2
            },
            {
              "type": "element",
              "rule": "horizontal rule between the sections",
              "tag": "hr",
              "within": "main",
              "min": 1
            },
            {
              "type": "element",
              "rule": "articles for each demonstration (6+)",
              "tag": "article",
              "within": "section",
              "min": 6
            },
            {
              "type": "element",
              "rule": "h4 on each article",
              "tag": "h4",
              "within": "article",
              "min": 6
            },
            {
              "type": "element",
              "rule": "figures with images",
              "tag": "img",
              "within": "figure",
              "min": 2
            },
            {
              "type": "element",
              "rule": "figcaption on each figure",
              "tag": "figcaption",
              "within": "figure",
              "min": 2
            },
            {
              "type": "html",
              "rule": "original-image links open in a new tab",
              "pattern": "<a[^>]*href=\"[^\"]*images/[^\"]*\"[^>]*target=\"_blank\"",
              "present": true
            },
            {
              "type": "html",
              "rule": "CSS-resizing demonstrations (inline width/height on images)",
              "pattern": "<img[^>]*style=\"[^\"]*(width|height)",
              "present": true
            },
            {
              "type": "html",
              "rule": "image names carry source and photographer (name@photographer_source)",
              "pattern": "images/[a-z0-9-]+@[a-z0-9-]+",
              "present": true
            }
          ]
        },
        "introduction_form.html": {
          "checks": [
            {
              "type": "heading",
              "rule": "h2 is Introduction Form",
              "level": 2,
              "pattern": "introduction form"
            },
            {
              "type": "heading",
              "rule": "h3 asks to submit the form",
              "level": 3,
              "pattern": "submit"
            },
            {
              "type": "element",
              "rule": "form with the introduction fields (15+ inputs)",
              "tag": "input",
              "within": "form",
              "min": 15
            },
            {
              "type": "html",
              "rule": "fields prefilled with your values",
              "pattern": "<input[^>]*value=\"[^\"]",
              "present": true
            },
            {
              "type": "html",
              "rule": "placeholders shown when the form is empty",
              "pattern": "<input[^>]*placeholder=\"[^\"]",
              "present": true
            },
            {
              "type": "html",
              "rule": "image upload input (defaults to your picture)",
              "pattern": "<input[^>]*type=\"file\"",
              "present": true
            },
            {
              "type": "html",
              "rule": "Submit button",
              "pattern": "<button[^>]*type=\"submit\"",
              "present": true
            },
            {
              "type": "html",
              "rule": "Reset button",
              "pattern": "<button[^>]*type=\"reset\"",
              "present": true
            },
            {
              "type": "html",
              "rule": "Clear button",
              "pattern": "<button[^>]*type=\"button\"",
              "present": true
            },
            {
              "type": "html",
              "rule": "introduction.js in the scripts folder drives the form",
              "pattern": "scripts/introduction\\.js",
              "present": true
            },
            {
              "type": "html",
              "rule": "M13: Generate HTML / JSON / XML buttons",
              "pattern": "generate\\s*(html|json|xml)",
              "fail_hint_pattern": "<button",
              "fail_hint": "buttons found, but no Generate HTML/JSON/XML - required from M13 on",
              "present": true
            }
          ]
        },
        "intro_form.html": {
          "checks": [
            {
              "type": "heading",
              "rule": "h2 is Introduction Form",
              "level": 2,
              "pattern": "introduction form"
            },
            {
              "type": "heading",
              "rule": "h3 asks to submit the form",
              "level": 3,
              "pattern": "submit"
            },
            {
              "type": "element",
              "rule": "form with the introduction fields (15+ inputs)",
              "tag": "input",
              "within": "form",
              "min": 15
            },
            {
              "type": "html",
              "rule": "fields prefilled with your values",
              "pattern": "<input[^>]*value=\"[^\"]",
              "present": true
            },
            {
              "type": "html",
              "rule": "placeholders shown when the form is empty",
              "pattern": "<input[^>]*placeholder=\"[^\"]",
              "present": true
            },
            {
              "type": "html",
              "rule": "image upload input (defaults to your picture)",
              "pattern": "<input[^>]*type=\"file\"",
              "present": true
            },
            {
              "type": "html",
              "rule": "Submit button",
              "pattern": "<button[^>]*type=\"submit\"",
              "present": true
            },
            {
              "type": "html",
              "rule": "Reset button",
              "pattern": "<button[^>]*type=\"reset\"",
              "present": true
            },
            {
              "type": "html",
              "rule": "Clear button",
              "pattern": "<button[^>]*type=\"button\"",
              "present": true
            },
            {
              "type": "html",
              "rule": "introduction.js in the scripts folder drives the form",
              "pattern": "scripts/introduction\\.js",
              "present": true
            },
            {
              "type": "html",
              "rule": "M13: Generate HTML / JSON / XML buttons",
              "pattern": "generate\\s*(html|json|xml)",
              "fail_hint_pattern": "<button",
              "fail_hint": "buttons found, but no Generate HTML/JSON/XML - required from M13 on",
              "present": true
            }
          ]
        }
      },
      "match_dirs": [
        "itis3135",
        "web115",
        "web-115",
        "web215",
        "web-215",
        "web250",
        "web-250",
        "itsc1110",
        "cis110",
        "cis-110"
      ],
      "cert_started_patterns": [
        "href=\"survey.html\"",
        "href=\"cards.html\"",
        "href=\"inventory.html\"",
        "href=\"documentation.html\"",
        "href=\"product.html\""
      ]
    },
    "hobby": {
      "match_dirs": [
        "hobby"
      ],
      "site_checks": [
        {
          "type": "element",
          "rule": "sections act as the pages (Who/What/When/Where/How/Why + AI Prompts)",
          "tag": "section",
          "min": 6
        },
        {
          "type": "element",
          "rule": "each section topic has an h2",
          "tag": "h2",
          "min": 6
        },
        {
          "type": "html",
          "rule": "no includes - header/footer written directly in the file",
          "pattern": "data-include|components/(header|footer)",
          "present": false
        },
        {
          "type": "html",
          "rule": "AI Prompts section (or 'only used AI for images' comment)",
          "pattern": "AI Prompts|only used AI for images",
          "present": true
        },
        {
          "type": "html",
          "rule": "embedded stylesheet does the styling",
          "pattern": "<style[^>]*>\\s*[^<\\s]",
          "present": true
        },
        {
          "type": "html",
          "rule": "nav switches sections (SPA-style)",
          "pattern": "<nav",
          "present": true
        },
        {
          "type": "html",
          "rule": "footer links to your design firm",
          "pattern": "<footer[\\s\\S]*?href=\"\\.\\./",
          "present": true,
          "fail_hint_pattern": "<footer[\\s\\S]*?href=\"[^\"]*/[a-z0-9-]+\\.[a-z]{2,4}/",
          "fail_hint": "firm is linked with an absolute URL - use a relative link (../yourfirm.tld/)"
        }
      ],
      "pages": {},
      "title_divider": "unique"
    },
    "designfirm": {
      "match_pattern": "\\.[a-z]{2,24}/",
      "site_checks": [
        {
          "type": "heading",
          "rule": "h1 is the firm name",
          "level": 1,
          "pattern": "\\S"
        },
        {
          "type": "element",
          "rule": "logo next to the h1",
          "tag": "img",
          "within": "header",
          "min": 1
        },
        {
          "type": "heading",
          "rule": "h2 is the page name (Services/Home/etc.)",
          "level": 2,
          "pattern": "\\S"
        },
        {
          "type": "element",
          "rule": "paragraphs describing company and client offer",
          "tag": "p",
          "within": "main",
          "min": 2
        },
        {
          "type": "element",
          "rule": "5-10 amazing things as a list",
          "tag": "li",
          "within": "ul",
          "min": 5
        },
        {
          "type": "css",
          "rule": "non-standard bullet style",
          "pattern": "list-style|::marker|li\\s*::?before",
          "present": true
        },
        {
          "type": "text",
          "rule": "contact phone number",
          "pattern": "\\(?\\d{3}\\)?[-. ]\\d{3}[-. ]\\d{4}",
          "present": true
        },
        {
          "type": "html",
          "rule": "contact email present",
          "pattern": "[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,}",
          "present": true
        },
        {
          "type": "html",
          "rule": "footer 'Page created by ...' with firm name + certification",
          "pattern": "created by|designed by",
          "present": true
        },
        {
          "type": "html",
          "rule": "no links back to course pages (standalone site)",
          "pattern": "contract\\.html|introduction\\.html|about\\.html",
          "present": false
        },
        {
          "type": "css",
          "rule": "styling is embedded (single page)",
          "pattern": "\\S",
          "present": true
        }
      ],
      "pages": {}
    },
    "mascot": {
      "match": "unrecognized folder, any location (code-side; folder is the student's mascot name)",
      "exclude_dirs": [
        "web123",
        "images",
        "components",
        "styles",
        "scripts",
        "standards",
        "evaluator",
        "GRADING-intros"
      ],
      "site_checks": [
        {
          "type": "text",
          "rule": "no course links (github, freecodecamp, codecademy) on the business site",
          "pattern": "github\\.com|freecodecamp\\.org|codecademy\\.com",
          "present": false
        }
      ],
      "pages": {}
    },
    "crappy": {
      "match_dirs": [
        "stuff"
      ],
      "inverse": true,
      "comment": "Sin checklist - PASS means the badness is present. Code-side checks add filename sins, heading order, inline-style count, and (JS) aspect distortion.",
      "site_checks": [
        {
          "type": "html",
          "rule": "image forced to a size with width/height attributes",
          "pattern": "<img[^>]*width=[^>]*height=",
          "present": true
        },
        {
          "type": "html",
          "rule": "enormous terrible alt text",
          "pattern": "alt=\"[^\"]{80,}\"",
          "present": true
        },
        {
          "type": "html",
          "rule": "caption is a heading (not figcaption) after the image",
          "pattern": "<img[^>]*>\\s*<h\\d",
          "present": true
        },
        {
          "type": "html",
          "rule": "Papyrus somewhere",
          "pattern": "papyrus",
          "present": true
        },
        {
          "type": "html",
          "rule": "Comic Sans somewhere else",
          "pattern": "comic sans",
          "present": true
        },
        {
          "type": "html",
          "rule": "a serif font too",
          "pattern": "font-family[^;]{0,60}?(?:\\b(?:times new roman|georgia|garamond|cambria|palatino|book antiqua|bookman|constantia|didot|baskerville|rockwell|pt serif|playfair display|merriweather|lora|noto serif|source serif|bitter|crimson text|eb garamond|libre baskerville|cardo|vollkorn|century schoolbook|goudy old style|calisto mt|cochin|perpetua|sabon|minion pro)\\b|(?<!sans-)(?<!sans )\\bserif\\b)",
          "present": true
        },
        {
          "type": "html",
          "rule": "black font color",
          "pattern": "color:\\s*(black|#000)",
          "present": true
        },
        {
          "type": "html",
          "rule": "white background",
          "pattern": "background(-color)?:\\s*(white|#fff)",
          "present": true
        },
        {
          "type": "css",
          "rule": "default link colors (no a selector anywhere)",
          "pattern": "(^|[^-\\w])a\\s*[,{:]",
          "present": false
        },
        {
          "type": "html",
          "rule": "centered bulleted list",
          "pattern": "<ul[^>]*center|ul[^{}]*\\{[^}]*text-align:\\s*center",
          "present": true
        },
        {
          "type": "html",
          "rule": "centered paragraph",
          "pattern": "<p[^>]*center|(^|[^-\\w])p[^{}]*\\{[^}]*text-align:\\s*center",
          "present": true
        },
        {
          "type": "html",
          "rule": "comically large font size",
          "pattern": "font-size:\\s*(?:([4-9]\\d|\\d{3,})px|(?:[3-9]|\\d{2,})(?:\\.\\d+)?r?em|(?:[3-9]\\d|\\d{3,})pt|(?:[5-9]|\\d{2,})(?:\\.\\d+)?v[wh]|(?:[3-9]\\d{2}|\\d{4,})%)",
          "present": true
        },
        {
          "type": "html",
          "rule": "underlined things that aren't links",
          "pattern": "<(?!a[\\s>])[a-z]+[^>]*text-decoration:\\s*underline|<u[\\s>]",
          "present": true
        },
        {
          "type": "html",
          "rule": "text with its own background color",
          "pattern": "<(p|span|h\\d)[^>]*background",
          "present": true
        },
        {
          "type": "html",
          "rule": "extra blank lines jammed in",
          "pattern": "(<br[^>]*>\\s*){2,}|&nbsp;\\s*</p>|<p>\\s*(&nbsp;|\\s)*</p>",
          "present": true
        },
        {
          "type": "html",
          "rule": "a very large border",
          "pattern": "border[^;:]*:\\s*[^;]*\\d{2,}px",
          "present": true
        },
        {
          "type": "html",
          "rule": "each border side different",
          "pattern": "border-top[^;]*;[\\s\\S]{0,200}border-(left|right|bottom)",
          "present": true
        }
      ],
      "pages": {}
    },
    "personal": {
      "match": "webspace root (host-aware, code-side)",
      "comment": "M3A: the personal home page. Code checks: personal h1/title, single course link, no shared assets, embedded styles.",
      "site_checks": [],
      "pages": {}
    }
  }
}