diff --git a/pco_chords.nimble b/pco_chords.nimble
index 3701e69..bcff610 100644
--- a/pco_chords.nimble
+++ b/pco_chords.nimble
@@ -1,6 +1,6 @@
 # Package
 
-version       = "0.3.0"
+version       = "0.3.1"
 author        = "Jonathan Bernard"
 description   = "Chord chart formatter compatible with Planning Center Online"
 license       = "MIT"
diff --git a/src/pco_chords.nim b/src/pco_chords.nim
index 6dce4c2..2c994e8 100644
--- a/src/pco_chords.nim
+++ b/src/pco_chords.nim
@@ -40,5 +40,5 @@ when isMainModule:
 
  except:
   fatal getCurrentExceptionMsg()
-  debug getCurrentException().getStackTrace
+  debug getCurrentException().getStackTrace()
   quit(QuitFailure)
diff --git a/src/pco_chordspkg/ast.nim b/src/pco_chordspkg/ast.nim
index 34d6a31..d791c2e 100644
--- a/src/pco_chordspkg/ast.nim
+++ b/src/pco_chordspkg/ast.nim
@@ -20,14 +20,16 @@ type
     ccnkColBreak,
     ccnkPageBreak,
     ccnkTransposeKey,
-    ccnkRedefineKey
+    ccnkRedefineKey,
+    ccnkNone
 
   ChordChartPitch* = enum Af, A, Bf, B, C, Df, D, Ef, E, F, Fs, G
 
   ChordChartChord* = object
     original*: string
     rootPitch*: ChordChartPitch
-    flavor*: string
+    flavor*: Option[string]
+    bassPitch*: Option[ChordChartPitch]
 
   ChordChartNode* = ref object
     case kind: ChordChartNodeKind
@@ -39,8 +41,8 @@ type
     of ccnkWord:
       spaceBefore*: bool
       spaceAfter*: bool
-      chord*: ChordChartChord
-      word*: string
+      chord*: Option[ChordChartChord]
+      word*: Option[string]
     of ccnkNote:
       note*: string
       inclInLyrics*: bool
@@ -50,6 +52,7 @@ type
       transposeSteps*: int
     of ccnkRedefineKey:
       newKey*: ChordChartPitch
+    of ccnkNone: discard
 
   ParserContext = ref object
     lines: seq[string]
@@ -58,6 +61,8 @@ type
     curSection: ChordChartNode
     unparsedLineParts: seq[string]
 
+let EMPTY_CHORD_CHART_NODE* = ChordChartNode(kind: ccnkNone)
+
 func `[]`*(ccmd: ChordChartMetadata, key: string): string =
   ccmd.optionalProps[key]
 
@@ -89,7 +94,9 @@ func `$`*(pitch: ChordChartPitch): string =
 func `+`*(pitch: ChordChartPitch, steps: int): ChordChartPitch =
   cast[ChordChartPitch]((ord(pitch) + steps) mod 12)
 
-func `-`*(a, b: ChordChartPitch): int = ord(a) - ord(b)
+func `-`*(a, b: ChordChartPitch): int =
+  result = ord(a) - ord(b)
+  if result < 0: result += 12
 
 func dump*(m: ChordChartMetadata, indent = ""): string =
   return indent & "Metadata\p" & join(m.pairs --> map(indent & "  " & it.key & ": " & it.value), "\p")
@@ -121,11 +128,20 @@ func dump*(ccn: ChordChartNode, indent = ""): string =
   of ccnkWord:
     result = ""
     if ccn.spaceBefore: result &= " "
-    if ccn.chord.flavor.len > 0:
-      result &= "[" & $ccn.chord.rootPitch & "_" & ccn.chord.flavor & "]"
-    elif ccn.chord.original.len > 0: result &= "[" & ccn.chord.original & "]"
-    if ccn.chord.original.len > 0: result &= "[" & ccn.chord.original & "]"
-    result &= ccn.word
+
+    if ccn.chord.isSome:
+      let chord = ccn.chord.get
+      result &= "["
+      if chord.flavor.isNone and chord.bassPitch.isNone:
+        result &= chord.original
+      else:
+        if chord.flavor.isSome:
+          result &= $chord.rootPitch & "_" & chord.flavor.get
+        if chord.bassPitch.isSome:
+          result &= $chord.rootPitch & "/" & $chord.bassPitch.get
+      result &= "]"
+
+    if ccn.word.isSome: result &= ccn.word.get
     if ccn.spaceAfter: result &= " "
 
   of ccnkNote:
@@ -133,6 +149,9 @@ func dump*(ccn: ChordChartNode, indent = ""): string =
     if not ccn.inclInLyrics: result &= "(chords only) "
     result &= ccn.note
 
+  of ccnkNone:
+    result &= indent & "NONE"
+
 func `$`*(cc: ChordChart): string =
   result = "ChordChart\p" &
     dump(cc.metadata, "  ") & "\p" &
@@ -181,31 +200,35 @@ func parsePitch*(ctx: ParserContext, keyValue: string): ChordChartPitch =
   of "f𝄪", "g", "a𝄫": return ChordChartPitch.G
   else: raise ctx.makeError(keyValue.strip & " is not a recognized key.")
 
+# see regexr.com/70nv1
 let CHORD_REGEX =
-  "([[:upper:]][b#♭♮𝄫𝄪]?)" &                #      chord root
-  "([mM1-9#b♭♮𝄫𝄪Δ+oøoø]|min|maj|aug|dim|sus|6\\/9)*" &  # chord flavor/type
-  "(\\/[[:upper:]])?"                               # optional bass
+  "([A-G1-7][b#♭♮𝄫𝄪]?)" &               #      chord root
+  "(([mM1-9#b♭♮𝄫𝄪Δ+oøoø][0-9]?|min|maj|aug|dim|sus|6\\/9|\\([1-9#b♭]+\\))*)" &  # chord flavor/type
+  "(\\/([A-G1-7][b#♭♮𝄫𝄪]?))?"             # optional bass
 let CHORD_PAT = re(CHORD_REGEX)
 
-proc parseChord*(ctx: ParserContext, chordValue: string): ChordChartChord =
+proc parseChord*(
+    ctx: ParserContext, chordValue: string
+  ): Option[ChordChartChord] =
+
   let m = chordValue.match(CHORD_PAT)
-  if m.isNone:
-    return ChordChartChord(
-      original: chordValue,
-      flavor: "")
+  if m.isNone: none[ChordChartChord]()
   else:
     let flavor =
-      if m.get.captures.contains(1): m.get.captures[1]
-      else: ""
+      if m.get.captures.contains(1) and m.get.captures[1].len > 0:
+        some(m.get.captures[1])
+      else: none[string]()
 
-    let bass =
-      if m.get.captures.contains(2): m.get.captures[2]
-      else: ""
+    let bassPitch =
+      if m.get.captures.contains(4) and m.get.captures[4].len > 0:
+        some(ctx.parsePitch(m.get.captures[4]))
+      else: none[ChordChartPitch]()
 
-    return ChordChartChord(
+    return some(ChordChartChord(
       original: chordValue,
       rootPitch: ctx.parsePitch(m.get.captures[0]),
-      flavor: flavor & bass)
+      flavor: flavor,
+      bassPitch: bassPitch))
 
 let METADATA_LINE_PAT = re"^([^:]+):(.*)$"
 let METADATA_END_PAT = re"^-+$"
@@ -289,7 +312,7 @@ proc parseLineParts(ctx: ParserContext, parts: varargs[string]): seq[ChordChartN
         spaceAfter: true, #FIXME
         spaceBefore: true, #FIXME
         chord: ctx.parseChord(m.get.captures[0]),
-        word: m.get.captures[1]))
+        word: some(m.get.captures[1])))
       result &= ctx.parseLineParts(m.get.captures[2])
       continue
 
@@ -300,7 +323,7 @@ proc parseLineParts(ctx: ParserContext, parts: varargs[string]): seq[ChordChartN
         spaceAfter: false, #FIXME
         spaceBefore: false, #FIXME
         chord: ctx.parseChord(m.get.captures[0]),
-        word: ""))
+        word: none[string]()))
       continue
 
     if p.len > 0:
@@ -308,8 +331,8 @@ proc parseLineParts(ctx: ParserContext, parts: varargs[string]): seq[ChordChartN
         kind: ccnkWord,
           spaceAfter: true, #FIXME
           spaceBefore: true, #FIXME
-          chord: ChordChartChord(original: "", flavor: ""),
-          word: p))
+          chord: none[ChordChartChord](),
+          word: some(p)))
 
 proc parseLine(ctx: ParserContext, line: string): ChordChartNode =
   result = ChordChartNode(kind: ccnkLine)
@@ -322,7 +345,7 @@ proc parseLine(ctx: ParserContext, line: string): ChordChartNode =
         spaceAfter: false, #FIXME
         spaceBefore: false, #FIXME
         chord: ctx.parseChord(it.strip),
-        word: ""))
+        word: none[string]()))
 
   else: result.line = ctx.parseLineParts(line.splitWhitespace)
 
@@ -330,7 +353,7 @@ proc readNote(ctx: var ParserContext, endPat: Regex): string =
   let startLineNum = ctx.curLineNum
   result = ""
 
-  while ctx.lines.len > ctx.curLineNum:
+  while ctx.lines.len > ctx.curLineNum and ctx.hasNextLine:
     let line = ctx.nextLine
     let m = line.find(endPat)
     if m.isSome:
@@ -362,7 +385,7 @@ proc parseBody(ctx: var ParserContext): seq[ChordChartNode] =
         # if this is the first character of the line, then let's parse the note
         result.add(ChordChartNode(
           kind: ccnkNote,
-          inclInLyrics: m.get.match .len < 2,
+          inclInLyrics: m.get.match.len < 2,
           note: ctx.readNote(NOTE_END_PAT)))
 
     m = line.match(TRANSPOSE_PAT)
@@ -384,6 +407,16 @@ proc parseBody(ctx: var ParserContext): seq[ChordChartNode] =
         newKey: cast[ChordChartPitch](newKeyInt)))
       continue
 
+    m = line.match(COL_BREAK_PAT)
+    if m.isSome:
+      result.add(ChordChartNode(kind: ccnkColBreak))
+      continue
+
+    m = line.match(PAGE_BREAK_PAT)
+    if m.isSome:
+      result.add(ChordChartNode(kind: ccnkPageBreak))
+      continue
+
     m = line.match(SECTION_LINE_PAT)
     if m.isSome:
       let captures = m.get.captures.toSeq
@@ -397,18 +430,6 @@ proc parseBody(ctx: var ParserContext): seq[ChordChartNode] =
         ctx.curSection.sectionContents &= ctx.parseLineParts(captures[3].get)
       continue
 
-    # FIXME: as implemented, this will not allow column breaks within a section
-    m = line.match(COL_BREAK_PAT)
-    if m.isSome:
-      result.add(ChordChartNode(kind: ccnkColBreak))
-      continue
-
-    # FIXME: as implemented, this will not allow page breaks within a section
-    m = line.match(PAGE_BREAK_PAT)
-    if m.isSome:
-      result.add(ChordChartNode(kind: ccnkPageBreak))
-      continue
-
     else:
       addToCurSection(ctx.parseLine(line))
       continue
@@ -418,6 +439,7 @@ proc parseChordChart*(s: string): ChordChart =
   var parserCtx = ParserContext(
     lines: s.splitLines,
     curLineNum: -1,
+    curSection: EMPTY_CHORD_CHART_NODE,
     unparsedLineParts: @[])
 
   let metadata = parseMetadata(parserCtx)
diff --git a/src/pco_chordspkg/cliconstants.nim b/src/pco_chordspkg/cliconstants.nim
index 9a282f4..6259a29 100644
--- a/src/pco_chordspkg/cliconstants.nim
+++ b/src/pco_chordspkg/cliconstants.nim
@@ -1,4 +1,4 @@
-const PCO_CHORDS_VERSION* = "0.3.0"
+const PCO_CHORDS_VERSION* = "0.3.1"
 
 const USAGE* = """Usage:
   pco_chords [options]
diff --git a/src/pco_chordspkg/html.nim b/src/pco_chordspkg/html.nim
index 6a95fdf..66ae9b7 100644
--- a/src/pco_chordspkg/html.nim
+++ b/src/pco_chordspkg/html.nim
@@ -1,4 +1,4 @@
-import std/logging, std/os, std/strutils
+import std/[logging, options, os, strutils]
 import zero_functional
 
 import ./ast
@@ -8,6 +8,7 @@ type
     chart: ChordChart
     currentKey: ChordChartPitch
     sourceKey: ChordChartPitch
+    currentSection: ChordChartNode
 
 const DEFAULT_STYLESHEET* = """
 
 """
 
 proc transpose(ctx: FormatContext, chord: ChordChartChord): string =
   let distance = ctx.currentKey - ctx.sourceKey
-  if distance != 0: return $(chord.rootPitch + distance) & chord.flavor
-  else: return chord.original
+
+  if distance != 0:
+    result = $(chord.rootPitch + distance)
+    if chord.flavor.isSome: result &= chord.flavor.get
+    if chord.bassPitch.isSome: result &= "/" & $(chord.bassPitch.get + distance)
+    #debug "transposed " & $ctx.sourceKey & " -> " & $ctx.currentKey &
+    #  " (distance " & $distance & ")\p\tchord: " & $chord & " to " & result
+
+  else: result = chord.original
 
 proc toHtml(ctx: var FormatContext, node: ChordChartNode, indent: string): string =
   result = ""
   case node.kind
   of ccnkSection:
+    ctx.currentSection = node
     result &= indent & "" & node.sectionName & "
\p"
 
@@ -75,6 +98,7 @@ proc toHtml(ctx: var FormatContext, node: ChordChartNode, indent: string): strin
     result &= contents.join("\p")
 
     result &= indent & "