diff --git a/.gitignore b/.gitignore
index a2e79d6e0a6ab24f663c963074ea7c0abcb2b861..340fd84abaa2cb02e94046d4011cba5457f2b900 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,4 @@
 *~
 *.pyc
-.DS_Store
 _site
 README.html
diff --git a/css/swc.css b/css/swc.css
index 73bb7268126974569e317d8a989fd5873d81607c..388eadc8d29886dd084aa78e1c625380c24450ae 100644
--- a/css/swc.css
+++ b/css/swc.css
@@ -8,6 +8,10 @@ h1, h2 {
     margin-bottom: 10px;
 }
 
+h1:first-child, h2:first-child {
+    margin-top: 10px;
+}
+
 h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {
     color: inherit;
 }
@@ -111,20 +115,25 @@ h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {
     color: inherit;
 }
 
-/* Objectives and key points */
+/* Objectives, Callout Box and Challenges */
+.objectives, .keypoints, .callout, .challenge {
+    margin: 1em 0;
+    padding: 0em 1em;
+}
+
 .objectives, .keypoints {
     background-color: azure;
     border: 5px solid azure;
-    margin: 1em 0;
-    padding: 0em 1em;
 }
 
-/* Challenges */
+.callout {
+    background-color: #EEE;
+    border: 5px solid #EEE;
+}
+
 .challenge {
     background-color: #CCFFCC;
     border: 5px solid #CCFFCC;
-    margin: 1em 0;
-    padding: 0em 1em;
 }
 
 /* Things to fix. */
@@ -146,14 +155,6 @@ blockquote {
     width: 90%;
 }
 
-/* Callout Box */
-.callout {
-    background-color: #EEE;
-    border: 5px solid #EEE;
-    margin: 1em 0;
-    padding: 0em 1em;
-}
-
 /* Tables used for displaying choices in challenges. */
 table.choices tr td {
     vertical-align : top;
@@ -165,7 +166,8 @@ table.outlined {
 }
 
 /* Code sample */
-pre.sourceCode{
+pre.sourceCode,
+pre.input {
     color: ForestGreen;
 }
 pre.output {
@@ -187,6 +189,22 @@ pre.error {
     line-height: 13pt;
   }
 
+  /* Objectives, Callout Box and Challenges */
+  .objectives, .keypoints {
+      background-color: unset;
+      border: 5px solid;
+  }
+
+  .callout {
+      background-color: unset;
+      border: 5px solid;
+  }
+
+  .challenge {
+      background-color: unset;
+      border: 5px solid;
+  }
+
   p,ul,ol,li,pre,code {
     font-size: 8pt;
     line-height: 9pt;
@@ -195,7 +213,29 @@ pre.error {
   code {
     padding: 0px;
     border: 0px;
-    background: none;
+    background: unset;
+  }
+
+  pre.sourceCode::before,
+  pre.input::before. {
+      content: "Input:";
+  }
+
+  pre.output::before {
+      content: "Output:";
+  }
+
+  pre.error::before {
+      content: "Error:";
+  }
+
+  pre.sourceCode code,
+  pre.input code,
+  pre.output code,
+  pre.error code {
+      display: block;
+      margin-top: 1em;
+      margin-left: 2em;
   }
 
   #github-ribbon {
diff --git a/tools/check.py b/tools/check.py
index 873ed2522290dd9f7ec94dc646026e0ec5474ab9..6621ab3e471a4a8ab9a5ff6d108b94c7bbd42ff9 100755
--- a/tools/check.py
+++ b/tools/check.py
@@ -10,7 +10,6 @@ Contains validators for several kinds of template.
 
 Call at command line with flag -h to see options and usage instructions.
 """
-from __future__ import print_function
 
 import argparse
 import glob
@@ -141,7 +140,7 @@ class MarkdownValidator(object):
         for h in missing_headings:
             logging.error("In {0}: "
                           "Header section is missing expected "
-                          "row {1}".format(self.filename, h))
+                          "row '{1}'".format(self.filename, h))
 
         return has_hrs and all(test_headers) and only_headers
 
@@ -203,33 +202,31 @@ class MarkdownValidator(object):
         return (len(missing_headings) == 0) and \
                valid_order and no_extra and correct_level
 
-    def _validate_one_link(self, link_node):
-        """Logic to validate a single external asset (image or link)
-
+    # Link validation methods
+    def _validate_one_html_link(self, link_node, check_text=False):
+        """
         Any local html file being linked was generated as part of the lesson.
         Therefore, file links (.html) must have a Markdown file
             in the expected folder.
 
         The title of the linked Markdown document should match the link text.
-
-        For other assets (links or images), just verify that a file exists
         """
         dest, link_text = self.ast.get_link_info(link_node)
 
-        if re.match(r"^[\w,\s-]+\.(html?)", dest, re.IGNORECASE):
-            # HTML files in same folder are made from Markdown; special tests
-            fn = dest.split("#")[0]  # Split anchor name from filename
-            expected_md_fn = os.path.splitext(fn)[0] + os.extsep + "md"
-            expected_md_path = os.path.join(self.markdown_dir,
-                                            expected_md_fn)
-            if not os.path.isfile(expected_md_path):
-                logging.error(
-                    "In {0}: "
-                    "The document links to {1}, but could not find "
-                    "the expected markdown file {2}".format(
-                        self.filename, dest, expected_md_path))
-                return False
+        # HTML files in same folder are made from Markdown; special tests
+        fn = dest.split("#")[0]  # Split anchor name from filename
+        expected_md_fn = os.path.splitext(fn)[0] + os.extsep + "md"
+        expected_md_path = os.path.join(self.markdown_dir,
+                                        expected_md_fn)
+        if not os.path.isfile(expected_md_path):
+            logging.error(
+                "In {0}: "
+                "The document links to {1}, but could not find "
+                "the expected markdown file {2}".format(
+                    self.filename, fn, expected_md_path))
+            return False
 
+        if check_text is True:
             # If file exists, parse and validate link text = node title
             with open(expected_md_path, 'rU') as link_dest_file:
                 dest_contents = link_dest_file.read()
@@ -247,37 +244,68 @@ class MarkdownValidator(object):
                         self.filename, dest,
                         link_text, dest_page_title))
                 return False
-        elif not re.match(r"^((https?|ftp)://)", dest, re.IGNORECASE)\
+        return True
+
+    def _validate_one_link(self, link_node, check_text=False):
+        """Logic to validate a single link to a file asset
+
+        Performs special checks for links to a local markdown file.
+
+        For links or images, just verify that a file exists.
+        """
+        dest, link_text = self.ast.get_link_info(link_node)
+
+        if re.match(r"^[\w,\s-]+\.(html?)", dest, re.IGNORECASE):
+            # Validate local html links have matching md file
+            return self._validate_one_html_link(link_node,
+                                                check_text=check_text)
+        elif not re.match(r"^((https?|ftp)://.+)", dest, re.IGNORECASE)\
                 and not re.match(r"^#.*", dest):
             # If not web URL, and not anchor on same page, then
             #  verify that local file exists
             dest_path = os.path.join(self.lesson_dir, dest)
+            dest_path = dest_path.split("#")[0]  # Split anchor from filename
             if not os.path.isfile(dest_path):
+                fn = dest.split("#")[0]  # Split anchor name from filename
                 logging.error(
                     "In {0}: "
                     "Could not find the linked asset file "
                     "{1} in {2}. If this is a URL, it must be "
                     "prefixed with http(s):// or ftp://.".format(
-                        self.filename, dest, dest_path))
+                        self.filename, fn, dest_path))
                 return False
         else:
-            logging.info(
+            logging.debug(
                 "In {0}: "
                 "Skipped validation of link {1}".format(self.filename, dest))
         return True
 
-    def _validate_links(self, links_to_skip=()):
+    def _partition_links(self):
+        """Fetch links in document. If this template has special requirements
+        for link text (eg only some links' text should match dest page title),
+        filter the list accordingly.
+
+        Default behavior: don't check the text of any links"""
+        check_text = []
+        no_check_text = self.ast.find_external_links()
+
+        return check_text, no_check_text
+
+    def _validate_links(self):
         """Validate all references to external content
 
         This includes links AND images: these are the two types of node that
         CommonMark assigns a .destination property"""
-        links = self.ast.find_external_links()
+        check_text, no_check_text = self._partition_links()
 
         valid = True
-        for link_node in links:
-            if link_node.destination not in links_to_skip:
-                res = self._validate_one_link(link_node)
-                valid = valid and res
+        for link_node in check_text:
+            res = self._validate_one_link(link_node, check_text=True)
+            valid = valid and res
+
+        for link_node in no_check_text:
+            res = self._validate_one_link(link_node, check_text=False)
+            valid = valid and res
         return valid
 
     def _run_tests(self):
@@ -309,6 +337,11 @@ class IndexPageValidator(MarkdownValidator):
     DOC_HEADERS = {'layout': vh.is_str,
                    'title': vh.is_str}
 
+    def _partition_links(self):
+        """Check the text of every link in index.md"""
+        check_text = self.ast.find_external_links()
+        return check_text, []
+
     def _validate_intro_section(self):
         """Validate the intro section
 
@@ -339,12 +372,6 @@ class IndexPageValidator(MarkdownValidator):
                     self.filename))
         return intro_section and prereqs_tests
 
-    def _validate_links(self, links_to_skip=('motivation.html',
-                                             'reference.html',
-                                             'discussion.html',
-                                             'instructors.html')):
-        return super(IndexPageValidator, self)._validate_links(links_to_skip)
-
     def _run_tests(self):
         parent_tests = super(IndexPageValidator, self)._run_tests()
         tests = [self._validate_intro_section()]
@@ -371,9 +398,9 @@ class TopicPageValidator(MarkdownValidator):
         if node_tests is False:
             logging.error(
                 "In {0}: "
-                "Learning Objectives should not be empty.".format(
-                    self.filename))
-
+                "Page should contain a blockquoted section with level 2 "
+                "title 'Learning Objectives'. Section should not "
+                "be empty.".format(self.filename))
         return node_tests
 
     def _validate_has_no_headings(self):
@@ -385,17 +412,13 @@ class TopicPageValidator(MarkdownValidator):
         if len(heading_nodes) == 0:
             return True
 
+        # Individual heading msgs are logged by validate_section_heading_order
         logging.error(
             "In {0}: "
             "The topic page should not have sub-headings "
             "outside of special blocks. "
             "If a topic needs sub-headings, "
             "it should be broken into multiple topics.".format(self.filename))
-        for n in heading_nodes:
-            logging.warning(
-                "In {0}: "
-                "The following sub-heading should be removed: {1}".format(
-                    self.filename, n.strings[0]))
         return False
 
     def _run_tests(self):
@@ -422,6 +445,15 @@ class ReferencePageValidator(MarkdownValidator):
                    "title": vh.is_str,
                    "subtitle": vh.is_str}
 
+    def _partition_links(self):
+        """For reference.md, only check that text of link matches
+        dest page subtitle if the link is in a heading"""
+        all_links = self.ast.find_external_links()
+        check_text = self.ast.find_external_links(
+            parent_crit=self.ast.is_heading)
+        dont_check_text = [n for n in all_links if n not in check_text]
+        return check_text, dont_check_text
+
     def _validate_glossary_entry(self, glossary_entry):
         """Validate glossary entry
 
@@ -432,10 +464,10 @@ class ReferencePageValidator(MarkdownValidator):
          terms manually."""
         if len(glossary_entry) < 2:
             logging.error(
-                    "In {0}:"
-                    "Glossary entry must have at least two lines- "
-                    "a term and a definition.".format(
-                        self.filename))
+                "In {0}: "
+                "Glossary entry '{1}' must have at least two lines- "
+                "a term and a definition.".format(
+                    self.filename, glossary_keyword))
             return False
 
         entry_is_valid = True
@@ -443,18 +475,20 @@ class ReferencePageValidator(MarkdownValidator):
             if line_index == 1:
                 if not re.match("^:   ", line):
                     logging.error(
-                            "In {0}:"
-                            "First line of definition must "
-                            "start with ':    '.".format(
-                                self.filename))
+                        "In {0}: "
+                        "At glossary entry '{1}' "
+                        "First line of definition must "
+                        "start with ':    '.".format(
+                            self.filename, glossary_keyword))
                     entry_is_valid = False
             elif line_index > 1:
                 if not re.match("^    ", line):
                     logging.error(
-                            "In {0}:"
-                            "Subsequent lines of definition must "
-                            "start with '    '.".format(
-                                self.filename))
+                        "In {0}: "
+                        "At glossary entry '{1}' "
+                        "Subsequent lines of definition must "
+                        "start with '     '.".format(
+                            self.filename,  glossary_keyword, ))
                     entry_is_valid = False
         return entry_is_valid
 
@@ -492,6 +526,15 @@ class InstructorPageValidator(MarkdownValidator):
                    "title": vh.is_str,
                    "subtitle": vh.is_str}
 
+    def _partition_links(self):
+        """For instructors.md, only check that text of link matches
+        dest page subtitle if the link is in a heading"""
+        all_links = self.ast.find_external_links()
+        check_text = self.ast.find_external_links(
+            parent_crit=self.ast.is_heading)
+        dont_check_text = [n for n in all_links if n not in check_text]
+        return check_text, dont_check_text
+
 
 class LicensePageValidator(MarkdownValidator):
     """Validate LICENSE.md: user should not edit this file"""
diff --git a/tools/test_check.py b/tools/test_check.py
index 0451c8444d914aca6ab9fe2635575f49814dfd34..05a802365a88a47ecff118bf842ae05988b4c0b1 100644
--- a/tools/test_check.py
+++ b/tools/test_check.py
@@ -202,6 +202,18 @@ Paragraph of introductory material.
         self.assertFalse(validator._validate_intro_section())
 
     # TESTS INVOLVING LINKS TO OTHER CONTENT
+    def test_should_check_text_of_all_links_in_index(self):
+        """Text of every local-html link in index.md should
+        match dest page title"""
+        validator = self._create_validator("""
+## [This link is in a heading](reference.html)
+[Topic Title One](01-one.html#anchor)""")
+        links = validator.ast.find_external_links()
+        check_text, dont_check_text = validator._partition_links()
+
+        self.assertEqual(len(dont_check_text), 0)
+        self.assertEqual(len(check_text), 2)
+
     def test_file_links_validate(self):
         """Verify that all links in a sample file validate.
         Involves checking for example files; may fail on "core" branch"""
@@ -295,6 +307,26 @@ minutes: not a number
 ---""")
         self.assertFalse(validator._validate_doc_headers())
 
+    def test_topic_page_should_have_no_headings(self):
+        """Requirement according to spec; may be relaxed in future"""
+        validator = self._create_validator("""
+## Heading that should not be present
+
+Some text""")
+        self.assertFalse(validator._validate_has_no_headings())
+
+    def test_should_not_check_text_of_links_in_topic(self):
+        """Never check that text of local-html links in topic
+        matches dest title """
+        validator = self._create_validator("""
+## [This link is in a heading](reference.html)
+[Topic Title One](01-one.html#anchor)""")
+        links = validator.ast.find_external_links()
+        check_text, dont_check_text = validator._partition_links()
+
+        self.assertEqual(len(dont_check_text), 2)
+        self.assertEqual(len(check_text), 0)
+
     def test_sample_file_passes_validation(self):
         sample_validator = self.VALIDATOR(self.SAMPLE_FILE)
         res = sample_validator.validate()
@@ -377,6 +409,28 @@ class TestInstructorPage(BaseTemplateTest):
     SAMPLE_FILE = os.path.join(MARKDOWN_DIR, "instructors.md")
     VALIDATOR = check.InstructorPageValidator
 
+    def test_should_selectively_check_text_of_links_in_topic(self):
+        """Only verify that text of local-html links in topic
+        matches dest title if the link is in a heading"""
+        validator = self._create_validator("""
+## [Reference](reference.html)
+
+[Topic Title One](01-one.html#anchor)""")
+        check_text, dont_check_text = validator._partition_links()
+
+        self.assertEqual(len(dont_check_text), 1)
+        self.assertEqual(len(check_text), 1)
+
+    def test_link_dest_bad_while_text_ignored(self):
+        validator = self._create_validator("""
+[ignored text](nonexistent.html)""")
+        self.assertFalse(validator._validate_links())
+
+    def test_link_dest_good_while_text_ignored(self):
+        validator = self._create_validator("""
+[ignored text](01-one.html)""")
+        self.assertTrue(validator._validate_links())
+
     def test_sample_file_passes_validation(self):
         sample_validator = self.VALIDATOR(self.SAMPLE_FILE)
         res = sample_validator.validate()
diff --git a/tools/validation_helpers.py b/tools/validation_helpers.py
index 6acc11c3dfccb80167d860c43b3b4cafe343bbbe..864d362a64a569108435364ae58f678fc1419fa1 100644
--- a/tools/validation_helpers.py
+++ b/tools/validation_helpers.py
@@ -121,21 +121,27 @@ class CommonMarkHelper(object):
 
         return dest, link_text
 
-    def find_external_links(self, ast_node=None):
+    def find_external_links(self, ast_node=None, parent_crit=None):
         """Recursive function that locates all references to external content
          under specified node. (links or images)"""
         ast_node = ast_node or self.data
+        if parent_crit is None:
+            # User can optionally provide a function to filter link list
+            # based on where link appears. (eg, only links in headings)
+            # If no filter is provided, accept all links in that node.
+            parent_crit = lambda n: True
 
         # Link can be node itself, or hiding in inline content
         links = [n for n in ast_node.inline_content
-                 if self.is_external(n)]
+                 if self.is_external(n) and parent_crit(ast_node)]
 
         if self.is_external(ast_node):
             links.append(ast_node)
 
         # Also look for links in sub-nodes
         for n in ast_node.children:
-            links.extend(self.find_external_links(n))
+            links.extend(self.find_external_links(n,
+                                                  parent_crit=parent_crit))
 
         return links