diff --git a/ui (gen4)/features/base.js b/ui (gen4)/features/base.js
index c24985e3..33a80fba 100755
--- a/ui (gen4)/features/base.js	
+++ b/ui (gen4)/features/base.js	
@@ -95,6 +95,8 @@ actions.Actions({
 		// between ribbons...
 		//
 		// supported modes:
+		//
+		// XXX should this be here???
 		'ribbon-focus-modes': [
 			'visual',	// select image closest visually
 			'order',	// select image closest to current in order
diff --git a/ui (gen4)/features/ui.js b/ui (gen4)/features/ui.js
index 5a9fc57f..7f3dc6e4 100755
--- a/ui (gen4)/features/ui.js	
+++ b/ui (gen4)/features/ui.js	
@@ -2385,6 +2385,7 @@ module.AutoSingleImage = core.ImageGridFeatures.Feature({
 // XXX add tap/click to focus...
 // XXX add pinch-zoom...
 // XXX add vertical scroll...
+// XXX disable drag in single image mode unless image is larger than the screen...
 // XXX BUG: current image indicator gets shown in random places...
 var DirectControljQ = 
 module.DirectControljQ = core.ImageGridFeatures.Feature({
@@ -2478,6 +2479,8 @@ module.DirectControljQ = core.ImageGridFeatures.Feature({
 })
 
 
+// XXX disable drag in single image mode unless image is larger than the screen...
+// XXX do not use this for production -- GSAp has bad license...
 var DirectControlGSAP = 
 module.DirectControlGSAP = core.ImageGridFeatures.Feature({
 	title: '',
@@ -2527,6 +2530,8 @@ module.DirectControlGSAP = core.ImageGridFeatures.Feature({
 		// XXX fast but uses messes up positioning...
 		// 		...setting type: 'left' will fix this but make things 
 		// 		really slow (as slow as jQuery.ui.draggable(..))...
+		// XXX shifting to using transforms for centering fixes the align
+		// 		issue but makes the initial move jump...
 		['updateRibbon', 
 			function(_, target){
 				var that = this
@@ -2536,22 +2541,12 @@ module.DirectControlGSAP = core.ImageGridFeatures.Feature({
 				if(r.length > 0 && !r.hasClass('draggable')){
 					r.addClass('draggable')
 
-					var o
+					var o, scale
 
 					Draggable.create(r, {
 						type: 'x',
-						onDragStart: function(){
-							o = r.position().left
-						},
+						cursor: 'auto',
 						onDragEnd: function(){
-							var l = r.position().left
-							l += o - l
-
-							that.ribbons.preventTransitions(r)
-							r[0].style.left = l
-							r[0].style.transform = 'translate3d(0, 0, 0)'
-							that.ribbons.restoreTransitions(r)
-
 							var c = that.ribbons.getImageByPosition('center', r)
 							that
 								.updateRibbon(c)
diff --git a/ui (gen4)/index.html b/ui (gen4)/index.html
index b7a3f3c1..dc0711d5 100755
--- a/ui (gen4)/index.html	
+++ b/ui (gen4)/index.html	
@@ -176,6 +176,7 @@ typeof(require) != 'undefined' && require('nw.gui').Window.get().showDevTools()
 
 
 
+
 
 
 
diff --git a/ui (gen4)/lib/jli.js b/ui (gen4)/lib/jli.js
index 7bcf461e..d2812a69 100755
--- a/ui (gen4)/lib/jli.js	
+++ b/ui (gen4)/lib/jli.js	
@@ -217,6 +217,8 @@ function setElementTransform(elem, offset, scale, duration){
 	elem = $(elem)
 	//var t3d = USE_3D_TRANSFORM ? 'translateZ(0)' : ''
 	var t3d = USE_3D_TRANSFORM ? 'translate3d(0,0,0)' : ''
+	//var translate = USE_3D_TRANSFORM ? 'translate3d' : 'translate'
+	var translate = 'translate'
 
 	if(offset == null){
 		offset = getElementOffset(elem)
@@ -237,9 +239,12 @@ function setElementTransform(elem, offset, scale, duration){
 		var scale = getElementScale(elem)
 	}
 	if(USE_TRANSFORM){
-		var transform = 'translate('+ 
+		var transform = translate+'('+ 
 				Math.round(offset.left) +'px, '+
-				Math.round(offset.top) +'px) scale('+ scale +') ' + t3d
+				//Math.round(offset.top) +'px'+ (USE_3D_TRANSFORM && ', 0px' || '') +') '
+				Math.round(offset.top) +'px) '
+			+'scale('+ scale +') '
+			+ t3d
 		elem.css({
 			'-ms-transform' : transform, 
 			'-webkit-transform' : transform, 
@@ -253,7 +258,10 @@ function setElementTransform(elem, offset, scale, duration){
 			top: ''
 		}, duration)
 	} else {
-		var transform = 'translate(0px, 0px) scale('+ scale +') ' + t3d
+		//var transform = translate+'(0px, 0px'+ (USE_3D_TRANSFORM && ', 0px' || '') +') '
+		var transform = translate+'(0px, 0px) '
+			+'scale('+ scale +') '
+			+ t3d
 		elem.css({
 			// NOTE: this will be wrong during a transition, that's why we 
 			// 		can pass the pre-calculated offset as an argument...
@@ -465,8 +473,8 @@ function stopAnimation(elem){
 
 // XXX account for other transitions...
 // XXX make a sync version...
-function setElementOffset(elem, l, t){
-	return setElementTransform(elem, [l, t])
+function setElementOffset(elem, l, t, scale){
+	return setElementTransform(elem, [l, t], scale)
 }
 
 
diff --git a/ui (gen4)/lib/util.js b/ui (gen4)/lib/util.js
index 5155b8d2..77b1e877 100755
--- a/ui (gen4)/lib/util.js	
+++ b/ui (gen4)/lib/util.js	
@@ -11,6 +11,14 @@ define(function(require){ var module = {}
 
 /*********************************************************************/
 
+// convert JS arguments to Array...
+var args2array =
+module.args2array =
+function(args){
+	//return Array.apply(null, args)
+	return [].slice.call(args)
+}
+
 // Quote a string and convert to RegExp to match self literally.
 var quoteRegExp =
 module.quoteRegExp =
@@ -67,6 +75,312 @@ function(path){
 
 
 
+/*********************************************************************/
+
+var _transform_parse = {
+	// 2d transforms:
+	//martix: [],
+
+	translate: ['left|0', 'top|0'],
+	translateX: ['left'],
+	translateY: ['top'],
+
+	scale: [
+		['scale'],
+		['scaleX|scale', 'scaleY|scale'],
+	],
+	scaleX: ['scaleX'],
+	scaleY: ['scaleY'],
+
+	rotate: ['rotate'],
+	
+	skew: ['skewX', 'skewY'],
+	skewX: ['skewX'],
+	skewY: ['skewY'],
+
+	// 3d transforms:
+	//martix3d: [],
+
+	translate3d: ['x|0', 'y|0', 'z|0'],
+	translateZ: ['z'],
+
+	scale3d: ['scaleX', 'scaleY', 'scaleZ'],
+	scaleZ: ['scaleZ'],
+
+	// XXX
+	//rotate3d: [x, y, z, angle],
+	// 	rotateX
+	// 	rotateY
+	// 	rotateZ
+	
+	perspective: ['perspective'],
+}
+var _transform_parse_rev = {}
+Object.keys(_transform_parse).forEach(function(func){
+	var args = _transform_parse[func]
+
+	// we got multiple signatures == merge...
+	if(!(args[0] instanceof Array)){
+		args = [args]
+	}
+
+	args
+		// merge lists of args...
+		.reduce(function(a, b){ return [].concat.call(a, b) })
+		.unique()
+		// split alternatives...
+		.map(function(a){ return a.split(/\|/g) })
+		.forEach(function(a){
+			var arg = a[0]
+			var alt = a.slice(1)
+
+			var e = _transform_parse_rev[arg] = _transform_parse_rev[arg] || {}
+
+			e.funcs = e.funcs || []
+			e.funcs.indexOf(func) < 0 && e.funcs.push(func)
+
+			e.alt = e.alt || []
+			// XXX we explicitly support only one alternative now...
+			e.alt = e.alt.concat(alt).unique()
+		})
+})
+
+
+// XXX get vendor...
+
+// 
+// 	Set element transform...
+// 	.transform({..})
+// 		-> element
+//
+// 	Get element transform...
+// 	.transform()
+// 	.transform([, ...])
+// 	.transform([, ...])
+// 		-> data
+//
+// Supported transformations:
+// 	x/y
+// 	scale
+// 	scaleX/scaleY
+// 	origin
+// 	originX/originY
+//
+// NOTE: pixel values are converted to numbers and back by default...
+//
+// XXX this will get/set values only on the first element, is this correct???
+// XXX how do we combine translate(..) and translate3d(..)???
+jQuery.fn.transform = function(){
+	var that = this
+	var args = args2array(arguments)
+
+	// XXX get the browser prefix...
+	var prefix = ''
+
+	// normalize...
+	args = args.length == 0 
+			|| typeof(args[0]) == typeof('str') ? args
+		: args[0].constructor === Array 
+			|| args.length == 1 ? args[0]
+		: args
+
+	var elem = $(this)[0]
+	var origin_str = elem.style[prefix + 'transformOrigin']
+	var transform_str = elem.style[prefix + 'transform']
+
+	// build the current state...
+	// NOTE: we'll need this for both fetching (parsing) and writing 
+	// 		(checking)...
+	var transform = {}
+	var functions = {}
+
+	// origin...
+	var origin = origin_str
+		.split(/\s+/)
+		// XXX add this to transform...
+
+	// transform...
+	transform_str
+		// split functions...
+		.split(/(\w+\([^\)]*)\)/)
+		// remove empty strings...
+		.filter(function(e){ return e.trim().length > 0})
+		// split each function...
+		.map(function(e){ return e
+			// split args...
+			.split(/\s*[\(,\s]\s*/)
+			// cleanup...
+			.filter(function(e){ return e.trim().length > 0 }) })
+		// build the structure...
+		.forEach(function(data){
+			var func = data.shift() 
+			var args = data
+
+			// XXX do we care about function vendor tags here???
+			var spec = _transform_parse[func]
+
+			functions[func] = args
+
+			// we do not know this function...
+			if(spec == null){
+				transform[func] = args
+
+			} else {
+				spec = spec[0] instanceof Array ? spec : [spec]
+				spec.forEach(function(s){ 
+					// skip non-matching signatures...
+					// XXX is this correct???
+					if(s.length != args.length){
+						return
+					}
+					s.forEach(function(e, i){ 
+						// this is for things that have optional arguments
+						// like scale(..)
+						// XXX should we treat this in some special way???
+						if(args[i] == null){
+							return
+						}
+
+						var alternatives = e.split(/\|/g)
+						var k = alternatives.shift().trim()
+
+						transform[k] = args[i].slice(-2) == 'px' 
+								|| /[0-9\.]+/.test(args[i]) ?
+							parseFloat(args[i]) 
+							: args[i] 
+					})
+				})
+			}
+		})
+
+
+	// get data...
+	if(args.constructor === Array){
+		var res = {}
+
+		// return the full transform...
+		if(args.length == 0){
+			return transform
+		}
+
+		args.forEach(function(arg){
+			// direct match in shorthand data...
+			if(arg in transform){
+				res[arg] = transform[arg]
+
+			// try and find an alias...
+			} else if(arg in _transform_parse_rev){
+				var funcs = _transform_parse_rev[arg].funcs
+				var alt = _transform_parse_rev[arg].alt[0]
+
+				// no alternatives...
+				if(!alt){
+					res[arg] = ''
+
+				// explicit number value...
+				} else if(/^[0-9\.]+$/.test(alt)){
+					res[arg] = parseFloat(alt)
+
+				// explicit string value...
+				} else if(/^(['"]).*\1$/.test(alt)){
+					res[arg] = alt.slice(1, -1)
+
+				} else {
+					var v = $(that).transform(alt)
+					res[arg] = v == '' ? alt : v
+				}
+
+
+			// collect from function...
+			} else if(arg in _transform_parse){
+				var v = res[arg] = {}
+				_transform_parse[arg].forEach(function(e){
+					var alternatives = e.split(/\|/g)
+					var k = alternatives.shift().trim()
+
+					v[k] = transform[k] != null ? transform[k] : ''
+				})
+
+			// don't know about this attr...
+			} else {
+				res[arg] = ''
+			}
+		})
+
+		// special case: we asked for a single value...
+		if(args.length == 1){
+			return res[args[0]]
+		}
+		return res
+	
+	// set data...
+	} else {
+		transform = Object.create(transform)
+		Object.keys(args).forEach(function(key){
+			// the changing value already exists...
+			if(key in transform
+					// get one of the shorthand keys...
+					// NOTE: we might need to pack or re-pack the date but we 
+					// 		can't decide here...
+					|| key in _transform_parse_rev
+					// got one of the standard keys...
+					|| key in _transform_parse){
+				transform[key] = args[key]
+
+			// key undefined...
+			} else {
+				console.warn('Ignoring key "%s".', key)
+				transform[key] = args[key]
+			}
+		})
+
+
+		console.log('>>>>', transform)
+
+		// XXX set new values and resolve new functions...
+		// XXX
+		
+
+		// build the value string...
+		var transform_str = ''
+		for(var f in functions){
+			transform_str += f +'('
+				+(functions[f]
+					// XXX test if px value...
+					.map(function(e){ return typeof(e) == typeof(123) ? e + 'px' : e })
+					.join(', '))+') '
+		}
+
+		console.log(transform_str)
+
+		// XXX STUB
+		return functions
+
+		// set the values...
+		elem.style.transform = transform_str
+		elem.style.transformOrigin = origin_str
+	}
+
+	return $(this)
+}
+
+// shorthands...
+jQuery.fn.scale = function(value){
+	if(value){
+		return $(this).transform({scale: value})
+	} else {
+		return $(this).transform('scale')
+	}
+}
+jQuery.fn.origin = function(value){
+	if(value){
+		return $(this).transform({origin: value})
+	} else {
+		return $(this).transform('origin')
+	}
+}
+
+
 /**********************************************************************
 * vim:set ts=4 sw=4 :                                                */
 return module })
diff --git a/ui (gen4)/ribbons.js b/ui (gen4)/ribbons.js
index 7a4d5d77..9a56b250 100755
--- a/ui (gen4)/ribbons.js	
+++ b/ui (gen4)/ribbons.js	
@@ -1946,8 +1946,12 @@ var RibbonsPrototype = {
 		ribbon
 			.css({
 				left: (rl + ((W-w)/2 + image_offset) - il) / scale,
+				//transform: 'translate3d('
+				//	+ ((rl + ((W-w)/2 + image_offset) - il) / scale) + 'px, 0px, 0px)'
 			})
 
+		//setElementOffset(ribbon, ((rl + ((W-w)/2 + image_offset) - il) / scale), 0, 1)
+
 		return this
 	},