diff --git a/blxGff3Parser.c b/blxGff3Parser.c
index d89bd5b7c4446d36a29cb7257c95f549be6695bc..b7645e7064237149da5c0de4b6e4eb22cacf26b9 100644
--- a/blxGff3Parser.c
+++ b/blxGff3Parser.c
@@ -532,6 +532,31 @@ static void parseAttributes(char *attributes,
 }
 
 
+/* Utility to remove any GFF3 escape characters from the given string and replace them with
+ * the real characters they represent. Takes into account special chars for generic attributes,
+ * and also the Target-name and ID attributes if requested */
+static void replaceGff3EscapeChars(char **text, const gboolean targetNameAtt, const gboolean idAtt)
+{
+  GString *gstr = g_string_new(*text);
+
+  seqtools_g_string_replace(gstr, "%3b", ";");
+  seqtools_g_string_replace(gstr, "%3d", "=");
+  seqtools_g_string_replace(gstr, "%09", "\t");
+
+  /* Target-name and ID attributes additionally have whitespace escaped */
+  if (targetNameAtt || idAtt)
+    seqtools_g_string_replace(gstr, "%20", " ");
+    
+  /* ID attribute additional has ">" escaped */
+  if (idAtt)
+    seqtools_g_string_replace(gstr, "%e3", ">");
+    
+  g_free(*text);
+  *text = gstr->str;
+  g_string_free(gstr, FALSE);
+}
+
+
 /* Parse a tag/data pair of the format "tag=data" */
 static void parseTagDataPair(char *text,
                              const int lineNum,
@@ -577,6 +602,7 @@ static void parseTagDataPair(char *text,
       else if (!strcmp(tokens[0], "url"))
         {
           gffData->url = g_strdup(tokens[1]);
+          replaceGff3EscapeChars(&gffData->url, FALSE, FALSE);
         }
       else
         {
diff --git a/detailview.c b/detailview.c
index dd0593db3627e64ccda4d2f0c6be3f6b7830f7e0..74347e4fa7ca42bf7b5b7458a2754ea101d467d7 100755
--- a/detailview.c
+++ b/detailview.c
@@ -1732,15 +1732,16 @@ static void getVariationDisplayRange(const MSP *msp,
  * when outlining it by the extent of the found variation. Also sets drawBackground to true
  * if the background of the base should be highlighted in the variation color (i.e. if the base
  * is part of a selected variation). */
-static gboolean coordAffectedByVariation(const int dnaIdx,
-                                         const BlxStrand strand, 
-                                         const MSP *mspList, 
-                                         BlxViewContext *bc,
-                                         gboolean *drawStartBoundary, 
-                                         gboolean *drawEndBoundary, 
-                                         gboolean *drawTopBoundary, 
-                                         gboolean *drawBottomBoundary,
-                                         gboolean *drawBackground)
+gboolean coordAffectedByVariation(const int dnaIdx,
+                                  const BlxStrand strand, 
+                                  const MSP *mspList, 
+                                  BlxViewContext *bc,
+                                  const MSP **mspOut, /* the variation we found */
+                                  gboolean *drawStartBoundary, 
+                                  gboolean *drawEndBoundary, 
+                                  gboolean *drawTopBoundary, 
+                                  gboolean *drawBottomBoundary,
+                                  gboolean *drawBackground)
 {
   gboolean result = FALSE;
   
@@ -1753,6 +1754,9 @@ static gboolean coordAffectedByVariation(const int dnaIdx,
         {
           result = TRUE;
           
+          if (mspOut)
+            *mspOut = msp;
+          
           /* We draw the start boundary of this base if it's the first base in the variation's range 
            * and the end boundary if it's the last, unless it's a zero-length feature, i.e. an insertion
            * site; in this case we draw a line to the right of the coord in the direction of the
@@ -1760,23 +1764,22 @@ static gboolean coordAffectedByVariation(const int dnaIdx,
           const gboolean isFirst = (dnaIdx == msp->qRange.min);
           const gboolean isLast = (dnaIdx == msp->qRange.max);
           const gboolean isZeroLen = mspIsZeroLenVariation(msp);
-          
-          *drawStartBoundary = (isZeroLen ? isLast : isFirst);
-          *drawEndBoundary = (isZeroLen ? isFirst : isLast);
 
-          /* Swap left and right if the display is reversed */
-          if (bc->displayRev)
-            {
-              gboolean tmp = *drawStartBoundary;
-              *drawStartBoundary = *drawEndBoundary;
-              *drawEndBoundary = tmp;
-            }
+          if (drawStartBoundary)
+            *drawStartBoundary = (isZeroLen != bc->displayRev ? isLast : isFirst);
+            
+          if (drawEndBoundary)
+            *drawEndBoundary = (isZeroLen != bc->displayRev ? isFirst : isLast);
 
-          *drawTopBoundary = !isZeroLen;
-          *drawBottomBoundary = !isZeroLen;
+          if (drawTopBoundary)
+            *drawTopBoundary = !isZeroLen;
+            
+          if (drawBottomBoundary)
+            *drawBottomBoundary = !isZeroLen;
 
           /* Highlight the background if the variation is selected (unless it's a zero-length variation) */
-          *drawBackground = !isZeroLen && blxContextIsSeqSelected(bc, msp->sSequence);
+          if (drawBackground)
+            *drawBackground = !isZeroLen && blxContextIsSeqSelected(bc, msp->sSequence);
           
           break;
 	}
@@ -1947,7 +1950,7 @@ void drawHeaderChar(BlxViewContext *bc,
     {
       gboolean drawBackground = TRUE;
     
-      if (coordAffectedByVariation(dnaIdx, strand, bc->mspList, bc, &drawLeft, &drawRight, &drawTop, &drawBottom, &drawBackground))
+      if (coordAffectedByVariation(dnaIdx, strand, bc->mspList, bc, NULL, &drawLeft, &drawRight, &drawTop, &drawBottom, &drawBackground))
         {
           /* The coord is affected by a SNP. Outline it in the "selected" SNP color
            * (which is darker than the normal color) */
@@ -2914,19 +2917,53 @@ static gboolean onButtonPressSnpTrack(GtkWidget *snpTrack, GdkEventButton *event
   {
     case 1:
     {
-      /* Select the SNP that was clicked on.  */
       GtkWidget *detailView = GTK_WIDGET(data);
-      blxWindowDeselectAllSeqs(detailViewGetBlxWindow(detailView));
+      GtkWidget *blxWindow = detailViewGetBlxWindow(detailView);
+      
+      if (event->type == GDK_BUTTON_PRESS) /* first click */
+        {
+          /* Select the variation that was clicked on.  */
+          blxWindowDeselectAllSeqs(blxWindow);
 
-      /* The SNP track is not the same width as the sequence column, so pass the
-       * sequence column header so that we can convert to the correct coords */
-      DetailViewColumnInfo *seqColInfo = detailViewGetColumnInfo(detailView, BLXCOL_SEQUENCE);
+          /* The SNP track is not the same width as the sequence column, so pass the
+           * sequence column header so that we can convert to the correct coords */
+          DetailViewColumnInfo *seqColInfo = detailViewGetColumnInfo(detailView, BLXCOL_SEQUENCE);
+
+          selectClickedSnp(snpTrack, seqColInfo->headerWidget, detailView, event->x, event->y, FALSE, TRUE, UNSET_INT); /* SNPs are always expanded in the SNP track */
+          
+          refreshDetailViewHeaders(detailView);
+          callFuncOnAllDetailViewTrees(detailView, refreshTreeHeaders, NULL);
+        }      
+      else if (event->type == GDK_2BUTTON_PRESS) /* double-click */
+        {
+          /* If a variation was double-clicked, open its URL in a browser. If multiple are selected,
+           * use the last-selected one. */
+          GList *seqItem = g_list_last(blxWindowGetSelectedSeqs(blxWindow));
+          
+          if (seqItem)
+            {
+              BlxSequence *seq = (BlxSequence*)(seqItem->data);
+              
+              /* (The url lives in the MSP, and there is only one MSP in a BlxSequence for a variation,
+               * so perhaps the url should be moved to the BlxSequence...) */
+              if (seq->type == BLXSEQUENCE_VARIATION && g_list_length(seq->mspList) > 0)
+                {
+                  const MSP const *msp = (const MSP const *)(seq->mspList->data);
+                  
+                  if (msp->url)
+                    {
+                      GError *error = NULL;
+                      seqtoolsLaunchWebBrowser(msp->url, &error);
+                      reportAndClearIfError(&error, G_LOG_LEVEL_CRITICAL);
+                    }
+                  else
+                    {
+                      g_warning("Variation '%s' does not have a URL.\n", mspGetSName(msp));
+                    }
+                }
+            }
+        }
 
-      selectClickedSnp(snpTrack, seqColInfo->headerWidget, detailView, event->x, event->y, FALSE, TRUE, UNSET_INT); /* SNPs are always expanded in the SNP track */
-      
-      refreshDetailViewHeaders(detailView);
-      callFuncOnAllDetailViewTrees(detailView, refreshTreeHeaders, NULL);
-      
       handled = TRUE;
       break;
     }
diff --git a/detailview.h b/detailview.h
index 4c0b7698f990f0235c8a39615ad25e339ac1d533..ed3d841ed3bb215f8e0d708a674a2f92a3bf74e3 100644
--- a/detailview.h
+++ b/detailview.h
@@ -181,6 +181,17 @@ GHashTable*             getRefSeqBasesToHighlight(GtkWidget *detailView, const I
 void                    drawColumnSeparatorLine(GtkWidget *widget, GdkDrawable *drawable, GdkGC *gc, const BlxViewContext *bc);
 gboolean                onExposeGenericHeader(GtkWidget *headerWidget, GdkEventExpose *event, gpointer data);
 
+gboolean                coordAffectedByVariation(const int dnaIdx,
+                                                 const BlxStrand strand, 
+                                                 const MSP *mspList, 
+                                                 BlxViewContext *bc,
+                                                 const MSP **msp,
+                                                 gboolean *drawStartBoundary, 
+                                                 gboolean *drawEndBoundary, 
+                                                 gboolean *drawTopBoundary, 
+                                                 gboolean *drawBottomBoundary,
+                                                 gboolean *drawBackground);
+
 void			drawHeaderChar(BlxViewContext *bc,
 				       DetailViewProperties *properties,
 				       const int dnaIdx,
diff --git a/detailviewtree.c b/detailviewtree.c
index d8f58e3cc9b6e2c4f0ca2567067ca90eb6ea367f..1e7086fc93cf1e20ecbd5b8f3b470d15845b3877 100755
--- a/detailviewtree.c
+++ b/detailviewtree.c
@@ -1401,6 +1401,7 @@ static gboolean onMouseMoveTree(GtkWidget *tree, GdkEventMotion *event, gpointer
       !(event->state & GDK_BUTTON4_MASK) &&
       !(event->state & GDK_BUTTON5_MASK))
     {
+      /* Feed back details about the currently-hovered over row in the detail-view statusbar area */
       GtkStatusbar *statusBar = treeGetStatusBar(tree);
       
       /* Remove any previous message */
@@ -1450,21 +1451,86 @@ static gboolean onMouseMoveTree(GtkWidget *tree, GdkEventMotion *event, gpointer
     }
 }
 
- 
-static gboolean onMouseMoveTreeHeader(GtkWidget *header, GdkEventMotion *event, gpointer data)
+
+/* Get the display index at the given position in the tree header */
+static int treeHeaderGetCoordAtPos(GtkWidget *header, GtkWidget *tree, const int x, const int y)
 {
-  GtkWidget *tree = GTK_WIDGET(data);
+  int baseIdx = UNSET_INT;
   
-  /* The start of the header widget is at the start of the sequence column, so offset the
-   * coords before propagating to the detail view */
   GtkWidget *detailView = treeGetDetailView(tree);
-  IntRange xRange;
-  detailViewGetColumnXCoords(detailView, BLXCOL_SEQUENCE, &xRange);
-  event->x += xRange.min;
+  GtkAdjustment *adjustment = detailViewGetAdjustment(detailView);
 
-  propagateEventMotion(tree, treeGetDetailView(tree), event);
+  if (x >= 0 && x <= header->allocation.width)
+    {
+      /* Get the 0-based char index at x */
+      gint charWidth = detailViewGetCharWidth(detailView);
+      int charIdx = (int)((double)x / (double)charWidth);
+      
+      /* Add the start of the scroll range to convert this to the display index */
+      baseIdx = charIdx + adjustment->value;
+    }
+  else if (x < 0)
+    {
+      baseIdx = adjustment->value;
+    }
+  else if (x > header->allocation.width)
+    {
+      baseIdx = adjustment->value = adjustment->page_size;
+    }
   
-  return FALSE;
+  return baseIdx;
+}
+
+
+static gboolean onMouseMoveTreeHeader(GtkWidget *header, GdkEventMotion *event, gpointer data)
+{
+  gboolean handled = FALSE;
+  GtkWidget *tree = GTK_WIDGET(data);
+
+  if (event->state & GDK_BUTTON2_MASK)
+    {
+      /* Propagate the event to the detail view. The start of the header widget is at 
+       * the start of the sequence column, so offset the coords before propagating. */
+      GtkWidget *detailView = treeGetDetailView(tree);
+      IntRange xRange;
+      detailViewGetColumnXCoords(detailView, BLXCOL_SEQUENCE, &xRange);
+      event->x += xRange.min;
+
+      propagateEventMotion(tree, treeGetDetailView(tree), event);
+      handled = FALSE;
+    }
+  else
+    {
+      /* If we're hovering over a base that's affected by a variation, then feed back info
+       * about the variation to the user. */
+      
+      /* First remove any previous message from the feedback area */
+      GtkStatusbar *statusBar = treeGetStatusBar(tree);
+      guint contextId = gtk_statusbar_get_context_id(GTK_STATUSBAR(statusBar), DETAIL_VIEW_STATUSBAR_CONTEXT);
+      gtk_statusbar_pop(GTK_STATUSBAR(statusBar), contextId);
+      
+      /* Get the index we're hovering over */
+      const int displayIdx = treeHeaderGetCoordAtPos(header, tree, event->x, event->y);
+      GtkWidget *detailView = treeGetDetailView(tree);
+      BlxViewContext *bc = treeGetContext(tree);
+
+      const int dnaIdx = convertDisplayIdxToDnaIdx(displayIdx, bc->seqType, detailViewGetActiveFrame(detailView), treeGetFrame(tree), bc->numFrames, bc->displayRev, &bc->refSeqRange);
+
+      /* See if there's a variation here */
+      const MSP *msp = NULL;
+
+      if (coordAffectedByVariation(dnaIdx, treeGetStrand(tree), bc->mspList, bc, &msp, NULL, NULL, NULL, NULL, NULL))
+        {
+          if (msp && mspGetSName(msp))
+            {
+              char *displayText = mspGetSSeq(msp) ? blxprintf("%s : %s", mspGetSName(msp), mspGetSSeq(msp)) : blxprintf("%s", mspGetSName(msp));
+              gtk_statusbar_push(GTK_STATUSBAR(statusBar), contextId, displayText);
+              g_free(displayText);
+            }
+        }
+    }
+      
+  return handled;
 }
 
 
@@ -1491,18 +1557,36 @@ static gboolean onEnterTree(GtkWidget *tree, GdkEventCrossing *event, gpointer d
 }
 
 
-static gboolean onLeaveTree(GtkWidget *tree, GdkEventCrossing *event, gpointer data)
+/* Clear the detail-view feedback area */
+static void clearStatusbar(GtkWidget *tree)
 {
-  /* Remove any message that was added by mousing over the tree rows */
   GtkStatusbar *statusBar = treeGetStatusBar(tree);
   guint contextId = gtk_statusbar_get_context_id(GTK_STATUSBAR(statusBar), DETAIL_VIEW_STATUSBAR_CONTEXT);
-
   gtk_statusbar_pop(GTK_STATUSBAR(statusBar), contextId);
+}
 
+
+static gboolean onLeaveTree(GtkWidget *tree, GdkEventCrossing *event, gpointer data)
+{
+  /* Remove any statusbar message that was added by mousing over the tree rows */
+  clearStatusbar(tree);
+  
   /* Return true to stop the default handler re-drawing when the focus changes */
   return TRUE;
 }
 
+static gboolean onLeaveTreeHeader(GtkWidget *header, GdkEventCrossing *event, gpointer data)
+{
+  /* Remove any statusbar message that was added by mousing over the tree header */
+  GtkWidget *tree = GTK_WIDGET(data);
+  GtkStatusbar *statusBar = treeGetStatusBar(tree);
+  guint contextId = gtk_statusbar_get_context_id(GTK_STATUSBAR(statusBar), DETAIL_VIEW_STATUSBAR_CONTEXT);
+  
+  gtk_statusbar_pop(GTK_STATUSBAR(statusBar), contextId);
+  
+  /* Return true to stop the default handler re-drawing when the focus changes */
+  return TRUE;
+}
 
 /* Add a row to the given tree containing the given MSP */
 void addMspToTree(GtkWidget *tree, MSP *msp)
@@ -2157,14 +2241,16 @@ static TreeColumnHeaderInfo* createTreeColHeader(GList **columnHeaders,
 
           gtk_widget_add_events(columnHeader, GDK_BUTTON_PRESS_MASK);
           gtk_widget_add_events(columnHeader, GDK_BUTTON_RELEASE_MASK);
-          gtk_widget_add_events(columnHeader, GDK_BUTTON2_MOTION_MASK);
+          gtk_widget_add_events(columnHeader, GDK_POINTER_MOTION_MASK);
+          gtk_widget_add_events(columnHeader, GDK_LEAVE_NOTIFY);
           g_signal_connect(G_OBJECT(columnHeader), "button-press-event",    G_CALLBACK(onButtonPressTreeHeader), tree);
           g_signal_connect(G_OBJECT(columnHeader), "button-release-event",  G_CALLBACK(onButtonReleaseTreeHeader), tree);
           g_signal_connect(G_OBJECT(columnHeader), "motion-notify-event",   G_CALLBACK(onMouseMoveTreeHeader), tree);
-          
+          g_signal_connect(G_OBJECT(columnHeader), "leave-notify-event",    G_CALLBACK(onLeaveTreeHeader), tree);
+
 	  break;
 	}
-	
+
       case BLXCOL_START:
 	{
 	  /* The start column header displays the start index of the current display range */
@@ -2653,7 +2739,7 @@ GtkWidget* createDetailViewTree(GtkWidget *grid,
   g_signal_connect(G_OBJECT(tree), "drag-end",		    G_CALLBACK(onDragEndTree),		NULL);
   g_signal_connect(G_OBJECT(tree), "drag-motion",	    G_CALLBACK(onDragMotionTree),	NULL);
   g_signal_connect(G_OBJECT(tree), "expose-event",	    G_CALLBACK(onExposeDetailViewTree), NULL);
-  
+
   GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree));
   g_signal_connect(G_OBJECT(selection), "changed", G_CALLBACK(onSelectionChangedTree), tree);
   
diff --git a/seqtoolsWebBrowser.c b/seqtoolsWebBrowser.c
index 3f8d16789b77196ee4acc1b15c96c399308a74f9..8b0034a99c83fa063db1ee15fc7021c732917570 100644
--- a/seqtoolsWebBrowser.c
+++ b/seqtoolsWebBrowser.c
@@ -49,7 +49,7 @@ typedef struct
 
 static char *findBrowser(BrowserConfig browsers, BrowserConfig *browser_out, GError **error) ;
 static void makeBrowserCmd(GString *cmd, BrowserConfig best_browser, char *url) ;
-static char *translateURLChars(char *orig_link) ;
+static char *translateURLChars(const char *orig_link) ;
 gboolean seqtools_g_string_replace(GString *string, char *target, char *source);
 
 
@@ -119,7 +119,7 @@ static GQuark err_domain_G = 0 ;
  * @param    error             pointer to NULL GError pointer for return of errors.
  * @return   gboolean          TRUE if launch of browser successful.
  */
-gboolean seqtoolsLaunchWebBrowser(char *link, GError **error)
+gboolean seqtoolsLaunchWebBrowser(const char *link, GError **error)
 {
   gboolean result = FALSE ;
   BrowserConfig best_browser = NULL ;
@@ -294,7 +294,7 @@ static void makeBrowserCmd(GString *cmd, BrowserConfig best_browser, char *url)
  *
  *    mh17: second thoughts: don't use, these rely on quoting: thrid thoughts: no amount of escaped quoting seems to work
  */
-static char *translateURLChars(char *orig_link)
+static char *translateURLChars(const char *orig_link)
 {
   char *url = NULL ;
   
diff --git a/utilities.c b/utilities.c
index ddc575ea6f36647a38a1f1faba81f830a7366cf9..9e6acaedda03937b79a9aee1769e4c7141b99b00 100644
--- a/utilities.c
+++ b/utilities.c
@@ -1074,6 +1074,11 @@ int mspGetSEnd(const MSP const *msp)
   return (mspGetMatchStrand(msp) == mspGetRefStrand(msp) ? msp->sRange.max : msp->sRange.min);
 }
 
+char* mspGetSSeq(const MSP const *msp)
+{
+  return (msp && msp->sSequence && msp->sSequence->sequence ? msp->sSequence->sequence->str : NULL);
+}
+
 /* Return the match sequence name. (Gets it from the BlxSequence if the MSP itself doesn't have
  * a name) */
 const char *mspGetSName(const MSP const *msp)
diff --git a/utilities.h b/utilities.h
index 46605ab809d905142a8428122e4c71a19c12f329..712634b1f42939e11b7b2d91f3ac1421bb8d03c9 100644
--- a/utilities.h
+++ b/utilities.h
@@ -230,6 +230,7 @@ int		      mspGetQStart(const MSP const *msp);
 int		      mspGetQEnd(const MSP const *msp);
 int		      mspGetSStart(const MSP const *msp);
 int		      mspGetSEnd(const MSP const *msp);
+char*                 mspGetSSeq(const MSP const *msp);
 int		      mspGetQRangeLen(const MSP const *msp);
 int		      mspGetSRangeLen(const MSP const *msp);
 int		      mspGetMatchSeqLen(const MSP const *msp);
@@ -394,7 +395,8 @@ void    gtk_text_buffer_set_markup                (GtkTextBuffer *buffer,
 
 
 /* seqtoolsWebBrowser.c */
-gboolean              seqtoolsLaunchWebBrowser(char *link, GError **error);
+gboolean              seqtoolsLaunchWebBrowser(const char *link, GError **error);
+gboolean              seqtools_g_string_replace(GString *string, char *target, char *source);
 
 
 #endif /* _utilities_h_included_ */